В уроке научимся передавать данные, используя протокол UDP. Создадим несколько программ, реализующих функции сервера и клиента с UDP протоколом.
Предыдущий урок Список уроков Следующий урок
Наконец я возвращаюсь к основной на данный момент теме уроков – обмен данными между устройствами в системе Ардуино.
Скорее всего, дальнейшая последовательность уроков на основную тему будет продолжать хаотично прерываться уроками по совершенно другим вопросам. Иного выхода не вижу. Время не течет в обратную сторону, нельзя переписать историю, переставить объекты прошлого. Постараюсь написать подряд несколько уроков об обмене данными, хотя уже накопились очень интересные другие темы.
Протокол UDP.
UDP – это простой протокол передачи данных без предварительной организации соединения абонентов. Протокол ориентирован на передачу датаграмм – отдельных небольших пакетов данных. Датаграммы передаются в одном направлении без проверки готовности приемного устройства и без подтверждения доставки.
Примерно так мы передаем в сотовых сетях короткие сообщения (SMS). Если еще отключить подтверждение о доставке, то совсем похоже получится.
В протоколе TCP:
- происходит соединение;
- затем передаются данные;
- данные проверяются с помощью контрольных кодов;
- если необходимо, данные автоматически повторяются;
- соединение разрывается.
Пользователю остается только передавать данные, все остальное делает библиотека реализации протокола.
В отличие от TCP, протокол UDP не гарантирует ничего. Он не гарантирует, что данные будут доставлены, что придут правильные данные, что пакеты будут доставляться в той же последовательности, как при передаче. Контрольным кодом защищен только заголовок UDP-пакета. Поэтому, единственное, что гарантирует протокол, это то, что данные не попадут по неправильному адресу.
Пользователю необходимо самому побеспокоиться о целостности информации, о повторной передачи данных, о склеивании пакетов и т.п.
Но зато передача UDP-данных происходит значительно быстрее, чем через TCP. Именно поэтому протокол UDP предпочитают многие приложения.
К тому же во многих задачах вполне допустимо искажение или потеря части информации. Например, при просмотре фильма лучше потерять несколько кадров, чем ждать, когда они будут заново переданы.
Мы выполняем задачи на микроконтроллерах невысокой производительности. Поэтому для нас особенно важно, что реализация протокола UDP требует меньше вычислительных ресурсов, чем TCP.
Лично я предпочитаю именно UDP. Все можно сделать оптимально своей задаче. Не надо ждать соединений, минимальная нагрузка на контроллер и т.п.
Я не буду рассказывать о формате данных протокола. Нам это не надо. Пакеты данных при передаче и приеме будет формировать библиотека. Но необходимо твердо понимать, что:
- UDP – протокол без установления предварительных соединений;
- его задача – доставлять отдельные пакеты данных (датаграммы) между IP адресами;
- в сети Ethernet максимальный размер датаграммы 1500 байт;
- протокол не гарантирует, что данные будут доставлены;
- данные могут быть искажены при передаче, средств определения такой ситуации нет;
- протокол не сообщит отправителю, доставлены ли данные и не повторит передачу пакета;
- пакеты данных могут быть доставлены не в той последовательности, как при передаче;
- у UDP протокола высокая скорость передачи данных, он требует для реализации минимальных вычислительных ресурсов.
Библиотека для реализации обмена по UDP-протоколу UIPEthernet.
В предыдущем уроке по теме обмена информацией я рассказывал о библиотеке UIPEthernet и мы использовали ее для реализации TCP протокола. Я даже создал небольшой справочник по функциям UIPEthernet.
Я повторю ссылки на справочную информацию по функциям библиотеки для реализации UDP протокола. Все остальное будет ясно на примерах.
Класс EthernetUDP – поддерживает UDP протокол. | ||
begin() | beginPacket() | stop() |
read() | endPacket() | remoteIP() |
peek() | parsePacket() | remotePort() |
write() | available() | flush() |
Я использую эту версию библиотеки UIPEthernet-2.0.6.
Общая постановка задачи.
Остальная часть урока будет построена аналогично уроку 64. Программы будут выполнять те же функции, только с использованием UDP.
Повторю формализованную задачу.
- У нас есть плата Ардуино с подключенным к ней модулем ENC28J60, т.е. сетевое Ардуино-устройство или локальный контроллер.
- Образована локальная сеть Ethernet из контроллера и компьютера. Попросту говоря, Ардуино-устройство через роутер подключено к компьютеру.
- Задача состоит в том, чтобы от компьютера предать данные Ардуино-устройству и считать данные из Ардуино в компьютер.
Все аппаратные средства, схемы, подключения совершенно такие же, как в уроке 64.
Для отладки и проверки будем использовать мои программы верхнего уровня и программу TCP/IP Builder 1.9
Мы уже пользовались ею в уроке 64.
UDP сервер.
При использовании TCP-протокола мы создавали программные объекты отдельно для сервера и клиента.
EthernetServer server(2000); // создаем сервер, порт 2000
EthernetClient client; // создаем клиента
В UDP-протоколе мы просто передаем данные с одного IP-адреса на другой. Поэтому программно клиент и сервер не выделяются. Часто такие программы называют UDP-приемник и передатчик.
Сервер или клиент определяются по признакам более высокого уровня: кто предоставляет услуги, кто постоянно включен, кто инициирует соединение. А с точки зрения программной реализации обмена клиент и сервер - равнозначные устройства. Вы это прочувствуете ниже.
- Первый вариант UDP-сервера выполняет следующие действия:
- Проверяет, пришел ли новый пакет от клиента.
- Выводит в последовательный порт сообщение Server address: с IP-адресом сервера.
- Передает принятые данные обратно клиенту.
Загрузить скетч можно по ссылке:
// UDP сервер, возвращает клиенту полученные данные,
// выводит их в последовательный порт
#include <SPI.h>
#include <UIPEthernet.h>
#define SERV_PORT 2000 // порт сервера
// определяем конфигурацию сети
byte mac[] = {0xAE, 0xB2, 0x26, 0xE4, 0x4A, 0x5C}; // MAC-адрес
byte ip[] = {192, 168, 1, 10}; // IP-адрес
char receivingBuffer[100]; // приемный буфер Udp-пакета
EthernetUDP udp; // создаем экземпляр Udp
void setup() {
Ethernet.begin(mac, ip); // инициализируем контроллер
udp.begin(SERV_PORT); // включаем прослушивание порта
Serial.begin(9600);
Serial.print("Server address:");
Serial.println(Ethernet.localIP()); // выводим IP-адрес контроллера
}
void loop() {
int size = udp.parsePacket(); // считываем размер принятого пакета
if (size) {
// есть пакет Udp, выводим информацию о пакете
Serial.print("Received packet from ");
IPAddress ipFrom = udp.remoteIP();
Serial.println(ipFrom);
Serial.print("Size ");
Serial.print(size);
Serial.print(", port ");
Serial.println(udp.remotePort());
// чтение Udp-пакета и передача в последовательный порт
udp.read(receivingBuffer, size);
receivingBuffer[size]=0;
Serial.println("-------------------");
Serial.println(receivingBuffer);
Serial.println();
// ответ клиенту
udp.beginPacket(udp.remoteIP(), udp.remotePort());
udp.write(receivingBuffer);
udp.endPacket();
}
}
В бесконечном цикле loop() проверяется, пришел ли новый пакет:
int size = udp.parsePacket(); // считываем размер принятого пакета
if (size) {
// есть пакет Udp
Если есть новый пакет, то выводится информация о нем в последовательный порт и данные считываются в буфер.
udp.read(receivingBuffer, size);
Затем данные выводятся в последовательный порт и передаются клиенту UDP-пакетом:
// ответ клиенту
udp.beginPacket(udp.remoteIP(), udp.remotePort());
udp.write(receivingBuffer);
udp.endPacket();
Для проверки надо загрузить скетч и открыть монитор последовательного порта. В окне монитора появится сообщение
Адрес сервера задается в строчке
byte ip[] = {192, 168, 1, 10}; // IP-адрес
Роль клиента будет выполнять компьютер.
Для проверки:
- Запустите программу TCP/IP Builder.
- В строке Local IP уже должен быть адрес компьютера. Надо добавить порт, для программы урока 2000. Хотя, после того, как я заменил роутер, программа перестала правильно определять адрес сетевой платы компьютера. Раньше был 192.168.1.2, а с новым роутером стал 192.168.1.100. Лучше посмотрите сетевой адрес вашего компьютера. Как это сделать написано в уроке 62.
- Выберите UDP.
- Нажмите кнопку Create Socket (Создать соккет).
- В строке IP укажите IP адрес локального контроллера и порт (те, что в плате Ардуино).
- Нажмите Connect (Подключиться).
- В результате на компьютере появится клиент.
Теперь можно посылать данные из окна Send data на сервер. Полученные от сервера данные будут появляться в окне Receive data.
Заметьте, что кнопка Connect (Соединение) для UDP не активна. В UDP-обмене не бывает соединений.
Я передал сообщение на сервер и получил его в ответ.
Оно же появилось в мониторе последовательного порта с данными об отправителе.
Для работы с UDP-устройствами я написал свою программу. Назвал ее UDP client, хотя она выполняет и роль сервера. Загрузить можно по ссылке:
Программа позволяет посылать UDP-пакеты по IP-адресу и принимать UDP-датаграммы.
- Поле IP адрес это IP-адрес по которому будет послано сообщение. (Для этой программы адрес сервера.)
- Удаленный порт – порт получателя. (Порт сервера).
- Локальный порт- порт приема сообщений. (Порт клиента.)
- IP-адрес получателя (клиента) формируется автоматически. Это сетевой адрес компьютера. Его можно посмотреть, нажав кнопку Конфигурация сети.
- Большое окно слева для приема сообщений (от сервера).
- Поле ниже для посылаемых сообщений (от клиента серверу).
- Кнопка Послать собственно инициирует посылку данных (на сервер).
- Кнопкой конфигурация сети можно узнать сетевой адрес компьютера (клиента).
Заметьте, что локальный порт можно задавать любой. Программа сервера выведет его в мониторе порта и перешлет клиенту обратно данные именно на этот порт. Удаленный порт может быть только тот, который задан в программе сервера (2000).
Проверка такая же: послать на сервер сообщение и получить ответ.
Если используется роутер с WiFi, то можно проверить работу сервера с мобильным Андроид устройством. Я установил программу UDP terminal, она первая попалась в Google Play Market.
Задал адреса и порты.
Послал сообщение на сервер. Получил ответ.
Сообщение с параметрами отправителя появилось и в мониторе последовательного порта.
Давайте, по аналогии с уроком 64 создадим UDP-сервер, который:
- Под управлением клиента включает и выключает светодиод, подключенный к выводу 2 платы Ардуино;
- В качестве ответа клиенту передает время, прошедшее с начала запуска программы Ардуино (включения платы).
Вот скетч такого сервера:
// UDP сервер, управляет светодиодом, передает клиенту время
#include <SPI.h>
#include <UIPEthernet.h>
#define SERV_PORT 2000 // порт сервера
// определяем конфигурацию сети
byte mac[] = {0xAE, 0xB2, 0x26, 0xE4, 0x4A, 0x5C}; // MAC-адрес
byte ip[] = {192, 168, 1, 10}; // IP-адрес
char receivingBuffer[100]; // приемный буфер Udp-пакета
EthernetUDP udp; // создаем экземпляр Udp
void setup() {
Ethernet.begin(mac, ip); // инициализируем контроллер
udp.begin(SERV_PORT); // включаем прослушивание порта
pinMode(2, OUTPUT); // вывод светодиода
}
void loop() {
int size = udp.parsePacket(); // считываем размер принятого пакета
if (size) {
// есть новый пакет
udp.read(receivingBuffer, size); // чтение Udp-пакета
// управление светодиодом
if(receivingBuffer[0] == '0') digitalWrite(2, LOW); // если пришел 0, гасим светодиод
if(receivingBuffer[0] == '1') digitalWrite(2, HIGH); // если пришла 1, зажигаем светодиод
receivingBuffer[0]=0;
// посылка клиенту в ответ времени
udp.beginPacket(udp.remoteIP(), udp.remotePort());
udp.println(millis());
udp.endPacket();
}
}
Логика работы программы:
- Если от клиента приходит символ 0 светодиод гасится.
- Если поступает символ 1 светодиод зажигается.
- При приходе от клиента любого символа в ответ передается время работы программы.
Проверим его работу с помощью программы UDP client.
При посылке 1 светодиод загорается, при 0 – гасится.
В качестве примера я написал специализированную программу клиента для управления этим сервером.
Светодиод гасится и зажигается ”птичкой”, время выводится в ”человеческом” виде.
Программу можно загрузить по ссылке:
DHCP сервер.
В предыдущих программах урока мы задавали IP адрес сервера явно. Применяли статические IP адреса. Все современные сети имеют возможность автоматического распределения адресов. Сервер, использующий этот способ назначения IP адресов, называется DHCP сервером. Об этом написано в уроке 62. Такой сервер мы создавали в уроке 64.
Для получения динамического IP-адреса достаточно при инициализации контроллера указать только один аргумент – mac адрес.
Чтобы первую программу урока перевести на использование динамических IP-адресов необходимо заменить в ней блок setup() на такой:
void setup() {
Serial.begin(9600);
Serial.println("Getting IP address using DHCP");
if (Ethernet.begin(mac) == 0) {
Serial.println("Failed to configure using DHCP");
while(true) ; // зависаем по ошибке
}
Serial.print("Server address:");
Serial.println(Ethernet.localIP()); // выводим IP-адрес контроллера
udp.begin(SERV_PORT); // включаем прослушивание порта
}
Полностью программу с DHCP UDP-сервером можно загрузить по ссылке:
Проверка отличается от предыдущей методики только тем, что до запуска сервера мы не знаем какой адрес указать при обращении к нему. Неизвестно какой адрес будет ему присвоен.
Загружаем программу в плату Ардуино. В мониторе последовательного порта появляется сообщение.
Указанный адрес сервера заносим в программу клиента и проверяем, как проходят сообщения.
У меня все работает.
UDP клиент.
Выше я писал, что с программной точки зрения в UDP-протоколе невозможно выделить сервер и клиент. Скорее мы разработаем приемо-передатчик UDP-сообщений. Он будет:
- Данные, которые передаются монитором последовательного порта посылать серверу.
- Выводить в последовательный порт сообщение о передаче с параметрами получателя.
- Данные, поступающие от сервера выводить в монитор последовательного порта с параметрами передатчика.
Попросту говоря, сообщения с монитора последовательного порта он будет передавать серверу, а сообщения с сервера выводить в монитор порта.
Вот ссылка для загрузки скетча:
В цикле loop() два программных блока.
Первый блок:
- принимает данные с UART в буфер;
- ждет пока встретится символ с кодом 10 (перевод строки);
- выводит параметры приемника в последовательный порт.
// собираем данные из UART в пакет
if( Serial.available() > 0 ) {
transmitBuffer[ii] = Serial.read();
if( transmitBuffer[ii] == 10 ) {
transmitBuffer[ii+1]= 0;
ii=0;
// передача UDP пакета
udp.beginPacket(ipServ, SERV_PORT);
udp.write(transmitBuffer);
udp.endPacket();
// сообщение в последовательный порт
Serial.print("UDP-packet transmission to ");
Serial.print(udp.remoteIP());
Serial.print(" Port: ");
Serial.println(udp.remotePort());
}
else {
ii++;
if(ii >= 100) ii=0;
}
}
Второй блок:
- проверяет есть ли новый пакет UDP-данных;
- выводит в последовательный порт сообщение с параметрами передатчика.
// проверка есть ли новые UDP-пакеты
int size = udp.parsePacket(); // считываем размер принятого пакета
if (size) {
// есть пакет Udp, выводим информацию о пакете
Serial.print("Received packet from ");
IPAddress ipFrom = udp.remoteIP();
Serial.println(ipFrom);
Serial.print("Size ");
Serial.print(size);
Serial.print(", port ");
Serial.println(udp.remotePort());
// чтение Udp-пакета и передача в последовательный порт
udp.read(receivingBuffer, size);
receivingBuffer[size]=0;
Serial.println("-------------------");
Serial.println(receivingBuffer);
Serial.println();
}
Проверка клиента с помощью разных программ.
Загружаем sketch_69_4 в плату Ардуино.
Запускаем мою программу UDP client.
Набираем сообщение в мониторе последовательного порта, нажимаем Отправить.
Видим в мониторе сообщение о посылке.
В программе UDP client появляется сообщение.
Набираем сообщение в UDP client. Нажимаем кнопку Послать.
Видим, что сообщение появилось в окне монитора.
Обмениваемся нескольким сообщениями.
Все работает.
Проверяем с помощью TCP/IP Builder.
Теперь мобильное Андроид-устройство и программа UDP terminal.
Это все. Не знаю кому как, а мне UDP-протокол для управления небольшим системами кажется намного предпочтительнее TCP.
В следующем уроке будем создавать веб-сервер.
Не кликабельна кнопка connect в TCP/IP Builder, все данные ввел по сути верно, с чем может быть связано? 🙁
В поле Local IP ввести айпи адрес своего ПК в локальной сети (узнать можно написав ipconfig в консоль виндовс).
В поле IP ввести айпи адрес вашей ардуино. Если вы все сделали правильно то он будет присвоен вашему устройству. Перевести положение переключателя на UDP и создать сокет. У меня все заработало.