В предыдущей статье мы остановились на месте, где Webduino уже мог обслуживать нас любыми файлами с SD-карты. Теперь нам нужно пропустить выбранные файлы через наш, так называемый, PHP и отправить результат клиенту.
Чтобы упростить весь процесс, мы предполагаем, что каждый файл, который необходимо обработать, нам известен. То есть мы регистрируем каждый такой файл (URL) с расширением addCommand.
Но как это должно работать? Идея в том, что у нас есть свои функции в коде скетча и результат работы которых нужно вставить в выбранные места в HTML-коде. Итак, мы хотим иметь файл HTML с этим фрагментом кода:
1 2 3 4 |
<p> Sensor reading result 1: RESULT1<br/> Sensor reading result 2<br/> </p> |
В результате работы нашего парсера мы хотим, чтобы RESULT1 и RESULT2 были заменены на результат работы функции в скетче.
Но, как сохранить в HTML то, что наш парсер должен поставить вместо него другой текст. Для простоты примем следующую формулу: #{X} она будет заменена вызовом соответствующей функции. X это мнемоника из одной буквы, для которой мы хотим вызвать функцию.
Функции должны иметь конкретное определение и не могут принимать аргументы. Почему? Принятие аргументов усложняет синтаксический анализатор и, вероятно, на данный момент в этом нет необходимости.
Мы выбрали формат HTML. Но, как наша функция будет передавать результат операции? Итак, мы предполагаем, что пример функции имеет следующее определение:
1 |
void timeReport(char *buf) { itoa(millis()/1000, buf, 10); }; |
Предполагается, что функция не возвращает данные ( void ), а принимает указатель на текстовый буфер. В этот буфер предполагается поместить результат своей работы, который затем будет вставлен в соответствующее место в HTML. Сама функция должна следить за тем, чтобы не переполнить этот буфер. Его размер определяется:
P4A_MAX_FUNCTION_RESPONSE_LENGTH .
Как видите, приведенная выше функция возвращает количество полных секунд, прошедших с момента запуска или сброса Arduino.
Как разобрать файл? Благодаря простому маркеру — это относительно просто. Читаем файл посимвольно. Если мы встречаем #, то читаем следующий символ и проверяем, является ли он фигурной скобкой { которые вместе образуют открывающую последовательность наших результатов. Пока нам не попадаются # (эти неинтересные символы), мы отправляем их в буфер, который в итоге будет отправлен в браузер.
Если следующий символ после # не фигурная скобка, мы отправляем в браузер и #, и следующий символ — нам в этот момент делать нечего, ждем следующего #.
Однако, если вторым знаком была фигурная скобка, то читаем следующий знак — это наша мнемоника! Вызываем соответствующую функцию в зависимости от мнемоники, отправляем результат в браузер.
Затем мы читаем файл до закрывающей скобки и сканируем файл в поисках следующего символа #.
Остается вопрос о назначении функций мнемоникам. Нам для этого послужит массив указателей на функцию.
Массив указателей на функцию
Теперь мы поговорим о более сложной теме — указателях на функции. Ну, если подумать, то скомпилированная функция — это адрес, по которому находится исполняющий ее код, и некий контракт, определяющий, как данные передаются в функцию, и как они из нее возвращаются.
Если контракт один и тот же для нескольких функций (то есть список типов аргументов и возвращаемое значение), функции можно записать в виде указателя и сохранить в массиве. Затем мы можем вызывать такую функцию по сохраненному указателю, нам не нужно знать ее имя в коде.
Именно это и послужит нам механизмом перевода мнемоники в вызываемые функции. Теперь вы знаете, почему все наши функции должны иметь одинаковый интерфейс/контракт (как мы установили будет для void возвращаемых данных и char * в качестве аргумента) — благодаря этому мы можем хранить их в одном массиве, индексом которого будет буква. Перво-наперво:
1 |
void (*_fcts['z'-'a'])(char *); |
Мы определяем массив указателей на функции. Пустота впереди указывает тип возвращаемого значения функции, что в круглых скобках в конце указывает, какие аргументы ожидает функция. Посередине находится объявление массива. Его размер ‘z’-‘a’ может показаться странным, но в этом контексте символы рассматриваются компилятором как байты. Итак, из кода «z» вычитаем код «a» — разница в количестве букв. Благодаря этому у нас есть массив, который может содержать столько указателей на функции, сколько строчных букв в латинице (точнее, в стандарте ASCII).
Если у нас есть буквенный код, просто вычитаем из него буквы и мы получим индекс из таблицы. В любом случае, давайте посмотрим код:
1 2 3 4 5 6 7 8 9 |
if (_fcts[c[0]-'a'] == NULL) { bufferedSend(server,"n/a"); continue; } else { //Вызов функции из таблицы _fcts[ c[0]-'a'](buf); //Запись ответа клиенту bufferedSend( server, buf ); } |
c[0] содержит знак нашей мнемотехники. Если массив не имеет значения (т. е. NULL) в индексе, c[0] — ‘a’ тогда вставляется HTML n/a — наш способ сигнализировать о плохой мнемонике.
‘a’ в ASCII он имеет код 97. Если наша мнемоника тоже ‘a‘ 97-97 = 0, то это первый элемент массива. Если мнемоника ‘b’ = ‘b’-‘a’ 98 — 97 = 1, то есть второй элемент массива и т.д. Было бы полезно проверить, находится ли мнемоника в правильном диапазоне, иначе можно попробовать вызвать функцию со случайным адресом (если индекс массива вне допустимого диапазона, то ОЗУ из-за пределов массива будет прочитано и процессор попытается интерпретировать значение из этого адреса как адрес вызываемой функции — гарантированный сбой).
Приведенный выше код также показывает, как вызывать функции в массиве:
1 |
_fcts[ c[0]-'a'](buf); |
Мы используем переменную для анализа файла status , благодаря которой мы знаем состояние нашего парсера. Возможно:
- P4A_PARSE_REGULAR — состояние, в котором ищем знак #
- P4A_PARSE_HASH — состояние, в котором ждем открытия скобки
- P4A_PARSE_COMMAND — состояние, в котором ищем мнемотехнику
Эти состояния упрощают управление тем, что делает наш парсер. В зависимости от текущего состояния и следующего знака вы можете решить, что делать дальше. Отправка данных в браузер или вызов функции на основе мнемоники.
Кому интересно, вот вся функция parseP4A :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
//Читает HTML-файл, анализирует наш макрос и отправляет обратно клиенту int parseP4A( char * filename, WebServer &server ) { //простой статус short int STATUS = P4A_PARSE_REGULAR; char c[2]; c[1] = 0; //буфер для хранения ответов от функций - проверка границ отсутствует, //поэтому функция не должна перезаписывать данные char buf[P4A_MAX_FUNCTION_RESPONSE_LENGTH]; if (! file.open(&root, filename, O_READ)) { return -1; } while ((file.read(c,1) ) > 0) { if (STATUS == P4A_PARSE_REGULAR && c[0] == '#') { //найден хэш, нам нужно проверить следующий статус STATUS = P4A_PARSE_HASH; continue; } if (STATUS == P4A_PARSE_HASH) { if (c[0] == '{') { //переходим в командный режим STATUS = P4A_PARSE_COMMAND; continue; } else { //вернуться к обычному режиму, но сначала отправить отложенный хеш STATUS = P4A_PARSE_REGULAR; bufferedSend(server, "#"); } } if (STATUS == P4A_PARSE_COMMAND) { if(c[0] == '}') { STATUS = P4A_PARSE_REGULAR; continue; }; if (c[0] >= 'a' && c[0] <='z') { //Команда найдена if (_fcts[c[0]-'a'] == NULL) { bufferedSend(server,"n/a"); continue; } else { //Вызов функции из таблицы _fcts[ c[0]-'a'](buf); //Запись ответа клиенту bufferedSend( server, buf ); } } } if (STATUS == P4A_PARSE_REGULAR) bufferedSend(server, c); } //принудительная очистка буфера flushBuffer(server); file.close(); return 0; } |
P4A или PHP для Arduino в работе
Предположим, мы хотим сделать хороший виртуальный барометр, но показывающий реальное давление. Мы будем использовать BMP085 — это удобная плата для подключения датчика давления и температуры. Ожидается, что результат будет как на картинке:
Стрелка должна показывать значение, считанное с датчика, а символ прогноза погоды должен меняться в зависимости от значения давления.
Как быть с тем, что, как мы писали в предыдущем эпизоде, веб-сервер Arduino — не лучшее решение, когда нужно обслуживать много файлов одновременно (картинок!)? Ну, если все это в любом случае должно быть доступно из сети, почему бы не поделиться статическими ресурсами с сетевого сервера? У нас есть такой сервер для своих нужд и на нем размещены все необходимые графические элементы. То есть циферблат барометра, изображение указателя и символы погоды.
На Arduino есть только файл HTML, на SD-карте. В скетче поместим функцию, которая вызывает парсер для этого файла:
1 2 3 4 5 6 |
void index(WebServer &server, WebServer::ConnectionType type, char *, bool){ server.httpSuccess(); if (!parseP4A("BARO.HTM", server)) { Serial.println("P4A: -1"); } }; |
parseP4A — это функция, которая анализирует данный файл и отправляет результат, используя объект сервера Webduino. Осталось зарегистрировать нашу функцию как команду по умолчанию:
1 |
webserver.setDefaultCommand(&index); |
Сам HTML использует JavaScript и объект canvas для работы самого Ethernet Shield. Это делается функцией draw, которая принимает в качестве аргумента давление в гектопаскалях. Когда страница готова к отображению (т.е. загружена), мы в onload вызываем функцию draw через атрибут, давление вставляется нашим P4A:
1 |
<body onload="draw(#{p});" |
Мнемоника p должна быть связана с правильной функцией. В скетче setup мы устанавливаем функцию для p:
1 |
_fcts['p'-'a'] = pressureReport; |
pressureReport имеет вид:
1 2 3 4 5 6 7 8 9 |
void pressureReport(char *buf) { bmp085_read_temperature_and_pressure (&temperature, &pressure); itoa(pressure/100.0, buf, 10); Serial.print ("temp & press: "); Serial.print (temperature/10.0); Serial.print(" "); Serial.println (pressure/100.0); }; |
Серийный номер используется для проверки того, что значения соответствуют ожидаемым и не влияют на работу барометра. bmp085_read_temperature_and_pressure — функция из обработчика BMP085.
Вы можете скачать весь код с нашего ЯндексДиска. Это скетч, который управляет нашим сервером, а также файл HTML и графика. Циферблат барометра, графика и код HTML/JS.
Скачайте архив и распакуйте в sketchbook. Поместите содержимое подкаталога html в основной каталог на SD-карте, поместите его в шилд. Исправьте скетч, введя правильный MAC и IP-адрес. После открытия главной страницы мы должны увидеть барометр, если у вас используется тоже датчик BMP085.
Итог
Код является бета-версией, т.е. — работает настолько, насколько это подтверждают наши тесты, могут быть (и наверняка есть) несколько багов, о которых мы понятия не имеем…
Код должен быть отсортирован, — например, функции, связанные с буферизованным выводом, должны быть перемещены в код Webduino. Мы планируем это сделать и отправить все изменения, которые были внесены в Webduino, разработчикам Webduino — возможно, что-то из этого будет прямо в библиотеке.
С Уважением, МониторБанк