12. Программирование сетевых взаимодействий

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


Сетевое оборудование

У вас на выбор есть несколько вариантов подключения Arduino к сети. Можно использовать плату расширения Ethernet с Arduino Uno, или приобрести модель Arduino со встроенным адаптером Ethernet, или раскошелиться и приобрести плату расширения WiFi для подключения к беспроводной сети.


Плата расширения Ethernet

Плата расширения Ethernet (рис. 12.1) не только дает возможность подключения к сети Ethernet, но и имеет слот для карты памяти microSD, которую можно использовать для хранения данных (см. раздел «Использование SD-карты» главы 6).


Рис. 12.1. Плата расширения Ethernet


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


Arduino Ethernet/EtherTen

Альтернативой использованию отдельной платы расширения является покупка Arduino со встроенным адаптером Ethernet. Официальной считается модель Arduino Ethernet, однако в продаже имеется очень неплохая и совместимая с Uno плата EtherTen, производимая компанией Freetronics (www.freetronics.com) (рис. 12.2).


Рис. 12.2. Плата EtherTen


Комбинированные платы, содержащие все необходимое, наиболее предпочтительны для сетевых проектов на основе Arduino. Платы Arduino Ethernet поддерживают технологию питания по линиям Ethernet (Power over Ethernet, PoE) через отдельный инжектор PoE, что позволяет уменьшить количество проводов, идущих к плате Arduino, до единственного кабеля Ethernet. Платы EtherTen выпускаются уже настроенными на питание с использованием технологии PoE. Более полную информацию об использовании технологии PoE в платах EtherTen можно найти по адресу www.doctormonk.com/2012/01/power-over-ethernet-poe.html.


Arduino и WiFi

Главная проблема подключения к Интернету через Ethernet заключается в необходимости прокладки кабеля. Если вы хотите подключить Arduino к Интернету или сети без использования проводов, то вам потребуется плата расширения WiFi (рис. 12.3). Эти платы стоят довольно дорого, но есть более дешевые альтернативы сторонних производителей, такие как Sparkfun WiFly (https://www.sparkfun.com/products/9954).


Рис. 12.3. Плата Arduino WiFi


Библиотека Ethernet

Библиотека Ethernet претерпела существенные изменения с момента выпуска в 2011 году версии Arduino 1.0. Она не только позволяет плате Arduino с адаптером Ethernet действовать в роли веб-сервера или веб-клиента (возможность посылать запросы, подобно браузерам), но и реализует дополнительные возможности, такие как поддержка протокола динамической конфигурации сетевого узла (Dynamic Host Configuration Protocol, DHCP), автоматически присваивающего плате IP-адрес.


ПРИМЕЧАНИЕ

Превосходное описание библиотеки Ethernet можно найти в официальной документации Arduino: http://arduino.cc/en/reference/ethernet [10].


Создание соединения

На первом этапе, прежде чем приступить к взаимодействиям по сети, необходимо установить соединение с сетью. Эту задачу решает функция Ethernet.begin(). Она позволяет вручную указать все параметры соединения с использованием следующего синтаксиса:

Ethernet.begin(mac, ip, dns, gateway, subnet)

Рассмотрим каждый из этих параметров:

• Mac — MAC-адрес сетевой карты (я расскажу о нем чуть позже);

• Ip — IP-адрес платы (можно выбрать любой допустимый для вашей сети);

• Dns — IP-адрес сервера доменных имен (Domain Name Server, DNS);

• Gateway — IP-адрес шлюза для выхода в Интернет (ваш домашний концентратор);

• Subnet — маска подсети.

Этот синтаксис кажется немного пугающим тем, кто не имеет опыта настройки параметров подключения к сети вручную. К счастью, все параметры, кроме mac, являются необязательными, и в 90% случаев вам придется указывать только параметры mac и ip или, весьма вероятно, только mac. Все остальные параметры будут настроены автоматически.

MAC-адрес, или адрес доступа к среде (Media Access Control), — это уникальный идентификатор сетевого интерфейса. Иными словами, это адрес платы расширения Ethernet или чего-то другого, предоставляющего сетевой интерфейс в распоряжение Arduino. Этот адрес должен быть уникальным только для вашей сети. Его обычно можно найти на наклейке с обратной стороны платы Ethernet или WiFi (рис. 12.4) или на упаковке. Если вы пользуетесь старой платой, не имеющей MAC-адреса, то можете просто создать свой адрес. Но не используйте в своей сети один и тот же адрес дважды.

Можно создать соединение с сетью с применением DHCP и получить динамический IP-адрес, как показано далее:

#include

#include

byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED };

