Онлайн-конструктор «умного дома»

Онлайн-конструктор «умного дома»

Создаёт web-интерфейс для управления и скетч для ардуино

Будучи не понаслышке знакомым с трудностями, которые испытывают строители «умных домов», решил запилить конструктор, который всё сделает сам, включая скетч для ардуины, и сервер HomestD для обмена данными. Подключение переферии к ардуине остаётся на совести хозяина, однако в скетче написано, что и куда.

Пользователю останется только скачать архив с файлами, распаковать его на целевом устройстве и загрузить в ардуину готовый скетч. Установка каких-либо дополнительных пакетов и серверов не требуется.

HomestD можно использовать на любом компьютере работающем под управлением Онлайн-конструктор «умного дома» Онлайн-конструктор «умного дома» Онлайн-конструктор «умного дома» или на роутере с прошивкой OpenWrt.

Для работы на роутере не потребуются дополнительные накопители (флешка, sd-карта).

Подключение ардуины

… к компьютеру не должно вызвать затруднений, а о том, как подключить ардуину к роутеру (по USB или UARTу) можно прочесть в сети.
При подключении к UARTу никаких пакетов устанавливать не нужно, только подпаяться к контактам и отредактировать файл /etc/inittab.

Пример для TL-MR3020:

nano /etc/inittab

::sysinit:/etc/init.d/rcS S boot
::shutdown:/etc/init.d/rcS K shutdown
#ttyATH0::askfirst:/bin/ash —login

Внешний вид

Идея web-интерфейса достаточно проста и минималистична.

Онлайн-конструктор «умного дома»
Главный экран интерфейса.

Если кому-то не по душе тёмные цвета, то подправить css-файл не составит труда.

На главном экране расположены кнопки с названиями помещений, нажатие на которые открывает панель с кнопками управления этим помещением.

Онлайн-конструктор «умного дома»

Здесь могут располагаться несколько кнопок (D2, D3 и т.д.) для включения чего-либо с возвратом состояния.

Несколько кнопок для отправки сигнала (SENTSIG1 и т.д), не требующего подтверждения.

И несколько полей (INDATA1 и т.д) для приёма каких-либо данных/сигналов.

Крестит закрывает панель.

Названия кнопок можно изменять по своему усмотрению и менять местами.

Пример:

Онлайн-конструктор «умного дома»

Кнопка Info скрывает панель с информацией о работоспособности системы.

Онлайн-конструктор «умного дома»

Надпись Connect! говорит о том, что всё хорошо, а Count update: — счётчик запросов (браузер с определённым интервалом запрашивает у ардуины данные). Интервал можно менять.

Если что-то произойдёт, то на экране появится сообщение ERROR, а в Info будет описана ошибка.

Онлайн-конструктор «умного дома»

Алгоритм работы описан в конце.

Конструктор

Я записал коротенькое видео по работе с конструктором:

Конструктор прост и понятен. Откройте конструктор в соседней вкладке:

В браузере должны быть включены cookie.

Конструктор работает не на всех мобильных устройствах, то есть «умный дом» будет работать на чём угодно, а вот конструировать надо на обычном компе.

Первая страница:

Онлайн-конструктор «умного дома»

Здесь выбирается количество помещений (максимум 10). Предположим, что будет два помещения (прихожая и кухня), тогда выберите 2 и нажмите «Далее».

На следующей странице нужно придумать название «умного дома» (будет написано на вкладке браузера) и вписать его в поле Название страницы.

Онлайн-конструктор «умного дома»

В поля Адрес сервера и Порт сервера ничего писать не нужно (сделано на будущее).

Названия помещений у нас уже придуманы (прихожая и кухня), вписываем их и нажимаем кнопку «Далее».

В названии вашего умного дома и названиях помещений, можно использовать буквы, цифры, пробел и нижнее_подчёркивание.

В дальнейшем Вы можете это исправить в файле index.html.

Здесь Вы увидите главный экран своего будущего интерфейса:

