Wednesday, January 2, 2013

Arduino Ethernet Shield - немножко температуры в сети

Хочу разобраться с Ethernet Shield для Arduino. Поскольку тема достаточно обширная и уложиться в одну статью будет не просто.
Не пытаясь ухватиться за все и сразу, в этот раз:
  • создадим простой "сервер" на базе Arduino и Ethernet Shield; 
  • подключим его к домашней сети;
  • получим данные через обычный браузер.

Для того, чтобы добавить некоторой "пикантности" и научиться комбинировать проекты - будем на "сервере" измерять температуру используя датчик DS18S20 из другой моей статьи.


История HTTP


Перед тем, как разобраться с Ethernet Shield неплохо было бы подтянуть мат. часть и разобраться с протоколом HTTP. Итак, HTTP - это сокращение от HyperText Transfer Prоtocоl. Рекомендую хотя бы почитать статью на википедии, так как я буду описывать все очень кратко.
В общем, протокол благодарно базируется на другом протоколе более низкого уровня TCP (Transmission Control Protocol). Нужен TCP для того, чтобы передавать данные от одного устройства подключенного в сеть, к другому. Из всего протокола нам интересны только два параметра. 
  • IP-адрес - так как TCP создан для TCP/IP сетей, то каждый хост (далее я буду так именовать устройства), будет иметь свой собственный адрес IP-адресс. Такой адрес состоит из 4-х чисел от 0 до 255 разделенных точками (пример: 192.168.0.100);
  • Порт (port) - число от 0 до 65535 (как по мне, проще запомнить 216)
Итак, мы практически готовы к осознанию протокола HTTP. HTTP - это протокол еще более высокого уровня, чем TCP. В оригинале, он разрабатывался (как следует из название) для передачи гипертекста. Стоит признаться - порой меня поражают осознание того, насколько светлые головы придумывали все эти протоколы, они просто загляденье. 
Итак, HTTP. По сути, это набор правил для сообщений между разными хостами. Другими словами, это некоторый набор условностей, которые позволяют понять одному хосту, что он него хочет другой, и ответить ему в соответствующей манере. Нам придется составлять сообщения в соответствии с этими правилами, так что их понимание сильно упростит процесс.

Постановка задачи


Мне бы хотелось отойти от очень простых примеров которые в сущности повторяют проделанные в сети тысячи раз опыты. В этом эксперименте:
  • Подключим Arduino с помощью Ethernet Shield к сети
  • Создадим простой web-интерфейс через который будем управлять Arduino
  • Подключим к Arduino датчик температуры DS18S20 из этой статьи
  • Подключим к Arduino простой светодиод
Вывод данных с датчика и управление светодиодом должно осуществляться через web-интерфейс.
Используются: Arduino, Ethernet Shield, DS18S20, Светодиод и пара резисторов (100 Ом для светодиода и 4,7 КОм для DS18S20).

Пишем простой и крутой код


Мы приступаем к самому интересному (на мой взгляд) моменту. Код ниже представляет собой всю программу необходимую для запуска нашего чудо-ehternet-термометра. В этот раз я решил поэкспериментировать с Gist сервисом от GitHub для кода. Мне кажется, это будет чуть удобнее.