void setup()

{

Ethernet.begin(mac);

}


Рис. 12.4. Наклейка с MAC-адресом на плате WiFi


Если потребуется присвоить плате фиксированный IP-адрес, что желательно, когда плата Arduino действует в роли веб-сервера, используйте примерно такой код:

#include

#include

byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED };

byte ip[] = { 10, 0, 1, 200 };

void setup()

{

Ethernet.begin(mac, ip);

}

IP-адрес в параметре ip должен быть допустимым для вашей сети. Если вызвать функцию Ethernet.begin без параметра с IP-адресом, она по­пытается получить его с использованием DHCP и вернет 1, если соединение было установлено и динамический IP-адрес успешно получен, в противном случае вернет 0. Можно написать тестовый скетч, который будет устанавливать соединение и вызывать функцию localIP для получения IP-адреса, присвоенного Arduino. Следующий пример выполняет такую проверку и выводит сообщение с результатами в монитор последовательного порта. Это полноценный скетч, который вы можете опробовать самостоятельно. Но не забудьте заменить в коде MAC-адрес на указанный на вашей плате:

// sketch_12_01_dhcp

#include

#include

byte mac[] = { 0x00, 0xAA, 0xBB, 0xCC, 0xDE, 0x02 };

void setup()

{

Serial.begin(9600);

while (!Serial){}; // для совместимости с Leonardo

if (Ethernet.begin(mac))

{

Serial.println(Ethernet.localIP());

}

else

{

Serial.println("Could not connect to network");

}

}

void loop()

{

}


Настройка веб-сервера

Проект «Физический веб-сервер», описанный далее в этой главе, иллюстрирует организацию скетча, реализующего веб-сервер. А в этом разделе мы рассмотрим функции, помогающие реализовать веб-сервер.

Большая часть функций, необходимых для реализации веб-сервера, содержится в классе EthernetServer. Для запуска веб-сервера после установки соединения с сетью требуется пройти еще два этапа. Во-первых, нужно создать новый объект сервера, указав номер порта, который должен использоваться для приема входящих запросов. Это объявление находится в скетче перед функцией setup:

EthernetServer server = EthernetServer(80);

Обычно для приема запросов веб-серверы используют порт 80. То есть если вы настроили свой веб-сервер на обслуживание порта 80, вам не ­придется добавлять этот номер в адреса URL, чтобы связаться с сервером.

Во-вторых, чтобы фактически запустить сервер, в функции setup нужно выполнить следующую команду:

server.begin();

Эта функция запустит сервер, который будет ждать, пока кто-то не запросит страницу, которую обслуживает данный сервер. Фактическое обслуживание осуществляется в функции loop с применением функции available. Эта функция возвращает null (если нет запросов для обслуживания) или объект EthernetClient. Данный объект, как это ни странно, используется также для отправки исходящих запросов из Arduino к внешним веб-серверам. В данном случае EthernetClient представляет соединение между веб-сервером и браузером клиента.

Получив этот объект, можно прочитать входящий запрос с помощью read и вернуть HTML-ответ с помощью функций write, print и println. Закончив отправку HTML-ответа клиенту, нужно завершить сеанс вызовом функции stop объекта клиента. Я расскажу, как это сделать, в разделе «Физический веб-сервер» далее в этой главе.


Выполнение запросов

Плата Arduino может действовать не только как веб-сервер, но и как веб-браузер, посылая запросы удаленным веб-серверам, которые могут находиться в вашей сети или в Интернете.

Чтобы выполнить запрос, сначала нужно установить соединение с сетью, как в случае с веб-сервером, описанном в предыдущем разделе, но вместо объекта EthernetServer создать объект EthernetClient:

EthernetClient client;

Больше с объектом клиента ничего не требуется делать до отправки веб-запроса. Чтобы отправить веб-запрос, следует выполнить следующие действия:

if (client.connect("api.openweathermap.org", 80))

{

client.println("GET /data/2.5/weather?q=Manchester,uk HTTP/1.0");

client.println();

while (client.connected())

{

while (client.available())

{

Serial.write(client.read());

}

}

client.stop();

}

Функция connect вернет true, если соединение с веб-сервером было успешно установлено. Две команды client.println посылают веб-серверу запрос на получение желаемой страницы. Затем два вложенных цикла while читают данные, пока клиент остается подключенным к веб-серверу и продолжают поступать данные.