Онлайн-конструктор «умного дома»

Нажмите «Прихожая»…

Онлайн-конструктор «умного дома»

Выберите, сколько вы хотите кнопок для включения чего-либо с возвратом статуса (Количество кнопок вкл/откл), кнопок для отправки сигнала не требующего подтверждения (Количество кнопок отправки сигнала) и полей для приёма каких-либо данных (Количество полей для приёма информации).

Для примера выбрано по одной кнопке (максимум по пять).

Теперь закройте панель кнопкой Х, проделайте то же самое с «Кухней» и нажмите кнопку «Далее»…

Онлайн-конструктор «умного дома»

Появится главный экран с кнопкой «Скачать архив».

Можно открыть «Прихожую» или «Кухню» и посмотреть, что получилось…

Онлайн-конструктор «умного дома»

Онлайн-конструктор «умного дома»

Поля для приёма данных заполняются при появлении сигнала.

На этом работа с конструктором закончена, нажмите Онлайн-конструктор «умного дома» и переходите к следующей части.

HomestD

Распаковав архив, у Вас появится папка — mydomXXXXXXXXXX, переименуйте её так, чтоб получилось mydom, и перейдите в неё.

Переименуйте файл indexXXXXXXXXX.html в index.html, а файл domXXXXXXXXX.ino переместите в папку со скетчами.

В папке mydom останутся файлы index.html, jquery.js и style.css.

Онлайн-конструктор «умного дома» Откройте файл index.html и в двенадцатой строчке — var flagobnov = 0, переправьте ноль на единицу — var flagobnov = 1.

Дополнительные пояснения в конце.

Скачайте и установите библиотеку CyberLib, а затем загрузите скетч (domXXXXXXXXX.ino) в ардуину.

И наконец остаётся последний шаг — скачать программу homestd для вашего устройства, переименовать (для удобства) homestdXXX в homestd и скопировать в папку mydom.

В итоге содержимое папки mydom будет выглядеть так: homestd, index.html, jquery.js и style.css.

HomestD — это web-сервер и сервер для ардуины. Назначение — это обмен данными между web-клиентом (браузер) и ардуиной. То есть homestd принимает запросы от клиента по протоколу TCP (протокол UDP будет добавлен в следующей версии) и передаёт их ардуине, и одновременно принимает данные от ардуины, которые забирает web-клиент.

Исходник#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <termios.h>
#include <time.h>
#include <pthread.h>

char response[] = "HTTP/1.1 200 OKrn"
"Content-Type: text/html; charset=UTF-8rnrn";

char response_css[] = "HTTP/1.1 200 OKrn"
"Content-Type: text/css; charset=UTF-8rnrn";

char response_js[] = "HTTP/1.1 200 OKrn"
"Content-Type: text/js; charset=UTF-8rnrn";

char response_text[] = "HTTP/1.1 200 OKrn"
"Content-Type: text/text; charset=UTF-8rnrn";

char response_403[] = "HTTP/1.1 200 OKrn"
"Content-Type: text/html; charset=UTF-8rnrn"
"<!DOCTYPE html><html><head><title>403</title>"
"<style>body { background-color: #312f2f }"
"h1 { font-size:4cm; text-align: center; color: #666;}</style></head>"
"<body><h1>403</h1></body></html>rn";

#define BUFSIZE 1024
#define ARRAY_SIZE 90000
#define BREADSIZE 512

char send1_array[ARRAY_SIZE] = {0,};
char send2_array[ARRAY_SIZE] = {0,};
char patch_to_dir[64] = {0,};
char fpfile[64] = {0,};
char buffer[BUFSIZE] = {0,};
int count_simvol = 0;
char device[32]={0,};
unsigned long int speedport = 0;
unsigned int PORTW = 0;
char bRead[BREADSIZE] = {0,};
int wr_fdb = 0;
char str_iz_file[BREADSIZE] = {0,};
int counterr = 0;
int count_reciv = 0;
int fd;

