Общие сведения:
В этом уроке мы научим несколько 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);
}

Обсуждение