Может показаться заманчивым объединить два цикла while в один с условием client.available() && client.connected(), но такое объединение — далеко не то же самое, что два отдельных цикла, так как данные могут поступать от веб-сервера фрагментами из-за низкой скорости сети или по другим причинам. Внешний цикл поддерживает соединение открытым, а внутренний извлекает данные.

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


Примеры использования Ethernet

Далее демонстрируются два примера практического использования библиотеки Ethernet. Вместе они охватывают большинство вариантов сетевых взаимодействий, которые могут вам пригодиться при создании своих проектов на Arduino.


Физический веб-сервер

Первый пример иллюстрирует наиболее частый случай использования сетевых возможностей Arduino. Здесь плата выступает в роли веб-сервера. Подключившись к веб-серверу Arduino, посетители смогут не только читать аналоговые входы, но и изменять уровни на цифровых выходах, щелкая на кнопках в веб-странице (рис. 12.5).


Рис. 12.5. Интерфейс физического веб-сервера


Этот пример демонстрирует отличный способ связать плату Arduino со смартфоном или планшетным компьютером, так как для отправки запросов Arduino достаточно самого простого браузера. Скетч, реализующий этот пример (sketch_12_02_server), содержит 172 строки кода, поэтому он не будет приводиться здесь целиком, но я рекомендую открыть его в Arduino IDE и заглядывать в него в процессе чтения моих пояснений.

Первая часть скетча содержит код, стандартный для любого скетча, реализующего сетевые взаимодействия. Здесь выполняется импортирование библиотек и определение объектов EthernetServer и EthernetClient.

Далее определяются переменные, служащие разным целям:

const int numPins = 5;

int pins[] = {3, 4, 5, 6, 7};

int pinState[] = {0, 0, 0, 0, 0};

char line1[100];

char buffer[100];

Константа numPins определяет размер массивов pins и pinState. Массив pinState предназначен для хранения состояний цифровых выходов, HIGH или LOW. Функция setup настраивает все контакты, перечисленные в массиве pins, на работу в режиме цифровых выходов. Она также устанавливает соединение с сетью, как было показано в примерах ранее. Наконец, массивы символов line1 и buffer предназначены для хранения первой и последующих строк HTTP-запроса соответственно.

Далее приводится функция loop:

void loop()

{

client = server.available();

if (client)

{

if (client.connected())

{

readHeader();

if (! pageNameIs("/"))

{

client.stop();

return;

}

client.println(F("HTTP/1.1 200 OK"));

client.println(F("Content-Type: text/html"));

client.println();

sendBody();

client.stop();

}

}

}

Функция проверяет наличие любых запросов от браузеров, ожидающих обработки. Если был получен запрос и соединение с клиентом еще не разорвано, вызывается функция readHeader. Эту функцию вы найдете ближе к концу скетча. Функция readHeader читает содержимое заголовка запроса в буфер (line1) и пропускает остальные строки запроса. Это необходимо, чтобы получить имя страницы, запрошенной браузером, и любые дополнительные параметры запроса, если они имеются.

Обратите внимание: из-за большого объема текста, который посылается скетчем в монитор последовательного порта и сеть, я использовал функцию F, сохраняющую массивы символов во флеш-памяти (см. главу 6).

После чтения заголовка вызывается функция pageNameIs (также находится ближе к концу скетча), чтобы проверить совпадение имени запрошенной страницы с именем корневой страницы (/). Если была запрошена не корневая страница, такой запрос игнорируется. Это важно, потому что многие браузеры посылают веб-серверу дополнительные запросы с целью получить значок для веб-сайта. Эти запросы не следует путать с другими запросами к серверу.

Теперь нужно сгенерировать ответ с заголовком и некоторой разметкой HTML, которую смог бы отобразить браузер. Функция sendHeader генерирует ответ «OK», чтобы показать, что запрос браузера признан допустимым. Функция sendBody, представленная далее, организована намного сложнее:

void sendBody()