void error_log(char *my_error)
{
memset(fpfile, 0, 64 * sizeof(char));
snprintf(fpfile, (int)strlen(patch_to_dir) + (int)strlen("Error.log "), "%s%s", patch_to_dir, "Error.log");
time_t t;
time(&t);
FILE *f;
f = fopen(fpfile, "a");

if(f == NULL)
{
printf("Error open Error.logn");
exit(0);
}

fprintf(f, "%s", ctime( &t));
fprintf(f, "%snn", my_error);
printf("%snError write to %sError.log.n", my_error, patch_to_dir);
fclose(f);
exit(0);
}

void warning_access_log(char *war_ac)
{
memset(fpfile, 0, 64 * sizeof(char));
snprintf(fpfile, (int)strlen(patch_to_dir) + (int)strlen("Warning_Access.log "), "%s%s", patch_to_dir, "Warning_Access.log");
time_t t;
time(&t);
FILE *f;
f = fopen(fpfile, "a");
fprintf(f, "%s", ctime( &t));
fprintf(f, "%snn", war_ac);
printf("%snWrite to %sAccess_Warning.log.nnn", war_ac, patch_to_dir);
fclose(f);
}

void read_in_file(char *name_file)
{
count_simvol = 0;
memset(send1_array, 0, ARRAY_SIZE * sizeof(char));
memset(fpfile, 0, 64 * sizeof(char));
snprintf(fpfile, (int)strlen(patch_to_dir) + (int)strlen(name_file) + 1, "%s%s", patch_to_dir, name_file);
FILE *file;
file = fopen(fpfile,"r");
if(file == NULL) error_log("Error open file");

int ch;
while(ch = getc(file), ch != EOF)
{
send1_array[count_simvol] = (char) ch;
count_simvol++;
if(count_simvol == ARRAY_SIZE — 2) break;
}

fclose(file);
}

void error_to_filebd(char *db_error)
{
if(wr_fdb == 1)
{
memset(fpfile, 0, 64 * sizeof(char));
snprintf(fpfile, (int)strlen(patch_to_dir) + (int)strlen("file.db "), "%s%s", patch_to_dir, "file.db");
FILE *f;
f = fopen(fpfile, "w");
fprintf(f, "%s", db_error);
fclose(f);
printf("Write to file.db — %sn", db_error);
}

memset(str_iz_file, 0, BREADSIZE);
strncpy(str_iz_file, db_error, 13);
}

void * thread_func()
{
int i = 0;
int err_count1 = 0;

for(;;)
{
int bytes = 0;
memset(bRead, 0, BREADSIZE * sizeof(char));
counterr = 0;

if((bytes = read(fd, bRead, BREADSIZE — 1)) == -1)
{
warning_access_log("Error_Read_from_Arduino.");
}

for(i = 0; i <= bytes; i++)
{
if(bRead[i] == ‘n’) break;
}

if(bRead[0] == ‘A’ && bRead[strlen(bRead)-2] == ‘Z’)
{
err_count1 = 0;
}

else
{
tcflush(fd, TCIFLUSH);
err_count1++;
if(err_count1 > 5)
{
err_count1 = 0;
error_to_filebd("NOT A_Z_SIM n");
}

printf("Not_A-Z_bRead: %snn", bRead);
continue;
}

if(strcmp(bRead, str_iz_file)==0)
{
printf("StrOK:%snn", bRead);
continue;
}

else
{
if(wr_fdb == 1)
{
char fpfile_2[64] = {0,};
snprintf(fpfile_2, (int)strlen("file.db ") + (int)strlen(patch_to_dir), "%s%s", patch_to_dir, "file.db");
FILE *f;
f = fopen(fpfile_2, "w");
if(f == 0) warning_access_log("NOT open file.db Arduina.");
fprintf(f, "%s", bRead);
fclose(f);
}

memcpy(str_iz_file, bRead, BREADSIZE);
printf("NotStr:%snn", bRead);
}

} // END (while) ardu

return 0;

} // END thread_func

