КОРЗИНА
магазина
8 (499) 500-14-56 | ПН. - ПТ. 12:00-18:00
ЛЕСНОРЯДСКИЙ ПЕРЕУЛОК, 18С2, БЦ "ДМ-ПРЕСС"

Обмениваемся массивом данных между тремя и более Piranha ESP32

Общие сведения:

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

Ссылки




Обсуждение

Гарантии и возврат Используя сайт Вы соглашаетесь с условями