Задача в этот раз посложнее и кода, соответственно, вышло немного больше. Главное без паники, сейчас во все разберемся.
Перед тем как лезть в главные функции любой Arduino программы loop и setup разберемся со всеми вспомогательными, и пойдем снизу вверх разбирать одну за одной.


  • void connectToSensor() эта функция, как и следующая являются кусочком разбитой на две части прошивки сделанной в статье о температурном датчике, для понимания процесса её придется прочитать. Конкретно этот кусочек кода ищет полный адрес подходящего нам сенсора на 1-wire шине, и как только находит первый - записывает его адрес в переменную addr. В дальнейшем этот адрес будет использоваться для вызова нашего датчика. 
  • float getTemperature(). Вторая часть в работе с датчиком температуры после поиска его адреса - это получение данных и вычисление температуры. Именно этим и занимается функция getTemperature. Следует отметить, что этот код претерпел некоторые изменения после публикации статьи о датчике. Дело в том, что и код на странице используемой для общения с датчиком библиотеки OneWire, тоже был изменен, и я решил последовать их примеру. Изменения не значительны, а идею я уже описывал - не буду повторяться.
  • void updateTemperature() - обновление данных с датчика на каждом цикле работы нашего контроллера будет слишком накладно, так как это достаточно "дорогостоящая" операция, ведь общение по шине 1-wire занимает некоторое время (главные недостаток протокола). Поэтому я обернул обновление данных в функцию и делаю это не чаще раза в 5 секунд. Функция millis() возвращает количество миллисекунд прошедших после запуска контроллера, именно с помощью нее здесь и вычисляется время последнего обновления. Функцию эту можно будет спокойно вызывать в каждом цикле loop.
  • String floatToString(float value, byte precision) - сервисная функция. Все что она делает - это преобразует float значение в строчку. Первый параметр - это само float значение, вторым можно регулировать количество знаков после запятой. Функция работает совсем просто - в начале берем целочисленную часть нашего value, а потом умножаем все значение на precesion и отнимаем вычисленную ранее целочисленную часть. В конце "собирается" строчка, в которой в начале идет целая часть, потом точка, а за ней разность вычисленная на предыдущем шаге. Пример: floatToString(12.258, 100) вернет "12.25" строчку. 
  • void generatePage(EthernetClient client). Вот мы и добрались до HTML странички. Функция generatePage записывает в переданный в качестве единственного параметра объект EthernetClient нашу html страничку. Я не буду расписывать HTML, предполагая, что у вас есть базовые знания этого языка разметки. На сгенерированной странице у нас будет отображаться текущее состояние нашего светодиода, температура полученная с нашего датчика и две ссылки одна для включения, вторая для выключения светодиода.
  • void redirectHeader(EthernetClient client, String path) - любой ответ от сервера передаваемый по http (как и сам запрос собственно) обязательно начинается с header'а. Так вот, метод redirectHeader, как можно догадаться из названия, оправляет пользователю ответ с кодом 302, который означает перенаправление. Получив такой заголовок браузер отправит следующий запрос на адрес указанным в заголовке Location. Наша функция получает этот самый путь в виде строки (второго параметра).
  • void successHeader(EthernetClient client) - эта функция тоже генерирует заголовок http ответа, только в этот раз с кодом 200. Код 200 обозначает успешное выполнение запроса и подразумевает, что после заголовка будут так же отправлены данные (в нашем случае html страница). Функция не принимает никаких строк в качестве параметров, так как всегда генерирует одинаковый ответ.
Теперь можно сказать, что со всеми вспомогательными функциями мы разобрались. Самое время взглянуть не переменные объявленные нами в самом начале кода, сразу после объявления библиотек. О каждой из них по порядку:
  • byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED }; - MAC адрес нашего устройства. Должен быть уникальным для каждого устройства в сети. Вроде бы на новых сериях Ethernet Shield этот адрес будет написан на самом shield, но сам не видел - так что точно не скажу
  • IPAddress ip(192,168,0,33); - создаем объект IPAddress для нашего устройства. Этот адрес будет изменяться в зависимости от настроек вашей сети, но в моем случае это 192.168.0.33
  • EthernetServer server(80); - создается объект server который предоставляет нам интерфейс для реализации собственно сервера. Единственные параметр передаваемый в конструктор - это номер порта, по которому следует ждать запросов от пользователей. 80-й порт выбран не с проста. Дело в том, что это порт по умолчанию для http запросов, и браузер будет обращаться именно к нему, если не указать ему другой специально
  • byte wirePin = 8; OneWire ds(wirePin); byte addr[8]; - собственно объявляем на каком pin у нас будет наш датчик температуры, затем создаем объект класса OneWire, который предоставляет API для работы с этим протоколом, и в конце концов - объявляем переменную которая будет хранить адрес нашего датчика, чтобы его не приходилось каждый раз искать снова
  • byte ledPin = 7; boolean isLedOn = false; - объявляем переменную которая будет хранить в себе номер порта на которой находится наш светодиод, и создаем еще одну boolean переменную которая содержит в себе сведения о текущем состоянии светодиода (false - выключен, true - включен)
  • unsigned long lastUpdate = 0; - переменная куда мы будем записывать время последнего обновления нашего датчика в миллисекунда. Начало отсчета - старт нашего микроконтроллера.
  • float temperature = -100.0; - текущая температура датчика. В качестве значения по-умолчанию я поставил -100, просто потому, что такое значение легко запомнить и его не может выдать наш датчик (физические ограничения), а значит сверив значение переменной temperature с -100 всегда можно сказать установлено это значение датчиком или он просто еще не успел обновить показания.
