После знакомства с Arduino, в голове каждого, рано или поздно, появится идея для проекта, который потребует подключения платы Ардуино к сети. И не важно, что это будет: домашняя автоматика, доступная по сети, или набор датчиков, передающих показания в базу данных, но Ардуино все равно нужно как-то подключить к Интернету. Здесь нам пригодится плата Ethernet Shield.
Сначала немного истории. Изначально плата Ethernet Shield была совместима с небольшим Arduino. Почему не с Мега? Ну, потому, что для связи с чипом W5100, который является сердцем шилда, используется протокол SPI — на цифровых входах: 10, 11, 12 и 13. В Arduino Mega SPI находится на других входах. Обойти это можно было, согнув ножки, и подключив их кабелем к нужным цифровым входам.
Но такие манипуляции проводились со старыми шилдами. В настоящее время, платы Ethernet Shield совместимы как с небольшими Arduino (UNO), так и с Mega (с процессорами ATmega1280 и ATmega2560 ). Как распознать обновленный шилд? Все очень просто, если у шилда есть слот для карты microSD — значит, это более новая версия Ethernet Shield.
Мы уже говорили, что W5100 — интегральная схема, управляющая Ethernet Shield. Данная схема отличается от многих других Ethernet-контроллеров тем, что стек TCP/IP реализован непосредственно на кристалле. Но что это значит для пользователя? Что библиотека, которую вам нужно использовать для связи, требует меньше оперативной памяти и занимает меньше флэш-памяти по сравнению с чипами, на которых нет стека TCP/IP.
Как использовать Ethernet Shield?
В Интернете есть много примеров того, как создавать веб-сайты, которые отображают данные из Arduino. Впрочем, если у вас уже есть опыт программирования на Arduino, вы наверняка знаете, что все строки, даже определенные в коде, занимают оперативную память, которой всегда не хватает.
Возьмем официальный пример кода из руководства по Arduino:
1 2 3 |
client.println("HTTP/1.1 200 OK"); client.println("Content-Type: text/html"); client.println(); |
Потребуется 40 байт оперативной памяти (15 символов в HTTP… + конечный ноль и 23 в Conte… + конечный ноль). Легко представить, что это значит, когда в нашем распоряжении вообще 2 КБ. HTML-страница не может быть слишком сложной.
Есть решение, которое может нам помочь — хранение строк во флэш памяти. Это может уменьшить использование оперативной памяти, но часто за счет дополнительного кода. Доступ к строкам, определенным таким образом, требует использования специального макроса, и компилятор не позволит нам использовать этот макрос при вызове функции ожидания.
1 |
char * |
Кроме того, любое изменение HTML-кода, которое мы хотим отправить, означает, что мы должны изменить скетч и загрузить его в Arduino.
Подождите, скажите вы, но у Ethernet Shield был слот для карты microSD — разве мы не можем как-то использовать пространство, предоставленное SD-картой? Можем, но нужно немного постараться.
Первое — Ethernet Shield необходимо настроить для работы в сети — это означает, что нужно произвести настройку MAC и IP адресов. Это можно сделать следующим образом:
1 2 |
byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED }; byte ip[] = { 192,168,1, 177 };Ethernet.begin(mac, ip); |
MAC-адрес лучше всего прочитать самостоятельно с наклейки внизу шилда. IP-адрес зависит от конфигурации сети.
1 |
Ethernet.begin |
Приведенная выше последовательность строк будет работать только в локальной сети — т.е. когда все IP-адреса находятся в одной подсети. Если шилд должен подключаться к хостам в других сетях (как в качестве клиента, так и в качестве сервера), вы должны указать еще один аргумент — таблицы с 4 числами — IP-адрес шлюза по умолчанию. Подробнее в описании на сайте Ардуино «Интернет библиотеки».
Большая часть скетча почти готова. Интернет библиотека, входящая в состав Arduino IDE, — это не то, что подойдет нам лучше всего. Это облегчает создание, в том числе TCP-сервера, но еще нужна часть для HTTP-сервера. Вот почему нам нужен Webduino — на основе Интернет библиотеки (кто-то проделал за нас большую работу по созданию HTTP-сервера).
Загружаем библиотеку со страницы «Загрузки» и распаковываем ее.
1 |
sketchbook/libraries |
Мы немного модифицируем Webduino для наших нужд, он будет доступен для скачивания в конце этой статьи, так что пока ничего устанавливать не нужно.
Вебдуино — как начать?
Для начала нам нужно знать, что использование этой строки:
1 |
void addCommand(const char *verb, Command *cmd); |
даст нам возможность зарегистрировать любую функцию, вызываемую при совпадении URL:
1 |
verb |
Пример:
1 |
webserver.addCommand("blob.htm", &blob); |
Если URL-адрес начинается (то есть часть после адреса Arduino) с blob.htm , то функция будет вызываться:
1 |
blob |
который должен принимать аргументы в соответствии с определенным типом:
1 |
Command |
1 |
typedef void Command(WebServer &server, ConnectionType type, char *url_tail, bool tail_complete); |
server объект WebServer, для которого был вызван метод type; тип подключения INVALID, GET, HEAD, POST; url_tail — это то, что осталось в URL-адресе после сопоставления blob.htm . Если URL-адрес был усечен из-за небольшого буфера, используемого Webduino (память), последний параметр tail_complete будет иметь значение false.
Хорошо, но не очень удобно прописывать каждую функцию, тем более, что мы хотим обслуживать данные с SD-карты, содержимое которой нам неизвестно. Нам сейчас пригодится:
1 |
setFailureCommand |
Эта строка позволит нам зарегистрировать функцию в нашем коде, вызываемом, когда не было другого совпадения с зарегистрированными функциями:
1 |
addCommand |
Поэтому, если URL-адрес не соответствует какой-либо ранее сообщенной функции, будет вызвана функция, указанная для:
1 |
setFailureCommand |
Теперь нужно проверить, так ли это:
1 |
url_tail |
Это имя файла на SD-карте (поскольку совпадения не было, url_tail содержит полный URL-адрес, включая первый символ / после адреса Arduino). Когда файла нет — выводим HTTP 404, если есть — просто отправляем клиенту.
Как прочитать файл с SD-карты?
Как и в случае с HTTP-сервером, нам не нужно делать все самостоятельно. Мы будем использовать библиотеку SdFatLib , которая имеет поддержку файловых систем FAT16 и FAT32 (что обычно и поддерживается картами SD и SDHC).
Код для инициализации и обработки файлов:
1 |
fetchSD |
который был зарегистрирован:
1 |
setFailureCommand |
Его задача — найти файл во вкладке и отправить его в браузер:
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 |
P(CT_PNG) = "image/png\n"; P(CT_JPG) = "image/jpeg\n"; P(CT_HTML) = "text/html\n"; P(CT_CSS) = "text/css\n"; P(CT_PLAIN) = "text/plain\n"; P(CT) = "Content-type: "; P(OK) = "HTTP/1.0 200 OK\n"; void fetchSD(WebServer &server, WebServer::ConnectionType type, char *urltail, bool){ char buf[32]; int16_t readed; ++urltail; char *dot_index; if (! file.open(&root, urltail, O_READ)) { //404 webserver.httpNotFound(); } else { if (dot_index = strstr(urltail, ".")) { ++dot_index; server.printP(OK); server.printP(CT); if (!strcmp(dot_index, "htm")) { server.printP(CT_HTML); } else if (!strcmp(dot_index, "css")) { server.printP(CT_CSS); } else if (!strcmp(dot_index, "jpg")) { server.printP(CT_JPG); } else { server.printP(CT_PLAIN); } server.print(CRLF); } readed = file.read(buf,30); while( readed > 0) { buf[readed] = 0; bufferedSend(server,buf,readed); readed = file.read(buf,30); } flushBuffer(server); file.close(); } } |
В самом начале мы зарегистрировали несколько символьных констант, хранящихся во флеш-памяти:
1 |
P(CT_PNG) = "image/png\n"; |
макрос:
1 |
P |
Этот макрос является частью Webduino, который используется для записи строк во флэш-память, а не в ОЗУ. И эти константы являются именами различных форматов данных, так называемого MIME Type, который мы хотим обрабатывать. О чем это? Браузер не знает, какие данные будут отправлены сервером. Будет это HTML или изображение, она узнает из заголовка Content-Type, о котором поговорим чуть позже.
Затем мы «избавляемся» от слэша:
1 |
++urltail; |
Затем пытаемся открыть файл на SD-карте — если не получается — выводим ошибку HTTP 404 (Not Found):
1 2 3 4 |
if (! file.open(&root, urltail, O_READ)) { // 404 webserver.httpNotFound(); } else { |
Если файл успешно открылся, то в:
1 |
else |
То мы постараемся прочитать его и отправить клиенту.
Теперь несколько комментариев. Во-первых, SdFatLib поддерживает только короткие имена в формате 8.3. Если вы попытаетесь использовать более длинные имена (что позволяет FAT32), помните, что имя, видимое SdFatLib, будет отличаться от того, которое вы увидите при сопряжении карты на своем компьютере. А если сделать:
index.html
(четыре символа в расширении) то имя будет
ind~1.htm
для SdFatLib.Ну и даже если сейчас переименовать комп в
index.htm
, то запись в каталоге будет в расширенном виде. Вам нужно удалить файл и создать его заново с именем в формате 8.3.
Второе замечание таково — по понятным причинам, мы не будем заботиться о каталогах и будем считать, что все файлы находятся в основном каталоге. Возможно, в более поздних версиях кода мы добавим поддержку немного более сложных структур.
Ну, хорошо, вернемся к коду:
1 |
fetchSD |
Так как нам удалось открыть файл, то ищем точку в имени файла и если находим, проверяем, будет ли остальное (расширение) соответствовать известным нам расширениям. Потому что нам недостаточно отправить данные HTTP-клиенту — мы должны отправить заголовок с информацией о правильном Content-Type (о нем мы говорили ранее), иначе данные могут быть неправильно интерпретированы браузером. </p>
Несколько слов о том, как выглядит ответ веб-сервера. Он разделен на две части. Первая, это так называемые заголовки. Браузер воспринимает все в начале как заголовок, пока не встретит пустую строку текста (строки разделяются символами CR LF). В остальном правильный ответ. То, что интерпретируется, будет зависеть от заголовков. Сервер может помочь браузеру, установив заголовок, определяющий тип данных:
1 |
Content-type: text/html |
До первого двоеточия находится имя заголовка ( здесь Content-Type ), за которым следует значение заголовка. Здесь используются так называемые MIME — типы (можете найти в Википедии). Например:
1 |
image/png |
для изображения PNG:
1 |
image/jpg |
для JPG или:
1 |
text/html |
для HTML-файла.
Обязательным заголовком является статус — то есть был ли обработан запрос клиента, была ли ошибка или перенаправление:
1 |
HTTP/1.0 200 OK |
Мы имею в виду, что пока все идет хорошо, будет контент. Во-первых, это протокол:
1 |
HTTP |
и его версия (версия 1.0) а потом сам код 200 — в HTTP — это означает, что все в порядке. Другими распространенными кодами являются 404 — ресурс не найден (знаменитый Not Found), 301 и 302 — редиректы.
Зная это, мы пытаемся распознать расширение файла и отправить соответствующий заголовок:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
if (dot_index = strstr(urltail, ".")) { ++dot_index; server.printP(OK); server.printP(CT); if (!strcmp(dot_index, "htm")) { server.printP(CT_HTML); } else if (!strcmp(dot_index, "css")) { server.printP(CT_CSS); } else if (!strcmp(dot_index, "jpg")) { server.printP(CT_JPG); } else { server.printP(CT_PLAIN); } server.print(CRLF); |
Итак, у нас уже есть отправленный HTTP-заголовок (заканчивается пустой строкой):
1 |
server.print(CRLF) |
поэтому мы будем отправлять только данные:
1 2 3 4 5 6 7 8 |
readed = file.read(buf,30); while( readed > 0) { buf[readed] = 0; bufferedSend(server,buf,readed); readed = file.read(buf,30); } flushBuffer(server); file.close(); |
Считываем 30 байт, отправляем клиенту через функцию буфера отправленных данных. Зачем? Ну а если использовать самое простое решение и отправлять данные посимвольно, то каждый символ будет в отдельном TCP-пакете. Это будет очень неэффективное решение. Потому, что:
1 |
server.write |
отправляет данные медленно.
Вот почему мы написали функцию:
1 |
bufferedSend |
которая принимает в качестве аргументов объект веб-сервера, указатель на буфер данных и размер буфера. Почему бы нам не использовать функции размера буфера символов, такие как:
1 |
strlen |
Потому что это может работать только тогда, когда данные являются текстовыми. Если данные двоичные (изображения), то маркер конца строки может появиться в потоке допустимых данных.
В WC и C++ концом строки является символ 0 (не цифра, а только байт со значением 0). Если в нашем потоке данных могут появляться нули, то все функции, связанные со строками и предлагаемые стандартной библиотекой, нам не пригодятся.
По этой причине мы должны явно указать количество данных, отправляемых в буфер.
И в основном это все. У нас есть веб-сервер на Arduino, который отправляет данные с SD-карты.
Имеет ли это смысл?
Всего несколько тестов с более сложным веб-сайтом (не один файл HTML, а несколько CSS и изображений), чтобы увидеть, что у этого решения есть свои ограничения. Arduino однопоточный, поэтому каждый элемент с нашего веб-сервера загружается по очереди. Это означает, что сайт загружается медленно с точки зрения пользователя.
Так для чего это нужно? Arduino может представить данные, собранные с датчиков, в более удобной форме, если нет ограничения объема оперативной памяти, необходимой для более сложного веб-сайта. Сохраняя HTML-код на SD-карте, мы избавляемся от этого ограничения. Но как поместить данные, собранные Arduino с датчиков, в HTML, хранящиеся на SD-карте?
Нам нужно что-то, что позволит нам вводить данные в HTML между «чтением» и «отправкой». Итак, что-то вроде PHP для Ардуино…
Конечно же, это преувеличение! Нам нужно что-то, что больше похоже на шаблоны, чем на полноценный PHP, но в начале PHP тоже не был полноценным
Как сделать такой парсер (и полный код скетча) — в нашей следующей статье «PHP для Arduino — часть 2».
С Уважением, МониторБанк