[Из песочницы] Онлайн-конструктор «умного дома»

0b4357edf47f498d88b4eb0e464fbfc8.png

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

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

Пользователю останется только скачать архив с файлами, распаковать его на целевом устройстве и загрузить в ардуину готовый скетч. Установка каких-либо дополнительных пакетов и серверов не требуется.
HomestD можно использовать на любом компьютере работающем под управлением 14b140fc60e240c180f7bb9b65c55def.pngc22086f96fcd4d2eb2e7485d5b7d5a59.pngdf76982d8aa64811bd3c1ca3ac2cae5f.png или на роутере с прошивкой 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-интерфейса достаточно проста и минималистична.

6fd306f1c454490b9587fba9e39dae89.png
Главный экран интерфейса.

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

dee53ee1526f48c38dff473533ef3165.png

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

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

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

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

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

Пример:

9cb2a0cbdba2455cb1c95a24911cdf9e.png

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

bf03e05e5d464af98a5715aab0cee2b0.png

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

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

f1a326cf9ca749ba9a4961f60d793f65.png

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

Конструктор


Конструктор прост и понятен.

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

3838fb99dd82423ba0ad34f488e3ca06.png

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

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

cdc1654835fe403483b812c97ca376ad.png

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

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

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

31494ce9d10d4523bbe7ffb4a16a0139.png

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

64c85fc7f26f4f1d9b009180a5d67c4b.png

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

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

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

c5a1956ef35f4a5eadfd8d8466f2bc6b.png

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

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

af2aedc781564b268ea593d447909c28.png

2582e92fe1144b24b79ae75082ea47da.png

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

На этом работа с конструктором закончена, нажмите 38ea3a2ad40c4c019b1dc08c5eb1a3c0.png и переходите к следующей части.

HomestD


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

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

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

4462256163f0466682c5e7267f4f884e.png Откройте файл 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 
#include 
#include 
#include  
#include 
#include 
#include 
#include 
#include 
#include  
#include   
#include   
#include 
#include 

char response[] = "HTTP/1.1 200 OK\r\n"
"Content-Type: text/html; charset=UTF-8\r\n\r\n";

char response_css[] = "HTTP/1.1 200 OK\r\n"
"Content-Type: text/css; charset=UTF-8\r\n\r\n";

char response_js[] = "HTTP/1.1 200 OK\r\n"
"Content-Type: text/js; charset=UTF-8\r\n\r\n";

char response_text[] = "HTTP/1.1 200 OK\r\n"
"Content-Type: text/text; charset=UTF-8\r\n\r\n";

char response_403[] = "HTTP/1.1 200 OK\r\n"
"Content-Type: text/html; charset=UTF-8\r\n\r\n"
"403"
""
"

403

\r\n"; #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.log\n"); exit(0); } fprintf(f, "%s", ctime( &t)); fprintf(f, "%s\n\n", my_error); printf("%s\nError 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, "%s\n\n", war_ac); printf("%s\nWrite to %sAccess_Warning.log.\n\n\n", 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 - %s\n", 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: %s\n\n", bRead); continue; } if(strcmp(bRead, str_iz_file)==0) { printf("StrOK:%s\n\n", 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:%s\n\n", 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: %s\n", 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.\n\n"); } 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.\n\n"); } 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.\n\n"); } 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*


eac9e2b3d1f44f15b9a75f9e5dea835f.png

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

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

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

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

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

1464aa1dcf194807be7423c490236ed9.png

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

76bca0baf0de40f7a441f56545d44c21.png

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

Пояснения


К скетчу…

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

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

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

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

Данные, которые будут выводиться в полях для приёма каких-либо данных, нужно поместить в функцию «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(); }
...

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

...
        
D2
D2
SENTSIG1
...

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

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


Браузер постоянно запрашивает данные и тем самым создаёт трафик. Чтобы этого избежать, можно либо закрыть страницу, либо раскомментировать этот блок:

/*slmode++;
   if(slmode > 70) 
    { 
      $(".pansl").show(300);
      flagobnov = 0;
      slmode = 0;
    }*/


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

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

© Geektimes