Онлайн конструктор веб-интерфейса для управления Raspberry Pi

e90295f65ba34783bf9f82964b011eba.png

В предыдущей части (рекомендую ознакомиться), был описан онлайн конструктор создающий web-интерфейс для управления «умным домом» и скетч для ардуино, для работы на разных компьютерах и роутерах с прошивкой OpenWrt.

Это онлайн конструктор web-интерфейса для управления GPIO на Raspberry Pi.
Система очень простая, она позволяет управлять по сети (внешней или локальной) пинами GPIO настроенными на выход и получать их статус (то есть видеть, что включено-выключено). Отправлять какие-либо команды, которые можно обрабатывать внутри Raspberry. Получать состояние пинов GPIO (в виде 0 или 1) настроенных на вход. Конструктор сам настроет нужные вам пины GPIO на вход или на выход.

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

Пользователю нужно только лишь собрать интерфейс в конструкторе, скачать архив с файлами и рапаковать его на своём RaspberryPi 2.
В архиве будут лежать готовые файлы относящихся к веб-части (html, css, js), sh-скрипт для инициализации пинов и сервер для обмена данными HomestDRp. Установка каких-либо дополнительных программ не требуется.

Несмотря на, может быть, покажущуюся сложность, все очень просто и займёт минут 10–15.

Внешний вид


Как уже говорилось в предыдущей части, идея web-интерфейса достаточно проста и в чём-то даже аскетична. Связано это с тем, что мне разонравились нарисованные комнаты с лампочками и прочие полумеры, поэтому выбран стиль «пульта от телевизора».

Работает на любом устройстве — компьютере, ноутбуке, планшете, мобильнике.

image

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

Откроем прихожую:

image

Здесь могут располагаться: несколько кнопок настроенных на выход (BCM0, BCM1 и т.д.) для включения чего-либо с возвратом статуса. То есть, если это что-то (например верхний свет) включено, то надпись подсветится.

Несколько кнопок (SENTSIG1 и т.д) для отправки любой команды в RaspberryPi.

И несколько полей (IN_BCM2 и т.д) для получения состояние пинов GPIO (в виде 0 или 1) настроенных на вход. 0 — пин подтянут к «земле», 1 — на пине есть напряжение.

В дальнейшем можно изменять названия кнопок и менять их местами.

Крестик справа-сверху — закрытие панели.

Под кнопкой Info скрывается панель с информацией о статусе системы.

image

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

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

image

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

Конец ознакомительной части.

Конструктор


Коротенькое видео по работе с конструктором, можно посмотреть вот по этой ссылке.

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

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

Конструктор предельно прост и интуитивно понятен. Открыв в соседней вкладке вот эту ссылку, вы окажитесь на первой странице (всего их четыре):

Чтоб понять и потренироваться, проделайте всё как написано ниже.

image

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

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

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

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

image

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

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

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

image

Нажмите на кнопку «Прихожая»…

image

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

Максимум можно выбрать по пять кнопок.

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

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

image

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

Внимание! У Raspbery Pi 2 всего 28 GPIO (BCM0 — BCM27). Если Вы в конструкторе сделаете больше 27 кнопок/полей приёма, то всем лишним BCM будет присваиваться номер 28, а у лишних полей приёма IN_BCM значение будет просто увеличиваться. Функционировать они не будут.

Подключение


Простенькая схема для испытания системы:

image

Подключите светодиод к пину BCM 1, через резистор 500–1000 Ом. При нажатии в интерфейсе кнопки BCM1, светик будет загораться/гаснуть, а надпись менять цвет.

Так же подключите проводок через резистор 500–1000 Ом к BCM 3, этим проводочком можно будет тыкать на +3.3 или Ground, и в поле IN_BCM3 нолик на единичку. Таким образом можно отслеживать какие-то события.

BCM 2 используйте только на выход. Если сделали как вход (IN_BCM2), то не пользуйтесь им или переделайте как выход (BCM2).

image

Применение кнопок SENTSIG описано ниже.

HomestDRp


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

Переименуйте файлы indexXXXXXXXXX.html в index.html и initXXXXXXXXX.sh в init.sh.

В папке mydomrp получатся файлы index.html, init.sh, jquery.js и style.css и программа homestdrp.