{

client.println(F(""));

sendAnalogReadings();

client.println(F("

Output Pins

"));

client.println(F("

"));

setValuesFromParams();

setPinStates();

sendHTMLforPins();

client.println(F(""));

client.println(F("

"));

client.println(F(""));

}

Она выводит простой макет HTML-страницы, опираясь на множество вспомогательных функций, которые были созданы, чтобы разбить код на более управляемые фрагменты. Первая из них — sendAnalogReadings:

void sendAnalogReadings()

{

client.println(F("

Analog Inputs

"));

client.println(F(""));

for (int i = 0; i < 5; i++)

{

int reading = analogRead(i);

client.print(F("

"));

}

client.println("

A")); client.print(i);

client.print(F("

")); client.print((float) reading / 205.0);

client.println(F(" V

");

}

Она выполняет обход всех аналоговых входов, читает их значения и выводит HTML-таблицу с прочитанными значениями в вольтах.

Возможно, вы обратили внимание на то, что sendBody вызывает также функции setValuesFromParams и setPinStates. Первая записывает в массив pinStates состояния HIGH или LOW цифровых выходов, извлекая их из параметров запроса с помощью функции valueOfParam:

int valueOfParam(char param)

{

for (int i = 0; i < strlen(line1); i++)

{

if (line1[i] == param && line1[i+1] == '=')

{

return (line1[i+2] — '0');

}

}

return 0;

}

Функция valueOfParam ожидает получения параметра запроса в виде единственной цифры. Как выглядят эти параметры, можно увидеть, если запустить пример, открыть страницу в браузере и щелкнуть на кнопке Update (Обновить). Адрес URL в адресной строке браузера изменится, и в нем появятся параметры, как показано далее:

192.168.1.10/?0=1&1=0&2=0&3=0&4=0

Список параметров начинается после символа ?. Параметры имеют вид X=Y и отделяются друг от друга символом &. Слева от знака = находится имя параметра (в данном случае цифры от 0 до 4), а справа — значения (в данном примере 1 означает «включено», а 0 — «выключено»). Для простоты параметры в этом примере могут иметь только односимвольные значения. Функция setPinStates устанавливает состояние цифровых выходов в соответствии со значениями элементов массива pinStates.

А теперь вернемся к функции sendBody. Вслед за таблицей со значениями аналоговых входов нужно послать разметку HTML с коллекцией раскрывающихся списков, соответствующих цифровым выходам. В каждом списке нужно выбрать пункт On (Включено) или Off (Выключено) в зависимости от текущего состояния цифрового выхода. Для этого нужно добавить текст «selected» в значение, соответствующее состоянию данного выхода в массиве pinStates.

Код разметки HTML для цифровых выходов заключается в форму, чтобы посетитель мог изменить значения в форме и, щелкнув на кнопке Update (Обновить), сгенерировать новый запрос к этой странице с соответствующими параметрами для установки цифровых выходов. А теперь посмотрим, как выглядит разметка HTML-страницы:

Analog Inputs

A00.58 V
A10.63 V
A20.60 V
A30.65 V
A40.60 V

Output Pins

Pin 3

Pin 4

Pin 5

Pin 6

Pin 7

value='Update'/>

Увидеть этот код можно, воспользовавшись функцией View Source (Исходный код страницы) в браузере.


Использование веб-службы JSON

Для иллюстрации возможности отправки веб-запросов из платы Arduino внешним веб-сайтам я воспользуюсь веб-службой, возвращающей данные о погоде в определенном географическом пункте. Плата будет выводить краткое описание погоды в монитор последовательного порта (рис. 12.6). Описываемый скетч посылает запрос один раз в момент запуска, но его нетрудно изменить, чтобы он запрашивал погоду каждый час и выводил результаты на двухстрочный жидкокристаллический дисплей.


Рис. 12.6. Получение информации о погоде от веб-службы


Скетч для этого примера получился очень коротким, всего 45 строк кода (sketch_12_03_web_request). Наибольший интерес для нас представляет функция hitWebPage:

void hitWebPage()

{

if (client.connect("api.openweathermap.org", 80))

{

client.println("GET /data/2.5/weather?q=Manchester,uk HTTP/1.0");

client.println();

while (client.connected())

{

if (client.available())

{

client.findUntil("description\":\"", "\0");

String description = client.readStringUntil('\"');

Serial.println(description);

}

}

client.stop();

}

}

Прежде всего необходимо подключить клиента к порту 80 сервера. Если соединение благополучно установлено, серверу посылается заголовок запроса:

client.println("GET /data/2.5/weather?q=Manchester,uk HTTP/1.0");

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

Далее в цикле while инструкция if проверяет получение данных от сервера, пока соединение с ним не закрыто. Непосредственное чтение данных из потока помогает избежать необходимости сохранять все данные в памяти. Данные поступают в формате JSON:

{"coord":{"lon":-2.23743,"lat":53.480949},

"sys":{"country":"GB","sunrise":1371094771,

"sunset":1371155927},"weather":[{"id":520, "main":"Rain",

"description":"light intensity shower rain", "icon": "09d"}]

"humidity":87,"temp_min":283.15,"temp_max":285.93},

"wind":{"speed:5.1,"deg":270},"rain":{"1h":0.83},

"clouds":{"all":40},"dt":1371135000,"id":3643123,

"name":"Manchester","cod":200}

Функция hitWebPage с помощью функций findUntil и readStringUntil извлекает фрагмент текста, следующий за словом «description», с двоеточием и двойной кавычкой до следующей двойной кавычки.

Функция findUntil просто игнорирует все, пока не встретит указанную строку. Затем функция readStringUntil читает текст из потока, пока не встретит двойную кавычку.


Библиотека WiFi

Библиотека WiFi, как можно было ожидать, очень похожа на библиотеку Ethernet. Если в скетче заменить Ethernet на WiFi, EthernetServer на WiFiServer и EthernetClient на WiFiClient, остальной код останется почти неизменным.


Создание соединения

Главное отличие библиотеки WiFi от Ethernet заключается в подключении к сети. Прежде всего нужно импортировать библиотеку WiFi:

#include

#include

Чтобы установить соединение с сетью, следует вызвать команду WiFi.begin и передать ей имя беспроводной сети и пароль:

WiFi.begin("MY-NETWORK-NAME", "mypassword");

Пример в разделе «Пример использования WiFi» иллюстрирует еще одно отличие, о котором вы должны знать.


Особые функции в библиотеке WiFi

Библиотека WiFi включает несколько специальных функций. Они перечислены в табл. 12.1.

Полное описание библиотеки WiFi можно найти по адресу http://arduino.cc/en/Reference/WiFi [11].


Таблица 12.1. Специальные функции в библиотеке WiFi

Функция Описание
WiFi.config Позволяет установить статические IP-адреса платы, сервера имен (DNS) и шлюза
WiFi.SSID Возвращает строку идентификатора беспроводной сети SSID
WiFi.RSSI Возвращает значение мощности сигнала типа long
WiFi.encriptionType Возвращает числовой код, соответствующий методу шифрования
WiFi.scanNetworks Возвращает количество найденных сетей, но никакой дополнительной информации о них не возвращается
WiFi.macAddress Помещает MAC-адрес адаптера WiFi в шестибайтный массив, переданный как параметр

Пример использования WiFi

Для этого примера я изменил скетч sketch_12_02_server, адаптировав его для работы с платой расширения WiFi. Полный исходный код можно найти в скетче sketch_12_04_server_wifi. Я не буду повторять пример целиком, только отмечу отличия от оригинальной версии.

Чтобы подключиться к беспроводной точке доступа, нужно указать имя беспроводной сети и пароль:

char ssid[] = "My network name"; // имя сети (SSID)

char pass[] = "mypassword"; // пароль для доступа к сети

Также нужно изменить имена классов сервера и клиента, EthernetServer и EthernetClient, на WiFiServer и WiFiClient:

WiFiServer server(80);

WiFiClient client;

При определении объекта сервера все так же требуется указать номер порта 80.

Следующее ракзличие между двумя платами заключается в инициализации подключения. В данном случае должна использоваться команда

WiFi.begin(ssid, pass);

Прочий код остался почти без изменений. В функции loop вы увидите команду delay(1) перед остановкой клиента, она дает клиенту время завершить чтение до того, как соединение будет закрыто. В версии на основе библиотеки Ethernet такая задержка не нужна. Обратите также внимание на то, что кое-где я объединил несколько вызовов client.print в один, чтобы каждый вызов выводил более длинные строки. Этот прием увеличивает скорость взаимодействий, потому что плата WiFi крайне неэффективно обрабатывает короткие строки. Но не забывайте, что каждый отдельный вызов client.print или client.println не может обрабатывать строки длиннее 90 байт — они просто не будут отправлены.

Версия программы на основе библиотеки WiFi работает заметно медленнее версии на основе библиотеки Ethernet и тратит на загрузку около 45 секунд. Плата расширения WiFi поддерживает возможность изменения прошивки, и если в будущем разработчики Arduino повысят эффективность работы платы WiFi, вам определенно стоит подумать об обновлении прошивки в своей плате расширения. Инструкции по прошивке платы WiFi можно найти на странице http://arduino.cc/en/Main/ArduinoWiFiShield.


В заключение

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

В следующей главе вы познакомитесь с цифровой обработкой сигналов (Digital Signal Processing, DSP) с помощью Arduino.


Загрузка...