Общие сведения:
В этом уроке мы научим несколько Piranha ESP32 (далее - плата) обмениваться данными в одной локальной сети. Данные будут передаваться по протоколу UDP. Одна из плат будет ведущей и будет отвечать за синхронизацию данных в остальных платах. На ней будет запущен UDP сервер, остальные будут клиентами. Ведомые будут посылать ведущей плате только свои данные, а ведущая будет отвечать, передавая ведомым весь массив данных о всех платах в сети. Таким образом в памяти каждой ESP32 будет копия данных других плат проекта.
Проект состоит из двух скетчей, первый нужно загрузить в одну из плат, а второй во все остальные с изменением их порядкового номера в начале скетча. Ведущая плата должна быть с порядковым номером 0
, остальные начинаться с 1
(1
, 2
, и т.д.).
Если в проекте участвует более трёх плат, то необходимо указать их количество в макросе NBOARDS
.
После загрузки в платы, каждая плата будет выводить в последовательный порт весь текущий массив данных:
Скетчи проекта рассчитаны на работу в уже существующей сети, данные которой необходимо указать в начале скетчей (константы SSID
и PASSWORD
). Если необходимо создать новую сеть, где одна из плат будет являться точкой доступа, в конце статьи есть дополнение о том как это сделать.
Фомат массива
При передаче и получении информации используются байты. Одним байтом возможно передать число от 0 до 255, что соответствует типу данных byte
и uint8_t
, а так же от -128 до 127, что соответствует типам char
и int8_t
. При передаче бóльших чисел используется трюк с преобразованием типов адресов (указателей). Например, если это массив с типом данных int
(что соответствует четырём байтам для esp32), то каждый элемент массива отсылается побайтово. Таким образом число типа int i = -1
будет передано как четыре байта со значениями 0xff
. Далее принимающая сторона преобразует байты обратно в int
. Этот приём работает только при приёме/передаче данных представляющих непрерывные структуры в памяти, таких как массивы. Со связанными списками это приём не работает. При таком подходе очень легко выйти за границы массива, поэтому при передаче данных передаётся и их размер в байтах полученный при помощи оператора sizeof
. Принимающая сторона вычисляет количество элементов приводя количество полученных байтов к размеру типа ожидаемых данных, так же при помощи оператора sizeof
.
Структура данных
В данном примере используется массив структур Си++. Структуру можно рассматривать как набор переменных и/или констант. Она задаётся при помощи директивы struct
следующим образом:
struct [НАЗВАНИЕ] { участник структуры 1; участник структуры 2; .............. участник структуры N; };
или
// Создаём прототип структуры struct MyStruct { // первый участник - целое число int number; // второй участник - вещественное число float f; };
Далее новую структуру или массив структур можно создать указав её как тип:
MyStruct newStruct;
Обращение к участникам структуры происходит так же как и к методам экземпляра класса Си++: через точку, если это сама структура или через дефис и знак "больше", если это указатель на структуру:
// Присваеваем участнику структуры значение newStruct.number = 42; // Выводим значение Serial.println(newStruct.number); // Создаём указатель на структуру и присваем ему значение // указателя на структуру, созданную ранее MyStruct* pointerToStruct = &newStruct; // Выводим значение через указатель Serial.pritln(pointerToStruct->number);
Передача данных
При передаче данных по UDP мы используем указатель на массив структур:
// Отправляем данные этой платы побайтово udp.broadcastTo((uint8_t*)&data[NUM], sizeof(data[0]), PORT);
Разберёмся с этой строчкой по порядку. Это строчка из скетча ведомых плат. udp.broadcastTo()
- это метод объекта класса AsyncUDP
, который отправляет данные побайтово на указанный в методе udp.connect()
ip-адрес. Метод принимает в качестве первого параметра указатель на массив байтов uint8_t
. В данном случае мы передаём адрес элемента массива структур этой платы, который мы определили как multidata
. data[NUM]
- это сам элемент, &data[NUM]
- его адрес в памяти, а (uint8_t*)
- преобразование типа указателя (с multidata
на uint8_t
). Второй параметр - количество байтов, которые нужно отправить по порядку, начиная с байта, который храниться по ранее переданному указателю. Здесь мы указываем размер одного элемента массива sizeof(data[0])
. Так же мы могли бы указать размер самой структуры sizeof(multidata)
. Последний параметр - UDP порт.
Из этого следует вывод, что таким образом мы можем отправлять данные, которые хранятся непрерывно в памяти. Если один из участников структуры - указатель на данные, то будет отправлен указатель, а не сами данные.
При передаче и приёме данных используйте одинаковые типы данных! В примерах используется проверка размера данных, но всё равно что-то может пойти не так. Если принимающая сторона будет ожидать int
, а передающая сторона передаст char
, то может произойти чтение или запись в ненадлежащую область памяти, от чего ESP32 может зависнуть, перезагружаться или работать непредсказуемо. В данных примерах структура данных должна быть одинаковой для всех скетчей. Для удобства её можно оформить как отдельный .h
файл и подключать при помощи директивы #include
Видео:
редактируется ...
Нам понадобится:
Подключение:
Скетчи проекта
Скетч для ведущей ESP32 (Сервер)
В этом скетче создаётся сервер UDP, который "слушает" определённый в скетче порт. Когда на ip адрес этой платы приходит пакет UDP с заданным номером порта, выполняется функция parsePacket
. В конце функции отправителю пакета отправляется весь массив структур с текущими значениями.
// Подключаем библиотеки #include "WiFi.h" #include "AsyncUDP.h" #include "ESPmDNS.h" // Определяем количество плат Piranha ESP32 в сети #define NBOARDS 3 // Определяем номер этой платы const unsigned int NUM = 0; // Определяем имя платы в сети const char* master_host = "esp32master"; // Определяем структуру данных для обмена /* У всех плат проекта должна быть одинаковая структура! Для удобства её можно записать в отдельный .h файл */ struct multidata { /* Номер платы (необходим для быстрого доступа по индексу в массиве структур) */ uint8_t num; // Текущие millis платы unsigned long millis; /* В структуру можно добавлять элементы например, ip-адрес текущей платы:*/ IPAddress boardIP; // или показания датчика: uint16_t sensor; }; // Массив структур для обмена multidata data[NBOARDS] {0}; // Определяем название и пароль точки доступа const char* SSID = "ssid Вашей точки доступа WiFi"; const char* PASSWORD = "пароль Вашей точки доступа WiFi"; // Определяем порт const uint16_t PORT = 49152; // Создаём объект UDP соединения AsyncUDP udp; // Определяем callback функцию обработки пакета void parsePacket(AsyncUDPPacket packet) { // Преобразуем указатель на данные к указателю на структуру const multidata* tmp = (multidata*)packet.data(); // Вычисляем размер данных (ожидаем получить размер в один элемент структур) const size_t len = packet.length() / sizeof(data[0]); // Если указатель на данные не равен нулю и размер данных больше нуля... if (tmp != nullptr && len > 0) { // Записываем порядковый номер платы data[tmp->num].num = tmp->num; // Записываем текущие millis платы data[tmp->num].millis = tmp->millis; // Записываем IP адрес data[tmp->num].boardIP = tmp->boardIP; // Записываем показания датчика data[tmp->num].sensor = tmp->sensor; // Отправляем данные всех плат побайтово packet.write((uint8_t*)data, sizeof(data)); } } void setup() { // Инициируем последовательный порт Serial.begin(115200); // Инициируем WiFi WiFi.begin(SSID, PASSWORD); // Ждём подключения WiFi Serial.print("Подключаем к WiFi"); while (WiFi.status() != WL_CONNECTED) { Serial.print("."); delay(100); } Serial.println(); // Записываем адрес текущей платы в элемент структуры data[NUM].boardIP = WiFi.localIP(); if (!MDNS.begin(master_host)) { Serial.println(data[NUM].boardIP); } // Инициируем сервер if (udp.listen(PORT)) { // вызываем callback функцию при получении пакета udp.onPacket(parsePacket); } Serial.println("Сервер запущен."); } void loop() { // Записываем текущие millis в элемент массива, соответствующий данной плате data[NUM].millis = millis(); // Записываем показания датчика (для демонстрации это просто millis / 10) data[NUM].sensor = millis() / 10; // Выводим значения элементов в последовательный порт for (size_t i = 0; i < NBOARDS; i++) { Serial.print("IP адрес платы: "); Serial.print(data[i].boardIP); Serial.print(", порядковый номер: "); Serial.print(data[i].num); Serial.print(", текущие millis: "); Serial.print(data[i].millis); Serial.print(", значение датчика: "); Serial.print(data[i].sensor); Serial.print("; "); Serial.println(); } Serial.println("----------------------------"); delay(1000); }
Скетч для ведомых ESP32 (клиенты)
В этом скетче раз в секунду отправляются данные текущей платы серверу, к которому плата подключена по ip-адресу. Ip-адрес найден по имени при помощи Multicast DNS. При получении ответа от сервера вызывается функция parsePacket()
которая заполняет новыми данными массив структур всех плат, кроме текущей.
// Подключаем библиотеки #include "WiFi.h" #include "AsyncUDP.h" #include "ESPmDNS.h" // Определяем количество плат #define NBOARDS 3 // Определяем номер этой платы const unsigned int NUM = 2; // Определяем структуру данных для обмена /* У всех плат проекта должна быть одинаковая структура! Для удобства её можно записать в отдельный .h файл */ struct multidata { /* Номер платы (необходим для быстрого доступа по индексу в массиве структур) */ uint8_t num; // Текущие millis платы unsigned long millis; /* В структуру можно добавлять элементы например, ip-адрес текущей платы:*/ IPAddress boardIP; // или показания датчика: uint16_t sensor; }; // Массив структур для обмена multidata data[NBOARDS] {0}; /* Определяем имена для mDNS */ // для ведущей платы const char* master_host = "esp32master"; // приставка имени ведомой платы const char* slave_host = "esp32slave"; // Определяем название и пароль точки доступа const char* SSID = "ssid Вашей точки доступа WiFi"; const char* PASSWORD = "пароль Вашей точки доступа WiFi"; // Определяем порт const uint16_t PORT = 49152; // Создаём объект UDP соединения AsyncUDP udp; // Определяем callback функцию обработки пакета void parsePacket(AsyncUDPPacket packet) { const multidata* tmp = (multidata*)packet.data(); // Вычисляем размер данных const size_t len = packet.length() / sizeof(data[0]); // Если адрес данных не равен нулю и размер данных больше нуля... if (tmp != nullptr && len > 0) { // Проходим по элементам массива for (size_t i = 0; i < len; i++) { // Если это не ESP на которой выполняется этот скетч if (i != NUM) { // Обновляем данные массива структур data[i].num = tmp[i].num; data[i].millis = tmp[i].millis; data[i].boardIP = tmp[i].boardIP; data[i].sensor = tmp[i].sensor; } } } } void setup() { // Добавляем номер этой платы в массив структур data[NUM].num = NUM; // Инициируем последовательный порт Serial.begin(115200); // Инициируем WiFi WiFi.begin(SSID, PASSWORD); // Ждём подключения WiFi Serial.print("Подключаем к WiFi"); while (WiFi.status() != WL_CONNECTED) { Serial.print("."); delay(100); } Serial.println(); // Записываем адрес текущей платы в элемент структуры data[NUM].boardIP = WiFi.localIP(); // Инициируем mDNS с именем "esp32slave" + номер платы if (!MDNS.begin(String(slave_host + NUM).c_str())) { Serial.println("не получилось инициировать mDNS"); } // Узнаём IP адрес платы с UDP сервером IPAddress server = MDNS.queryHost(master_host); // Если удалось подключиться по UDP if (udp.connect(server, PORT)) { Serial.println("UDP подключён"); // вызываем callback функцию при получении пакета udp.onPacket(parsePacket); } } void loop() { // Записываем текущие millis в элемент массива, соответствующий данной плате data[NUM].millis = millis(); // Записываем показания датчика (для демонстрации это просто millis / 10) data[NUM].sensor = millis() / 10; // Отправляем данные этой платы побайтово udp.broadcastTo((uint8_t*)&data[NUM], sizeof(data[0]), PORT); // Выводим значения элементов в последовательный порт for (size_t i = 0; i < NBOARDS; i++) { Serial.print("IP адрес платы: "); Serial.print(data[i].boardIP); Serial.print(", порядковый номер: "); Serial.print(data[i].num); Serial.print(", текущие millis: "); Serial.print(data[i].millis); Serial.print(", значение датчика: "); Serial.print(data[i].sensor); Serial.print("; "); Serial.println(); } Serial.println("----------------------------"); delay(1000); }
Дополнение
Соединение Piranha ESP32 напрямую (без WiFi роутера)
Для соединения нескольких Piranha ESP32 напрямую (без WiFi роутера) необходимо изменить скетч только ESP32, которая работает в качестве сервера и в скетчах клиентов указать название и пароль для WiFi, которые указаны в скетче сервера.
В скетче ведущей платы (сервера) необходимо подключить библиотеку WiFiAP
и заменить блок инициализации WiFi:
#include "WiFiAP.h" const char* SSID = "name"; const char* PASSWORD = "password"; void setup() { ... WiFi.softAP(SSID, PASSWORD); ... }
при такой инициализации получить ip-адрес платы можно при помощий WiFi.softAPIP()
.
Полный скетч сервера для подключения напрямую
// Подключаем библиотеки #include "WiFi.h" #include "AsyncUDP.h" #include "ESPmDNS.h" #include "WiFiAP.h" // Определяем количество плат Piranha ESP32 в сети #define NBOARDS 3 // Определяем номер этой платы const unsigned int NUM = 0; // Определяем имя платы в сети const char* master_host = "esp32master"; // Определяем структуру данных для обмена /* У всех плат проекта должна быть одинаковая структура! Для удобства её можно записать в отдельный .h файл */ struct multidata { /* Номер платы (необходим для быстрого доступа по индексу в массиве структур) */ uint8_t num; // Текущие millis платы unsigned long millis; /* В структуру можно добавлять элементы например, ip-адрес текущей платы:*/ IPAddress boardIP; // или показания датчика: uint16_t sensor; }; // Массив структур для обмена multidata data[NBOARDS] {0}; // Определяем название и пароль точки доступа const char* SSID = "esp32asAP"; const char* PASSWORD = "mysecurepassword"; // Определяем порт const uint16_t PORT = 49152; // Создаём объект UDP соединения AsyncUDP udp; // Определяем callback функцию обработки пакета void parsePacket(AsyncUDPPacket packet) { // Преобразуем указатель на данные к указателю на структуру const multidata* tmp = (multidata*)packet.data(); // Вычисляем размер данных (ожидаем получить размер в один элемент структур) const size_t len = packet.length() / sizeof(data[0]); // Если указатель на данные не равен нулю и размер данных больше нуля... if (tmp != nullptr && len > 0) { // Записываем порядковый номер платы data[tmp->num].num = tmp->num; // Записываем текущие millis платы data[tmp->num].millis = tmp->millis; // Записываем IP адрес data[tmp->num].boardIP = tmp->boardIP; // Записываем показания датчика data[tmp->num].sensor = tmp->sensor; // Отправляем данные всех плат побайтово packet.write((uint8_t*)data, sizeof(data)); } } void setup() { // Инициируем последовательный порт Serial.begin(115200); // Инициируем WiFi WiFi.softAP(SSID, PASSWORD); // Записываем адрес текущей платы в элемент структуры data[NUM].boardIP = WiFi.softAPIP(); if (!MDNS.begin(master_host)) { Serial.println(data[NUM].boardIP); } // Инициируем сервер if (udp.listen(PORT)) { // вызываем callback функцию при получении пакета udp.onPacket(parsePacket); } Serial.println("Сервер запущен."); } void loop() { // Записываем текущие millis в элемент массива, соответствующий данной плате data[NUM].millis = millis(); // Записываем показания датчика (для демонстрации это просто millis / 10) data[NUM].sensor = millis() / 10; // Выводим значения элементов в последовательный порт for (size_t i = 0; i < NBOARDS; i++) { Serial.print("IP адрес платы: "); Serial.print(data[i].boardIP); Serial.print(", порядковый номер: "); Serial.print(data[i].num); Serial.print(", текущие millis: "); Serial.print(data[i].millis); Serial.print(", значение датчика: "); Serial.print(data[i].sensor); Serial.print("; "); Serial.println(); } Serial.println("----------------------------"); delay(1000); }
Обсуждение