Разрабатываем web-site для микроконтроллера

sbuuirno411hgzt3t8pisu5w9sg.png
С приходом в нашу жизнь различного рода умных розеток, лампочек и других подобных устройств, необходимость наличия веб-сайтов на микроконтроллерах стала неоспоримой. А благодаря проекту lwIP (и его младшему брату uIP) подобным функционалом никого не удивишь. Но поскольку lwIP направлен на минимизацию ресурсов, то с точки зрения дизайна, функционала, а также удобства использования и разработки, подобные сайты сильно отстают от тех, к которым мы привыкли. Даже для встроенных систем, сравнить, например, с сайтом для администрирования на самых дешевых роутерах. В данной статье мы попробуем разработать сайт на Линуксе для какого-нибудь умного устройства и запустить его на микроконтроллере.
Для запуска на микроконтроллере будем использовать Embox. В состав этой RTOS входит HTTP сервер с поддержкой CGI. В качестве HTTP сервера на Linux будем использовать встроенный в python HTTP сервер.
python3 -m http.server -d 

Статический сайт


Начнем с простого статического сайта, состоящего из одной или нескольких страниц.
Тут все просто, давайте создадим папку и в ней index.html. Этот файл будет скачиваться по умолчанию, если в браузере задан только адрес сайта.
$ ls website/
em_big.png  index.html

Сайт еще будет содержать логотип Embox, файл «em_big.png», который мы встроим в html.

Запустим http сервер

python3 -m http.server -d website/

Зайдем в браузере на localhost:8000
pijmqddrbpsblwumcpz6h3s-cb8.png

Теперь добавим наш статический сайт в файловую систему Embox. Это можно сделать скопировав нашу папку в папку rootfs/ темплейта (текущий темплейт в папке conf/rootfs). Или создать модуль указав в нем файлы для rootfs.

$ ls website/
em_big.png  index.html  Mybuild

Содержимое Mybuild.

package embox.demo

module website {
    @InitFS
    source "index.html",
        "em_big.png",
}

Для простоты мы положим наш сайт прямо в корневую папку (аннотация @InitFs без параметров).

Нам также нужно включить наш сайт в конфигурационном файле mods.conf и туда же добавить сам httd сервер

    include embox.cmd.net.httpd    
    include embox.demo.website

Кроме того, давайте запустим сервер с нашим сайтом во время старта системы. Для этого добавим строчку в файл conf/system_start.inc

"service httpd /",

Естественно все эти манипуляции нужно делать с конфигом для платы. После этого собираем и запускаем. Заходим в браузере на адрес вашей платы. В моем случае это 192.168.2.128

И имеем такую же картинку как и для локального сайта
quz2z2lqabnikqyvxf1e2kjwvps.png

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

Какой-бы статический контент не положили в папку с сайтом, например, js или css файлы, мы можем его использовать без каких-либо дополнительных усилий.

Добавим в наш сайт app.js (сайт на angular) и в нем пару вкладок. Страницы для этих вкладок положим в папку partials, изображения в папку images/, а css файлы в css/.

$ ls website/
app.js  css  images  index.html  Mybuild  partials

Запустим наш сайт.
2eohk67sgl5ql3mm9szyxcpevl0.png

Согласитесь, сайт выглядит гораздо привычнее и приятнее. Причем все это сделано на стороне браузера. Как мы и сказали, весь контекст все еще статический. И мы его можем разрабатывать на хосте как обычный сайт.

Естественно, при этом можно использовать все средства разработки обычных веб-девелоперов. Так, открыв консоль в браузере, мы обнаружили сообщение об ошибке, о том что не хватает favicon.ico:
dwpmhj1ljc7mbmr5stoqwz4dkvu.png

Выяснили, что это иконка которая отображается во вкладке браузера. Можно конечно положить файл с этим именем, но порой не хочется тратить на это место. Напоминаю, что хотим запускаться в том числе и на микроконтроллерах, где памяти мало.

Поиск в интернете сразу выдал, что можно обойтись и без файла, нужно всего лишь добавить строку в head секцию html. Хотя ошибка не мешала, но сделать сайт чуть лучше всегда приятно. И главное, что мы убедились, что обычные средства разработчика вполне применимы при предлагаемом подходе.

Динамический контент


CGI


Перейдем к динамическому контенту. Common Gateway Interface (CGI) интерфейс взаимодействия web-сервера с утилитами командной строки, позволяющий создавать динамический контент. Иными словами, CGI позволяет использовать вывод утилит для генерации динамического контента.

Давайте взглянем на какой-нибудь CGI скрипт

#!/bin/bash

echo -ne "HTTP/1.1 200 OK\r\n"
echo -ne "Content-Type: application/json\r\n"
echo -ne "Connection: Connection: close\r\n"
echo -ne "\r\n"

tm=`LC_ALL=C date +%c`
echo -ne "\"$tm\"\n\n"

Вначале в стандартный output печатается http заголовок, а затем печатаются данные самой страницы. output может быть перенаправлен куда угодно. Можно просто запустить этот скрипт из консоли. Увидим следующее:

