[Из песочницы] Онлайн-конструктор «умного дома»
Создаёт 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-интерфейса достаточно проста и минималистична.
Главный экран интерфейса.
На главном экране расположены кнопки с названиями помещений, нажатие на которые открывает панель с кнопками управления этим помещением.
Здесь могут располагаться несколько кнопок (D2, D3 и т.д.) для включения чего-либо с возвратом состояния.
Несколько кнопок для отправки сигнала (SENTSIG1 и т.д), не требующего подтверждения.
И несколько полей (INDATA1 и т.д) для приёма каких-либо данных/сигналов.
Крест закрывает панель.
Названия кнопок можно изменять по своему усмотрению и менять местами.
Пример:
Кнопка Info скрывает панель с информацией о работоспособности системы.
Надпись Connect! говорит о том, что всё хорошо, а Count update: — счётчик запросов (браузер с определённым интервалом запрашивает у ардуины данные). Интервал можно менять.
Если что-то произойдёт, то на экране появится сообщение ERROR, а в Info будет описана ошибка.
Алгоритм работы описан в конце.
Конструктор
Конструктор прост и понятен.
Первая страница:
Здесь выбирается количество помещений (максимум 10). Предположим, что будет два помещения (прихожая и кухня), тогда выберите 2 и нажмите «Далее».
На следующей странице нужно придумать название «умного дома» (будет написано на вкладке браузера) и вписать его в поле Название страницы.
В поля Адрес сервера и Порт сервера ничего писать не нужно (сделано на будущее).
Названия помещений у нас уже придуманы, вписываем их и нажимаем кнопку «Далее».
Здесь Вы увидите главный экран своего будущего интерфейса:
Нажмите «Прихожая»…
Выберите, сколько вы хотите кнопок для включения чего-либо с возвратом статуса (Количество кнопок вкл/откл), кнопок для отправки сигнала не требующего подтверждения (Количество кнопок отправки сигнала) и полей для приёма каких-либо данных (Количество полей для приёма информации).
Для примера выбрано по одной кнопке (максимум по пять).
Теперь закройте панель кнопкой Х, проделайте то же самое с «Кухней» и нажмите кнопку «Далее»…
Появится главный экран с кнопкой «Скачать архив».
Можно открыть «Прихожую» или потом «Кухню» и посмотреть, что получилось…
Поля для приёма данных заполняются при появлении сигнала.
На этом работа с конструктором закончена, нажмите и переходите к следующей части.
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
#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*
Второй параметр — 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)», во время включения/отключения чего-либо.
Функция «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.