Обязательно! Откройте файл index.html и в двенадцатой строчке — var flagobnov = 0, переправьте нолик на единичку — var flagobnov = 1 (отключено в конструкторе). Сохраните файл.

Дополнительные пояснения к файлам, даны в конце статьи.

Теперь к программе homestdrp…

homestdrp — это web-сервер, который принимает запросы от клиента, передаёт команды RaspberryPi, считывает состояние пинов GPIO и отправляет обратно информацию/статус web-клиенту. Иными словами, его назначение — это обмен данными между web-клиентом (браузер) и RaspberryPi. Работает по протоколу ТСР, а в перспективе и по UDP.

Исходник
#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 BSIZ 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; unsigned int PORTW = 0; int len_dat = 0; int count_warning_log =0; void error_log(char *my_error) { char file_error_log[32] = {0,}; snprintf(file_error_log, 26, "%s", "/var/log/ErrorhomeRp.log"); time_t t; time(&t); FILE *f; f = fopen(file_error_log, "a"); if(f == NULL) { printf("Error open /var/log/ErrorhomeRp.log.\n"); exit(0); } fprintf(f, "%s", ctime( &t)); fprintf(f, "Error %s\n\n", my_error); printf("Error %s Write to /var/log/ErrorhomestdRp.log.\n", my_error); fclose(f); exit(0); } void warning_access_log(char *war_ac) { count_warning_log++; char file_Warning_Access_log[32] = {0,}; snprintf(file_Warning_Access_log, 31, "%s", "/var/log/Warning_AccessRp.log"); if(count_warning_log > 100) { system("gzip -f /var/log/Warning_AccessRp.log"); count_warning_log =0; time_t t; time(&t); FILE *f; f = fopen(file_Warning_Access_log, "w"); fprintf(f, "%s", ctime( &t)); fprintf(f, "%s\n\n", war_ac); printf("Write to /var/log/Warning_AccessRp.log:\n%s\n", war_ac); fclose(f); } else { time_t t; time(&t); FILE *f; f = fopen(file_Warning_Access_log, "a"); fprintf(f, "%s", ctime( &t)); fprintf(f, "%s\n\n", war_ac); printf("Write to /var/log/Warning_AccessRp.log:\n%s\n", war_ac); 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("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); } int main(int argc, char *argv[]) { if(argc != 4) error_log("not argumets."); PORTW = strtoul(argv[1], NULL, 0); strncpy(patch_to_dir, argv[2], 63); len_dat = atoi(argv[3]); warning_access_log("START"); 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("bind."); } if(listen(sock, 10) == -1) { close(sock); error_log("listen."); } char flin_fldb[64] = {0,}; snprintf(flin_fldb, (int)strlen(patch_to_dir) + (int)strlen("init.sh 1 "), "%s%s", patch_to_dir, "init.sh 1"); system(flin_fldb); char bufRec[BSIZ] = {0,}; char To_GPIO[64] = {0,}; FILE *mf; char in_data[256] = {0,}; char gp_file[32] = {0,}; printf("Receive data from RPi\n\n"); for(;;) { client_fd = accept(sock, (struct sockaddr *) &cli_addr, &sin_len); if(client_fd == -1) continue; memset(buffer, 0, BUFSIZE * sizeof(char)); read(client_fd, buffer, BUFSIZE - 1); if((strstr(buffer, "file.db")) != NULL) { memset(in_data, 0, 256 * sizeof(char)); memset(bufRec, 0, BSIZ * sizeof(char)); int i = 0; int ip = 0; for(i = 0; i < len_dat; i++) { memset(gp_file, 0, 32 * sizeof(char)); snprintf(gp_file, 29, "%s%d%s", "/sys/class/gpio/gpio", i, "/value"); if(ip > 54) { in_data[ip] = '0'; ip++; in_data[ip] = ' '; ip++; } else { mf = fopen (gp_file, "r"); if(mf == NULL) error_log("gpio, fail last argument."); in_data[ip] = getc(mf); ip++; in_data[ip] = ' '; ip++; fclose (mf); } } printf("Data:%s\n", in_data); int len_ara = (int)strlen(in_data) + 59; snprintf(bufRec, len_ara, "%s%s", response_text, in_data); write(client_fd, bufRec, len_ara - 1); close(client_fd); } else if((strstr(buffer, "comanda")) != NULL) { memset(To_GPIO, 0, 64); snprintf(To_GPIO, (int)strlen(patch_to_dir) + (int)strlen("init.sh 0 ") + 4, "%sinit.sh 0 %c%c%c", patch_to_dir, buffer[13], buffer[14], buffer[15]); system(To_GPIO); close(client_fd); warning_access_log(buffer); printf("To Gpio:%s\n", To_GPIO); } else if((strstr(buffer, "GET / ")) != NULL) { read_in_file("index.html"); int len_ara = count_simvol + (int)strlen(response) + 1; memset(send2_array, 0, len_ara * sizeof(char)); 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) { read_in_file("style.css"); int len_ara = count_simvol + (int)strlen(response_css) + 1; memset(send2_array, 0, len_ara * sizeof(char)); 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) { read_in_file("jquery.js"); int len_ara = count_simvol + (int)strlen(response_js) + 1; memset(send2_array, 0, len_ara * sizeof(char)); 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 homestdrp.c -o homestdrp