./cgi-bin/gettime
HTTP/1.1 200 OK
Content-Type: application/json
Connection: Connection: close

"Fri Feb  5 20:58:19 2021"

А если вместо стандартного output это будет socket то браузер получит эти данные.

CGI часто реализуют с помощью скриптов, даже говорят cgi scripts. Но это не обязательно, просто на скриптовых языках подобные вещи делать быстрее и удобнее. Утилита предоставляющая CGI может быть реализована на любом языке. И так как мы ориентируемся на микроконтроллеры, следовательно, стараемся заботиться об экономии ресурсов. Давайте то же самое реализуем на С.

#include 
#include 
#include 

int main(int argc, char *argv[]) {
    char buf[128];
    char *pbuf;
    struct timeval tv;
    time_t time;

    printf(
        "HTTP/1.1 200 OK\r\n"
        "Content-Type: application/json\r\n"
        "Connection: Connection: close\r\n"
        "\r\n"
    );


    pbuf = buf;

    pbuf += sprintf(pbuf, "\"");

    gettimeofday(&tv, NULL);
    time = tv.tv_sec;
    ctime_r(&time, pbuf);

    strcat(pbuf, "\"\n\n");

    printf("%s", buf);

    return 0;
}

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

В наш app.js добавим обработчик для вызова CGI скрипта для одной из нашей вкладки

app.controller("SystemCtrl", ['$scope', '$http', function($scope, $http) {
    $scope.time = null;

    $scope.update = function() {
        $http.get('cgi-bin/gettime').then(function (r) {
            $scope.time = r.data;
        });
    };

    $scope.update();
}]);

Небольшой нюанс по запуску на Linux с помощью встроенного сервера python. В нашу строку запуска нужно добавить аргумент --cgi для поддержки CGI:

python3 -m http.server --cgi -d .

yzspbpop0ie_nmjgpqyojtbdszu.png

Автоматическое обновление динамического контента


Теперь давайте разберемся с еще одним очень важным свойством динамического сайта — автоматическим обновлением содержимого. Есть несколько механизмов для его реализации:
  • Server Side Includes (SSI)
  • Server-sent Events (SSE)
  • WebSockets
  • И так далее

Server Side Includes (SSI).


Server Side Includes (SSI). Это несложный язык для динамического создания веб-страниц. Обычно файлы использующие SSI имеют формат .shtml.

Сам SSI имеет даже директивы управления, if else и так далее. Но в большинстве примеров для микроконтроллеров, которые мы находили, он используется следующим образом. В .shtml страницу вставляется директива, которая периодически перегружает всю страницу. Это может быть, например


Или

И тем или иным образом происходит генерация контента, например, с помощью задания специального обработчика.

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

4wn3zwt_7nuzr-syva4uw7nnzdi.gif

Обновление страницы (см. вкладку) сильно заметно. И перезагружать всю страницу, выглядит как чрезмерно избыточное действие.

Приведен стандартный пример из FreeRTOS — https://www.freertos.org/FreeRTOS-For-STM32-Connectivity-Line-With-WEB-Server-Example.html

Server-sent Events


Server-sent Events (SSE) это механизм, который позволяет установить полудуплексное (одностороннее) соединение между клиентом и сервером. Клиент в этом случае открывает соединение, и сервер использует его для передачи данных клиенту. При этом, в отличие от классических CGI скриптов, цель которых сформировать и отправить ответ клиенту, после чего завершиться, SSE предлагает «непрерывный» режим. То есть сервер может отправлять сколько угодно данных до тех пор пока либо не завершится самостоятельно, либо клиент не закроет соединение.

Есть несколько небольших отличий от обычных CGI скриптов. Во-первых, http заголовок будет немного другой:

        "Content-Type: text/event-stream\r\n"
        "Cache-Control: no-cache\r\n"
        "Connection: keep-alive\r\n"

Connection, как видно, не close, а keep-alive, то есть продолжающееся соединение. Чтобы браузер не кешировал данные нужно указать Cache-Control no-cache. Ну и наконец, нужно указать что используется специальный тип данных Content-Type text/event-stream.

Этот тип данных представляет из себя специальный формат для SSE:

: this is a test stream

data: some text

data: another message
data: with two lines

В нашем случае данные нужно упаковать в следующую строку

data: { "time”: "”}

Наш CGI скрипт будет выглядеть

#!/bin/bash

echo -ne "HTTP/1.1 200 OK\r\n"
echo -ne "Content-Type: text/event-stream\r\n"
echo -ne "Cache-Control: no-cache\r\n"
echo -ne "Connection: keep-alive\r\n"
echo -ne "\r\n"

while true; do
    tm=`LC_ALL=C date +%c`
    echo -ne "data: {\"time\" : \"$tm\"}\n\n" 2>/dev/null || exit 0
    sleep 1
done

Вывод если запустить скрипт

$ ./cgi-bin/gettime
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive

data: {"time" : "Fri Feb  5 21:48:11 2021"}

data: {"time" : "Fri Feb  5 21:48:12 2021"}