void * thread2_func()
{
for(;;)
{
sleep(1);
counterr++;
if(counterr > 2) error_to_filebd("NOT CONNECT n");
}

return 0;

}

void open_port()
{
fd = open(device, O_RDWR | O_NOCTTY);
if(fd == -1) error_log("Error — NOT open /dev/ttyX");
else
{
struct termios options;
tcgetattr(fd, &options);

switch(speedport)
{
case 4800:
cfsetispeed(&options, B4800);
cfsetospeed(&options, B4800);
break;

case 9600:
cfsetispeed(&options, B9600);
cfsetospeed(&options, B9600);
break;

case 19200:
cfsetispeed(&options, B19200);
cfsetospeed(&options, B19200);
break;

case 38400:
cfsetispeed(&options, B38400);
cfsetospeed(&options, B38400);
break;

case 57600:
cfsetispeed(&options, B57600);
cfsetospeed(&options, B57600);
break;

case 115200:
cfsetispeed(&options, B115200);
cfsetospeed(&options, B115200);
break;

default:
error_log("Error — Speed_port");
break;
}

options.c_cflag |= (CLOCAL | CREAD);
options.c_iflag = IGNCR;
options.c_cflag &= ~PARENB;
options.c_cflag &= ~CSTOPB;
options.c_cflag &= ~CSIZE;
options.c_cflag |= CS8;
options.c_cc[VMIN] = 1;
options.c_cc[VTIME] = 1;
options.c_lflag = ICANON;
options.c_oflag = 0;
options.c_oflag &= ~OPOST;
tcflush(fd, TCIFLUSH);
tcsetattr(fd, TCSANOW, &options);
}
}

int main(int argc, char *argv[])
{
if(argc != 6) error_log("Not argumets.");

strncpy(device, argv[1], 31);
speedport = strtoul(argv[2], NULL, 0);
PORTW = strtoul(argv[3], NULL, 0);
strncpy(patch_to_dir, argv[4], 63);
wr_fdb = atoi(argv[5]);

open_port();
sleep(2);
tcflush(fd, TCIFLUSH);
warning_access_log("START");

int pt1 = 1;
pthread_t ardu_thread;
int result = pthread_create(&ardu_thread, NULL, &thread_func, &pt1);
if(result != 0) error_log("Error — creating thread.");

int pt2 = 1;
pthread_t counterr_thread;
int result2 = pthread_create(&counterr_thread, NULL, &thread2_func, &pt2);
if(result2 != 0) error_log("Error — creating thread2.");

int one = 1, client_fd;
struct sockaddr_in svr_addr, cli_addr;
socklen_t sin_len = sizeof(cli_addr);

int sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (sock < 0) error_log("Not socket.");

setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(int));

svr_addr.sin_family = AF_INET;
svr_addr.sin_addr.s_addr = INADDR_ANY;
svr_addr.sin_port = htons(PORTW);

if(bind(sock, (struct sockaddr *) &svr_addr, sizeof(svr_addr)) == -1)
{
close(sock);
error_log("Error bind.");
}

if(listen(sock, 10) == -1)
{
close(sock);
error_log("Error listen.");
}

int dev_echo = strlen(device) + 18;
char otvet[BREADSIZE] = {0,};
char to_Ardu[64] = {0,};