Осталось разобрать как это все связывается вместе в setup и loop функциях. Самые очевидные места я буду пропускать. Начнем с void setup(void):
  • Ethernet.begin(mac,ip); - передаем объявленные и инициализированные ранее mac и ip метод begin. Это еще не создает соединения, но инициализирует нас как host в сети.
  • server.begin(); - "включает" наш сервер если так можно выразиться. После вызова этой команды, наш сервер получит возможность прослушивать 80-й порт указанный нами при инициализации объекта.
  • connectToSensor(); - вызывает нашу функцию которая должна найти адрес нашего датчика и записать его в переменную addr;
Ну, осталось самое сложное - вникнуть в void loop(void):
  • updateTemperature(); - наша функция, которая обновляет температуру полученную от сенсора, о ней я уже писал, здесь её просто вызываем
  • EthernetClient client = server.available(); - получаем от сервера объект класса EthernetClient, который содержит в себе методы для работы с подключившемся к нашему серверу клиентом (в нашем случае клиентом будет браузер)
  • if(client) { - проверяем, получили ли мы что либо от метода available(). На самом деле, это нормально не получить ничего и просто снова вернуться в начало функции loop. Так будет происходить все время, пока никто не подсоединиться к нашему серверу. В таком случае сервер будет "в холостую" гонять цикл loop в ожидании клиента.
  • String request; boolean currentLineIsBlank = true; boolean requestLineReceived = false; - если клиент все же нашелся, мы объявляем несколько переменных. Строка request будет содержать в себе первую строчку HTTP запроса - именно она содержит относительный путь ресурса, к которому клиент пытается получить доступ. Исходя их этого относительного пути мы и будем в последствии выполнять те или иные действия. Две других переменных вспомогательные. currentLineIsBlank - помогает нам отловить случай, когда приходит пустая строка запроса - это обозначает окончание запроса и момент времени, когда самое время отправлять ответ клиенту. Вторая переменная requestLineReceived - является флагом, который выставляется в значение true после того, как первая строчка будет получена и записана в упомянутую уже переменную request. Этот флаг обозначает, что последующие строчки запроса можно пропускать, так как они у нас никак не обрабатываются.
  • while(client.connected()){ - цикл, который остановится если клиент отсоединиться так и не дождавшись ответа от нашего сервера. В этом цикле мы будем получать запрос от клиента, а так же формировать ответ.
  • if (client.available()) { - проверяет доступны ли еще данные от клиента. Так как запрос поступает по сети, то может быть и такое, что мы уже обработали все данные которые были доступны, но это еще не является окончание запроса. Тогда описанный ранее цикл while будет крутиться до тех пор, пока новая порция данных не будет получена
  • char c = client.read(); - считываем символ из потока данных доступных от клиента. Далее идет обработка этого символа. 
Описание дальнейших строчек не представляется мне такой уж хорошей идеей, так там переплетается сразу несколько действий и придется часто перепрыгивать с одного куска кода на другой - это ухудшает восприятия. Если описывать суть, то я бы её формулировал следующим образом:
В начале в строку request по одному добавляются символы до тех пор, пока мы не встретим символ окончания строки. В результате строка request будет содержать первую строчку полученную нами от клиента. Все остальные строчки запроса клиента пропускаются и мы лишь ожидаем окончание запроса. Напомню, если кто-то так и не читал про http - окончанием запроса является пустая строка. 
Сразу после окончания запроса клиента мы начинаем разбирать первую строку его запроса помещенную ранее в переменную request. В этой строке нас будет интересовать значение path которое находится между двумя пробелами, после декларации HTTP метода и до обозначения версии протокола.
После того, как строка получена, мы начинаем сравнивать её с тремя возможными значениями. Если строка ссылается к корню (иными словами равна "/"), то мы вызываем функцию которая генерирует нам header с 200-м кодом, а потом другой функцией передаем клиенту нашу html страницу. 
Если же строка равна /switch-on или /switch-off  включаем или выключаем светодиод соответственно, изменяем сведения о его состоянии и перенаправляем пользователя в корень нашего сервера.
После этого мы ожидаем одну миллисекунду, чтобы клиент успел получить все данные от нашего сервера с помощью функции delay, и последним шагом закрываем соединение с этим клиентом client.stop();.
Вот собственно и весь код. Я даю себе слово, что больше не буду так подробно разбирать код построчно, так как это занимает ну очень много времени. Ладно, продолжим.

Собираем схему

В принципе, эта схема будет практически идентична той, которую мы используем в примере с датчиком, только в этот раз к Arduino будет сверху нахлабучен еще и shield, ну и конечно же добавиться светодиод - куда же без него.



Видео, куда без него

Ничего особенного, поморгали светодиодом, посмотрели температуру.



Вот собственно и все, что хотелось рассказать.