data: {"time" : "Fri Feb  5 21:48:13 2021"}

И так далее раз в секунду

Тоже самое на С

#include 
#include 
#include 

int main(int argc, char *argv[]) {
    char buf[128];
    char *pbuf;
    struct timeval tv;
    time_t time;

    printf(
        "HTTP/1.1 200 OK\r\n"
        "Content-Type: text/event-stream\r\n"
        "Cache-Control: no-cache\r\n"
        "Connection: keep-alive\r\n"
        "\r\n"
    );

    while (1) {
        pbuf = buf;

        pbuf += sprintf(pbuf, "data: {\"time\" : \"");

        gettimeofday(&tv, NULL);
        time = tv.tv_sec;
        ctime_r(&time, pbuf);

        strcat(pbuf, "\"}\n\n");

        if (0 > printf("%s", buf)) {
            break;
        }

        sleep(1);
    }

    return 0;
}

И наконец, нужно еще сообщить angular, что у нас SSE, то есть модифицировать код для нашего контроллера

app.controller("SystemCtrl", ['$scope', '$http', function($scope, $http) {
    $scope.time = null;

    var eventCallbackTime = function (msg) {
        $scope.$apply(function () {
            $scope.time = JSON.parse(msg.data).time
        });
    }

    var source_time = new EventSource('/cgi-bin/gettime');
    source_time.addEventListener('message', eventCallbackTime);

    $scope.$on('$destroy', function () {
        source_time.close();
    });

    $scope.update = function() {
    };

    $scope.update();
}]);

Запускаем сайт, видим следующее:
b0fah456vx3jvgeyt00x2swf-i0.gif

Заметно, что в отличие от использования SSI, страница не перегружается, и данные плавно и приятно для глаза обновляются.

Демо


Конечно приведенные примеры не реальные поскольку очень простые. Их цель показать разницу между используемыми на микроконтроллерах и в остальных системах подходов.

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

Разработка сайта велась на хосте. Нужно было только сделать маленькие заглушки для эмуляции светодиодов и данных с датчика. Данные с датчика это просто случайные значения получаемые через стандартный RANDOM

#!/bin/bash

echo -ne "HTTP/1.1 200 OK\r\n"
echo -ne "Content-Type: text/event-stream\r\n"
echo -ne "Cache-Control: no-cache\r\n"
echo -ne "Connection: keep-alive\r\n"
echo -ne "\r\n"

while true; do
    x=$((1 + $RANDOM % 15000))
    y=$((1 + $RANDOM % 15000))
    z=$((1 + $RANDOM % 15000))
    echo -ne "data: {\"rate\" : \"x:$x y:$y z:$z\"}\n\n" 2>/dev/null || exit 0
    sleep 1
done

Состояние светодиодов просто храним в файле.

#!/bin/python3

import cgi
import sys

print("HTTP/1.1 200 OK")
print("Content-Type: text/plain")
print("Connection: close")
print()

form = cgi.FieldStorage()
cmd = form['cmd'].value

if cmd == 'serialize_states':
    with open('cgi-bin/leds.txt', 'r') as f:
        print('[' + f.read() + ']')

elif cmd == 'clr' or cmd == 'set':
    led_nr = int(form['led'].value)

    with open('cgi-bin/leds.txt', 'r+') as f:
        leds = f.read().split(',')
        leds[led_nr] = str(1 if cmd == 'set' else 0)
        f.seek(0)
        f.write(','.join(leds))

То же самое тривиально реализовано и в C варианте. При желании можно посмотреть код в репозитории папка (project/website).

На микроконтроллере конечно используются реализации взаимодействующие с реальной периферией. Но так как это просто команды и драйвера, они были отлажены отдельно. Поэтому сам перенос сайта на микроконтроллер, не занял времени.

Скриншот запущенный на хосте выглядит так
waoijvxhrxvxtng_gfoiw5h65yu.png

На коротком видео можно увидеть работу на реальном микроконтроллере. Отмечу, что происходит не только общение по http, но и например установка даты с помощью ntp из командной строки в Embox, и конечно обращение с периферией.

Самостоятельно все приведенное в статье можно воспроизвести по инструкции на нашем вики

Заключение


В статье мы показали, что возможно разрабатывать красивые интерактивные сайты и запускать их на микроконтроллерах. Причем делать это легко и быстро используя все средства разработки под хост и затем запускать из на микроконтроллерах. Естественно, разработкой сайта может заниматься профессиональный веб-дизайнер, в то время как embedded разработчик будет реализовывать логику работы устройства. Что очень удобно и сильно экономит время выхода на рынок.

Естественно за это придется расплачиваться. Да SSE потребует немного больше ресурсов чем SSI. Но мы с помощью Embox легко вместились в STM32F4 причем без оптимизации и использовали всего 128 кб ОЗУ. Меньше просто проверять не стали. Так что накладные расходы не такие уж большие. А удобство разработки и качество самого сайта сильно выше. И при этом конечно не стоит забывать, что современные микроконтроллеры заметно подросли и продолжают это делать. Ведь от устройств требуют быть все более умными.

© Habrahabr.ru