for(;;)
{
client_fd = accept(sock, (struct sockaddr *) &cli_addr, &sin_len);

if(client_fd == -1) continue;

memset(buffer, 0, BUFSIZE);
read(client_fd, buffer, BUFSIZE — 1);

if((strstr(buffer, "file.db")) != NULL)
{
memset(otvet, 0, BREADSIZE);

int c_sim = 0;
for(c_sim = 0; c_sim <= BREADSIZE — 1; c_sim++)
{
if(str_iz_file[c_sim] == ‘n’) break;
}

snprintf(otvet, 59 + c_sim, "%s%s", response_text, str_iz_file);
write(client_fd, otvet, c_sim + 58);
close(client_fd);
printf("Trans otvet.n");
}

else if((strstr(buffer, "comanda")) != NULL)
{
memset(to_Ardu, 0, 64);
snprintf(to_Ardu, dev_echo, "echo ‘Y+=Z%c%c%c’ > %s", buffer[13], buffer[14], buffer[15], device);
system(to_Ardu);
close(client_fd);
warning_access_log(buffer);
printf("To Ardu: %sn", to_Ardu);
}

else if((strstr(buffer, "GET / ")) != NULL)
{
memset(send2_array, 0, ARRAY_SIZE * sizeof(char));
read_in_file("index.html");
int len_ara = count_simvol + (int)strlen(response) + 1;

snprintf(send2_array, len_ara, "%s%s", response, send1_array);
write(client_fd, send2_array, count_simvol + 59);
close(client_fd);
warning_access_log(buffer);
printf("Trans index.html.nn");
}

else if((strstr(buffer, "style.css")) != NULL)
{
memset(send2_array, 0, ARRAY_SIZE * sizeof(char));
read_in_file("style.css");
int len_ara = count_simvol + (int)strlen(response_css) + 1;

snprintf(send2_array, len_ara, "%s%s", response_css, send1_array);
write(client_fd, send2_array, count_simvol + 58);
close(client_fd);
warning_access_log(buffer);
printf("Trans style.css.nn");
}

else if((strstr(buffer, "jquery.js")) != NULL)
{
memset(send2_array, 0, ARRAY_SIZE * sizeof(char));
read_in_file("jquery.js");
int len_ara = count_simvol + (int)strlen(response_js) + 1;

snprintf(send2_array, len_ara, "%s%s", response_js, send1_array);
write(client_fd, send2_array, count_simvol + 57);
close(client_fd);
warning_access_log(buffer);
printf("Trans jquery.js.nn");
}

else
{
write(client_fd, response_403, sizeof(response_403) — 1);
close(client_fd);
warning_access_log(buffer);
}
}

} //END main

// gcc -Wall -Wextra -Werror homestd.c -o homestd -lpthread
// ./homestd /dev/ttyUSB0 57600 80 /var/www/vse/tpl/mydom2/ 0
// make package/homestd/compile V=s

Подключаем ардуину, копируем папку mydom в любое удобное место на целевом устройстве, например в корень (путь будет выглядеть так — /mydom) и запускаем командой:
sudo /mydom/homestd /dev/ttyUSB0 57600 80 /mydom/ 0

На роутере без sudo.

Первый параметр — /dev/ttyUSB0, путь к ардуине. Узнать можно так:
ls /dev/tty*

Онлайн-конструктор «умного дома»

Второй параметр — 57600, скорость «сом»-порта.

Третий параметр — TCP порт. Порт можно указать любой, однако если у Вас больше нет никаких серверов занимающих стандартный (80) порт, то укажите его. Если система ставится на роутер, то скорее всего там есть «web-морда» и 80-ый порт будет занят. Тогда укажите что-нибудь другое, например 82 (заходить в «умный дом» так — адрес:82).

Четвёртый параметр — путь к папке mydom (слеш / в конце обязателен).

Пятый параметр — может быть 0 или 1. Если указать 1, тогда в папке mydom будет создаваться текстовый файл file.db, в который будут записываться данные полученные от ардуины. Это сделано для того, чтоб можно было забирать эти данные и заносить куда-либо.

Все действия homestd, сопровождаются записью в файл Access_Warning.log

Онлайн-конструктор «умного дома»

Ошибки записываются в файл Error.log

Онлайн-конструктор «умного дома»

Если всё заработало, то переходите в браузер и начинайте пользоваться. Если что-то не так, то приступайте к поиску ошибок и пишите в комментах…

Пояснения

К скетчу…

Задача ардуины — принимать команды от сервера, выполнять действие и через каждые 440мс отправлять статус/информацию обратно.

