В этом руководстве мы рассмотрим, как отправлять данные с ESP32 на несколько плат ESP32 или ESP8266 (конфигурация «один ко многим») с помощью протокола ESP-NOW. Платы будем программировать используя среду разработки Arduino.
Обзор проекта
Кратко:
- Одна плата ESP32 в роли отправителя;
- Несколько плат ESP32 или ESP8266 в роли приемников. В данном руководстве данные отправляются одновременно на два ESP32 и на одну ESP8266, но вы, конечно, можете добавить и больше. Необходимо немного изменить код приемника в зависимости от того, какая у вас плата – ESP32 или ESP8266;
- Плата-отправитель получает подтверждение, если сообщения успешно доставлены, то есть, мы знаем, какие платы получили данные, а какие нет;
- В данном руководстве мы будем обмениваться случайными числами между досками. Вы можете изменить код, чтобы отправлять команды или показания датчиков.
В этом руководстве рассматриваются две задачи:
- отправка одного и того же сообщения на все платы;
- отправка разных сообщений на каждую плату.
Что нам нужно для проекта?
Мы запрограммируем платы ESP32 / ESP8266 в среде разработки Arduino, поэтому перед тем, как продолжить, убедитесь, что эти платы установлены в вашей Arduino IDE.
Вам также понадобится несколько плат ESP32 и/или ESP8266.
Получение MAC-адреса платы
Чтобы отправлять сообщения через ESP-NOW, вам нужно знать MAC-адрес приемных плат. Каждая плата имеет уникальный MAC-адрес
Загрузите следующий код на каждую плату-приемник, чтобы получить их MAC-адреса.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
#ifdef ESP32 #include <WiFi.h> #else #include <ESP8266WiFi.h> #endif void setup(){ Serial.begin(115200); Serial.println(); Serial.print("ESP Board MAC Address: "); Serial.println(WiFi.macAddress()); } void loop(){ } |
После загрузки кода нажмите кнопку RST / EN, и MAC-адрес должен отобразиться на мониторе порта.
Стоит записать MAC-адрес платы на бирке, чтобы было удобнее идентифицировать каждую плату.
Код для платы-отправителя (ESP-NOW)
Следующий код отправляет данные нескольким (в нашем случае, трем) платам ESP32 по протоколу ESP-NOW. В коде нужно указать MAC-адрес ваших плат-приемников, а также добавить или удалить строки кода в зависимости от количества плат-приемников.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 |
/********* Rui Santos Complete project details at https://RandomNerdTutorials.com/esp-now-one-to-many-esp32-esp8266/ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files. The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. *********/ #include "esp_now.h" #include "WiFi.h" // ЗАМЕНИТЕ MAC-АДРЕСАМИ ПЛАТ-ПРИЕМНИКОВ uint8_t broadcastAddress1[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; uint8_t broadcastAddress2[] = {0xFF, , , , , }; uint8_t broadcastAddress3[] = {0xFF, , , , , }; typedef struct test_struct { int x; int y; } test_struct; test_struct test; // сообщение, если данные отправлены void OnDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) { char macStr[18]; Serial.print("Packet to: "); // Записывает МАС-адрес в строку snprintf(macStr, sizeof(macStr), "%02x:%02x:%02x:%02x:%02x:%02x", mac_addr[0], mac_addr[1], mac_addr[2], mac_addr[3], mac_addr[4], mac_addr[5]); Serial.print(macStr); Serial.print(" send status:\t"); Serial.println(status == ESP_NOW_SEND_SUCCESS ? "Delivery Success" : "Delivery Fail"); } void setup() { Serial.begin(115200); WiFi.mode(WIFI_STA); if (esp_now_init() != ESP_OK) { Serial.println("Error initializing ESP-NOW"); return; } esp_now_register_send_cb(OnDataSent); // регистрируем платы в сети esp_now_peer_info_t peerInfo; peerInfo.channel = 0; peerInfo.encrypt = false; // первая плата memcpy(peerInfo.peer_addr, broadcastAddress1, 6); if (esp_now_add_peer(&peerInfo) != ESP_OK){ Serial.println("Failed to add peer"); return; } // вторая плата memcpy(peerInfo.peer_addr, broadcastAddress2, 6); if (esp_now_add_peer(&peerInfo) != ESP_OK){ Serial.println("Failed to add peer"); return; } /// третья плата memcpy(peerInfo.peer_addr, broadcastAddress3, 6); if (esp_now_add_peer(&peerInfo) != ESP_OK){ Serial.println("Failed to add peer"); return; } } void loop() { test.x = random(0,20); test.y = random(0,20); esp_err_t result = esp_now_send(0, (uint8_t *) &test, sizeof(test_struct)); if (result == ESP_OK) { Serial.println("Sent with success"); } else { Serial.println("Error sending the data"); } delay(2000); } |
Как работает код?
Подключаем библиотеки esp_now.h и WiFi.h.
1 2 |
#include "esp_now.h" #include "WiFi.h" |
Записываем МАС-адреса получателей. В нашем случае – три получателя.
1 2 3 |
uint8_t broadcastAddress1[] = {0x3C, 0x71, 0xBF, 0xC3, 0xBF, 0xB0}; uint8_t broadcastAddress2[] = {0x24, 0x0A, 0xC4, 0xAE, 0xAE, 0x44}; uint8_t broadcastAddress3[] = {0x80, 0x7D, 0x3A, 0x58, 0xB4, 0xB0}; |
Затем создаем структуру, в которой находятся наши данные. Мы ее назвали test_struct и она содержит две целочисленные переменные.
1 2 3 4 |
typedef struct test_struct { int x; int y; } |
Создаем новую переменную в test_struct и называем ее test, в ней будут храниться значения переменных.
1 |
test_struct test; |
Определяем функцию OnDataSent().Она будет вызываться при отправке сообщения. В данном случае функция показывает отправлено ли сообщение и на какой МАС-адрес. Таким образом, мы знаем какая плата получила сообщение.
1 2 3 4 5 6 7 8 9 10 |
void OnDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) { char macStr[18]; Serial.print("Packet from: "); // записывает МАС-адрес в строку snprintf(macStr, sizeof(macStr), "%02x:%02x:%02x:%02x:%02x:%02x", mac_addr[0], mac_addr[1], mac_addr[2], mac_addr[3], mac_addr[4], mac_addr[5]); Serial.print(macStr); Serial.print(" send status:\t"); Serial.println(status == ESP_NOW_SEND_SUCCESS ? "Delivery Success" : "Delivery Fail"); } |
В функции setup(), запускаем монитор порта для дебага:
1 |
Serial.begin(115200); |
Выставляем режим клиента WiFi:
1 |
WiFi.mode(WIFI_STA); |
Запускаем протокол ESP-NOW:
1 2 3 4 |
if (esp_now_init() != ESP_OK) { Serial.println("Error initializing ESP-NOW"); return; } |
После инициализации протокола определяем действия при отправке. В данном случае указываем ранее созданную функцию OnDataSent().
1 |
esp_now_register_send_cb(OnDataSent); |
Регистрируем платы в сети.
Для отправки данных нужно указать адреса всех плат в сети:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// регистрируем платы esp_now_peer_info_t peerInfo; peerInfo.channel = 0; peerInfo.encrypt = false; // первая плата memcpy(peerInfo.peer_addr, broadcastAddress1, 6); if (esp_now_add_peer(&peerInfo) != ESP_OK){ Serial.println("Failed to add peer"); return; } // вторая плата memcpy(peerInfo.peer_addr, broadcastAddress2, 6); if (esp_now_add_peer(&peerInfo) != ESP_OK){ Serial.println("Failed to add peer"); return; } /// третья плата memcpy(peerInfo.peer_addr, broadcastAddress3, 6); if (esp_now_add_peer(&peerInfo) != ESP_OK){ Serial.println("Failed to add peer"); return; } |
Если у вас больше плат – скопируйте строки выше и укажите ее МАС-адрес:
1 2 3 4 5 |
memcpy(peerInfo.peer_addr, broadcastAddress, 6); if (esp_now_add_peer(&peerInfo) != ESP_OK){ Serial.println("Failed to add peer"); return; } |
В цикле loop(), каждые две секунды отправляем сообщение (время задержки можно изменить).
Присваиваем значение каждой переменной:
1 2 |
test.x = random(0,20); test.y = random(0,20); |
Помните, что test является структурой. Здесь присваиваем значения, которые нужно отправить в структуру. В данном случае мы просто отправляем рандомные числа. На практике эти данные можно заменить, скажем, показаниями датчиков.
Наконец, отправляем следующее сообщение:
1 |
esp_err_t result = esp_now_send(0, (uint8_t *) &test, sizeof(test_struct)); |
Первым аргументом функции esp_now_send () является MAC-адрес получателя. Если вы передадите 0 в качестве аргумента, плата отправит одно и то же сообщение всем зарегистрированным одноранговым узлам. Проверяем, успешно ли отправилось сообщение:
1 2 3 4 5 6 |
if (result == ESP_OK) { Serial.println("Sent with success"); } else { Serial.println("Error sending the data"); } |
Цикл loop() выполняется каждые 2000мс (2 секунды)
1 |
delay(2000); |
Отправка разных данных на платы
Данный код очень похож на предыдущий за исключением нескольких моментов:
Если вы хотите отправлять разные данные, нужно создать несколько структур, например:
1 2 3 |
test_struct test; test_struct test2; test_struct test3; |
В данном случае структура одного вида (test_struct), однако, возможно отправлять и структуры разных видов.
Далее, присваиваем рандомные числа переменным их наших структур.
1 2 3 4 5 6 |
test.x = random(0,20); test.y = random(0,20); test2.x = random(0,20); test2.y = random(0,20); test3.x = random(0,20); test3.y = random(0,20); |
И наконец, нужно вызвать функцию esp_now_send() для каждого получателя.
Для примера, отправляем данные из структуры на плату с MAC-адресом broadcastAddress1.
1 2 3 4 5 6 7 8 9 10 11 |
esp_err_t result1 = esp_now_send( broadcastAddress1, (uint8_t *) &test, sizeof(test_struct)); if (result1 == ESP_OK) { Serial.println("Sent with success"); } else { Serial.println("Error sending the data"); } |
Повторяем с другими платами. Отправляем данные из test2 на вторую плату:
1 2 3 4 5 6 7 8 9 10 11 |
esp_err_t result2 = esp_now_send( broadcastAddress2, (uint8_t *) &test2, sizeof(test_struct)); if (result2 == ESP_OK) { Serial.println("Sent with success"); } else { Serial.println("Error sending the data"); } |
И на третью плату из test3:
1 2 3 4 5 6 7 8 9 10 11 |
esp_err_t result3 = esp_now_send( broadcastAddress3, (uint8_t *) &test3, sizeof(test_struct)); if (result3 == ESP_OK) { Serial.println("Sent with success"); } else { Serial.println("Error sending the data"); } |
Далее представлен полный код для отправки разных сообщений на платы.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 |
/********* Rui Santos Complete project details at https://RandomNerdTutorials.com/esp-now-one-to-many-esp32-esp8266/ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files. The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. *********/ #include "esp_now.h" #include "WiFi.h" // ЗАМЕНИТЕ МАС-АДРЕСАМИ ПЛАТ-ПОЛУЧАТЕЛЕЙ uint8_t broadcastAddress1[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; uint8_t broadcastAddress2[] = {0xFF, , , , , }; uint8_t broadcastAddress3[] = {0xFF, , , , , }; typedef struct test_struct { int x; int y; } test_struct; void OnDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) { char macStr[18]; Serial.print("Packet to: "); // Записывает МАС-адрес отправителя в строку snprintf(macStr, sizeof(macStr), "%02x:%02x:%02x:%02x:%02x:%02x", mac_addr[0], mac_addr[1], mac_addr[2], mac_addr[3], mac_addr[4], mac_addr[5]); Serial.print(macStr); Serial.print(" send status:\t"); Serial.println(status == ESP_NOW_SEND_SUCCESS ? "Delivery Success" : "Delivery Fail"); } void setup() { Serial.begin(115200); WiFi.mode(WIFI_STA); if (esp_now_init() != ESP_OK) { Serial.println("Error initializing ESP-NOW"); return; } esp_now_register_send_cb(OnDataSent); // регистрируем сеть esp_now_peer_info_t peerInfo; peerInfo.channel = 0; peerInfo.encrypt = false; memcpy(peerInfo.peer_addr, broadcastAddress1, 6); if (esp_now_add_peer(&peerInfo) != ESP_OK){ Serial.println("Failed to add peer"); return; } memcpy(peerInfo.peer_addr, broadcastAddress2, 6); if (esp_now_add_peer(&peerInfo) != ESP_OK){ Serial.println("Failed to add peer"); return; } memcpy(peerInfo.peer_addr, broadcastAddress3, 6); if (esp_now_add_peer(&peerInfo) != ESP_OK){ Serial.println("Failed to add peer"); return; } } void loop() { test_struct test; test_struct test2; test_struct test3; test.x = random(0,20); test.y = random(0,20); test2.x = random(0,20); test2.y = random(0,20); test3.x = random(0,20); test3.y = random(0,20); esp_err_t result1 = esp_now_send( broadcastAddress1, (uint8_t *) &test, sizeof(test_struct)); if (result1 == ESP_OK) { Serial.println("Sent with success"); } else { Serial.println("Error sending the data"); } delay(500); esp_err_t result2 = esp_now_send( broadcastAddress2, (uint8_t *) &test2, sizeof(test_struct)); if (result2 == ESP_OK) { Serial.println("Sent with success"); } else { Serial.println("Error sending the data"); } delay(500); esp_err_t result3 = esp_now_send( broadcastAddress3, (uint8_t *) &test3, sizeof(test_struct)); if (result3 == ESP_OK) { Serial.println("Sent with success"); } else { Serial.println("Error sending the data"); } delay(2000); } |
Код для платы-приемника ESP32
Загрузите следующий код на платы-приемники (в нашем случае используются три платы-приемника).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
test_struct test; /********* Rui Santos Complete project details at https://RandomNerdTutorials.com/esp-now-one-to-many-esp32-esp8266/ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files. The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. *********/ #include "esp_now.h" #include "WiFi.h" //Пример структуры для приема данных //ДОЛЖЕН СОВПАДАТЬ СО СТРУКТУРОЙ ПЛАТЫ-ОТПРАВИТЕЛЯ typedef struct test_struct { int x; int y; } test_struct; //Создаем элемент struct_message с названием myData test_struct myData; //обратная функция, которая вызывается, когда сообщение получено void OnDataRecv(const uint8_t * mac, const uint8_t *incomingData, int len) { memcpy(&myData, incomingData, sizeof(myData)); Serial.print("Bytes received: "); Serial.println(len); Serial.print("x: "); Serial.println(myData.x); Serial.print("y: "); Serial.println(myData.y); Serial.println(); } void setup() { //Запускаем монитор порта Serial.begin(115200); //Выставляем режим клиента WiFi WiFi.mode(WIFI_STA); //Запускаем протокол ESP-NOW if (esp_now_init() != ESP_OK) { Serial.println("Error initializing ESP-NOW"); return; } // После запуска протокола определяем функцию recv CB // чтобы получать информацию о приеме esp_now_register_recv_cb(OnDataRecv); } void loop() { } |
Как работает код?
Аналогично отправителю, сначала подключаем библиотеки:
1 2 |
#include "esp_now.h" #include "WiFi.h" |
Создаем структуру для получения данных. Она должна быть идентичной той, что содержится в коде платы-отправителя.
1 2 3 4 |
typedef struct test_struct { int x; int y; } |
Создаем переменную test_struct с названием myData.
1 |
test_struct myData; |
Определяем обратную, функцию, которая вызывается при получении сообщения по ESP-NOW. У функции onDataRecv() должны быть следующие параметры:
1 |
void OnDataRecv(const uint8_t * mac, const uint8_t *incomingData, int len) { |
Копируем данные из переменной incomingData и записываем в myData.
1 |
memcpy(&myData, incomingData, sizeof(myData)); |
Итак, структура myData содержит несколько переменных со значениями, полученных от платы-отправителя. Для доступа, например к переменной x, вызовите функцию myData.x.
1 2 3 4 5 6 7 |
Serial.print("Bytes received: "); Serial.println(len); Serial.print("x: "); Serial.println(myData.x); Serial.print("y: "); Serial.println(myData.y); Serial.println(); |
В функции setup(), запускаем монитор порта.
1 |
Serial.begin(115200); |
Выставляем режим клиента WiFi.
1 |
WiFi.mode(WIFI_STA); |
Запускаем протокол ESP-NOW:
1 2 3 4 |
if (esp_now_init() != ESP_OK) { Serial.println("Error initializing ESP-NOW"); return; } |
Регистрируем обратную функцию. В данном случае – OnDataRecv(), которую мы создали ранее.
1 |
esp_now_register_recv_cb(OnDataRecv); |
Код платы-приемника ESP8266
Если вы используете плату ESP8266 в качестве приемника, загрузите вместо нее следующий код:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
/********* Rui Santos Complete project details at https://RandomNerdTutorials.com/esp-now-one-to-many-esp32-esp8266/ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files. The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. *********/ #include "ESP8266WiFi.h" #include "espnow.h" //Пример структуры для получения данных //Должна совпадать со структурой на плате-отправителе typedef struct test_struct { int x; int y; } test_struct; //Создаем элемент struct_message с именем myData test_struct myData; //обратная функция void OnDataRecv(uint8_t * mac, uint8_t *incomingData, uint8_t len) { memcpy(&myData, incomingData, sizeof(myData)); Serial.print("Bytes received: "); Serial.println(len); Serial.print("x: "); Serial.println(myData.x); Serial.print("y: "); Serial.println(myData.y); Serial.println(); } void setup() { //Запускаем монитор порта Serial.begin(115200); //Устанавливаем режим клиента WiFi WiFi.mode(WIFI_STA); //Инициализируем протокол ESP-NOW if (esp_now_init() != 0) { Serial.println("Error initializing ESP-NOW"); return; } // После запуска протокола определяем функцию recv CB // чтобы получать информацию о приеме esp_now_set_self_role(ESP_NOW_ROLE_SLAVE); esp_now_register_recv_cb(OnDataRecv); } void loop() { } |
За исключением некоторых деталей, этот код очень похож на код для платы-приемника ESP32.
Демонстрация
Включите все платы, откройте монитор порта Arduino для COM-порта, к которому подключен отправитель.
Должны начать поступать сообщения «Delivery Success» с соответствующим MAC-адресом получателя в мониторе порта.
Если вы отключите питание одной из плат, то получите сообщение «Delivery fail» для этой платы.
Если вы хотите проверить, действительно ли платы получают сообщения, можете открыть монитор порта для COM-порта, к которому они подключены, или использовать PuTTY для установки последовательной связи с вашими платами.
Если вы используете PuTTY, выберите «Последовательная связь», введите номер COM-порта и скорость передачи (115200), как показано ниже, и нажмите «Открыть».
Затем вы должны увидеть получаемые сообщения.
Повторите эти действия для каждой из плат и убедитесь, что они получают сообщения.
Заключение
В этом руководстве вы узнали, как отправлять данные на несколько плат ESP32 или ESP8266 с одной ESP32 с помощью ESP-NOW (конфигурация один-ко-многим). Используйте данное руководство как базу для создания более серьезных проектов.
Вопросы по прошивке и работе с кодом лучше писать напрямую автору в комментариях к статье (на англ. языке)