Теперь копируем папку mydomrp в любое удобное место на RaspberryPi, например в корень, — /mydomrp, делаем файлы homestdrp и init.sh исполняемым…

sudo chmod +x /mydomrp/homestdrp
sudo chmod +x /mydomrp/init.sh

И заспускаем программу с тремя параметрами:

sudo /mydomrp/homestdrp 80  /mydomrp/ 6

Позже, для автоматизации запуска, добавите эту команду в файл rc.local

nano /etc/rc.local

Вписать надо до строчки exit 0, вот так:

...

(/mydomrp/homestdrp 80  /mydomrp/ 6)&

exit 0

О параметрах:

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

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

Четвёртый параметр — количество данных посылаемых клиену (вдаваться в подробности этого пункта не стоит, по крайней мере сейчас), подсмотреть эту цифру нужно в файле index.html в строке if (vars.length == 6). В коде она находится вот здесь:

...
show();
setInterval(show,680); 
function show(){  
    if(flagobnov == 1) { 
            $.ajax({ 
                type: "POST",
                url: "file.db", 
                timeout:560,           
                cache: false,       
                success: function(data){   
                           var vars = data.split(" "); 

                           if(vars.length == 6) ЭТА ЦИфРА
                             {        
                               count_obnov++; 
                           ...


После успешного старта, homestdrp первым делом запускает скрипт init.sh (Start init.sh), который вначале удаляет все ссылки на все GPIO, а потом создаёт нужные (Export selected pin) и задаёт им режимы (in/out) (Initialization selected pin).

image

На ошибки — sh: echo: I/O error не обращайте внимания, так и должно быть. Ведь скрипт очищает GPIO которых и так нет.

Теперь открыв страничку в браузере вы увидите различные сообщения и отправку считанных данных GPIO:

image

Все действия homestdrp, сопровождаются записью в файл /var/log/Warning_AccessRp.log, и туда же пишутся предупреждения.

В случае критической ошибки (например не дописать аргумент), она будет записана в файл /var/log/ErrorhomestdRp.log и программа остановится.

image

Теперь, если остановить homestdrp (Ctrl + c), то интерфейс сообщит об ошибке:

image

Пояснения


Как пользоваться кнопкой SENTSIGx?

Открыв файл init.sh (из папки /mydomrp), среди прочего, вы найдёте вот такие строки:

...
        104 )
        # reaction to the button SENTSIG1
        ;;

        109 )
        # reaction to the button SENTSIG2
        ;;
...

Вот сюда то (вместо # reaction to the button SENTSIG1) и нужно вписывать свои команды.
То есть, если вместо »# reaction to the button SENTSIG1» написать reboot (без #), сохранить файл и нажать в интерфейсе кнопку SENTSIG1, то малинка перезагрузится, или если вместо »# reaction to the button SENTSIG2» написать apt-get update && apt-get upgrade, то обновится.

В общем можно делать всё, что душе угодно.

Пояснения к файлу index.html

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

...
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 и т.д.) можно здесь:

...
        
D2
D2
SENTSIG1
...

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

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

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

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

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

После внесения изменений в index.html обязательно обновите страничку в браузере.

На этом пока всё, спасибо.

© Geektimes