Для кнопок для включения чего-либо формируются флаги (d2, d3…) принимающие значения 1 или 0, эти значения присваиваются им в функции «switch(cod_comand)», во время включения/отключения чего-либо.


case 100:
D2_High;
d2 = 1;
clear_port();
break;

case 101:
D2_Low;
d2 = 0;
clear_port();
break;

Функция «void trans()» отправляет эти значения (вместе с другими данными) серверу.

void trans()
{
Serial.print(‘A’);
Serial.print(‘ ‘);
Serial.print(0);
Serial.print(‘ ‘);
Serial.print(d2);
Serial.print(‘ ‘);
Serial.print(d3);

Команды от кнопок для отправки сигнала не требующего подтверждения просто обрабатываются в функции «switch(cod_comand)».


case 106:
// какая-то реакция на кнопку SENTSIG1
clear_port();
break;

case 107:
// какая-то реакция на кнопку SENTSIG2
clear_port();
break;

Данные, которые будут выводиться в полях для приёма каких-либо данных, нужно поместить в функцию «void trans()». Например, нужно отправить показания температуры, тогда пишем:


Serial.print(temp); // INDATA3


temp — это какая-то переменная, в которую вы записываете показания датчика.

В интерфейсе, в поле «INDATA3» будет Ваша температура. Также можно посылать какую-то строку, не разделённую пробелами, например, так:


Serial.print("okey"); // INDATA3

К файлу index.html…

Браузер с интервалом 680мс запрашивает данные у ардуины…

setInterval(show,680);


… получает ответ в текстовом виде (данные разделены пробелами) и раскладывает их по переменным.

/* приём */
if(vars[2] == 1) { $(‘.d2otkl’).show(); $(‘.d2vkl’).hide(); }
else if(vars[2] == 0) { $(‘.d2otkl’).hide(); $(‘.d2vkl’).show(); }

$(‘#indata3’).html(‘INDATA3’ + ‘ ‘ + vars[3]);

if(vars[4] == 1) { $(‘.d3otkl’).show(); $(‘.d3vkl’).hide(); }
else if(vars[4] == 0) { $(‘.d3otkl’).hide(); $(‘.d3vkl’).show(); }

Если устанавливаете систему там, где качество связи плохое (например на даче), то есть пинги туда очень длинные, то появятся ошибки «timeout». Чтобы это исправить, нужно увеличить таймаут запроса:


show();
setInterval(show,680);
function show(){
if(flagobnov == 1) {
$.ajax({
type: "POST",
url: "file.db",
timeout:560, /* эта цифра (в миллисекундах)*/
cache: false,


По умолчанию стоит 560мс, увеличивайте её с шагом в 100 мс и пробуйте. Соответственно нужно увеличивать и setInterval(show,680), так же на 100 мс.

Изменять названия кнопок (D2, D3, SENTSIG1 и т.д.) можно здесь:

<div class=’knop kon d2vkl’>D2</div>
<div class=’knop koff d2otkl’>D2</div>

<div class=’knop kon sent1′>SENTSIG1</div>

Изменять названия полей для приёма данных (INDATA3, INDATA5 и т.д.) можно здесь:

$(‘#indata3’).html(‘INDATA3’ + ‘ ‘ + vars[3]);

Браузер постоянно запрашивает данные и тем самым создаёт трафик. Чтобы этого избежать, можно либо закрывать страницу, либо раскомментировать этот блок:
/*slmode++;
if(slmode > 70)
{
$(".pansl").show(300);
flagobnov = 0;
slmode = 0;
}*/

Тогда через ~минуту страница будет закрываться полупрозрачной панелью и обновления остановятся. Клик на панель уберёт её и обновления возобновляться.

На этом пока всё, в следующей части будет добавлен UDP клиент/сервер и работа с GPIO на RPi.

П.С. Вначале с конструктором происходили непонятные вещи, но теперь всё должно работать исправно.