В данном руководстве рассмотрим, как настроить пересылку данных по схеме one-slave-multi-master на платах ESP-8266 с помощью протокола ESP-NOW. Данная конфигурация удобна, когда требуется получать данные с нескольких нод на одну плату. Платы будем программировать в среде Arduino.
У нас также есть и другие руководства по ESP-NOW на ESP8266:
И для ESP32:
Обзор проекта
Настроим плату ESP8266 для получения данных с нескольких таких же плат с использованием протокола ESP-NOW по следующей схеме:
Что будет реализовано в ходе проекта?
– Одна плата ESP8266 работает в режиме получателя;
– Несколько плат работают в режиме отправителей. В данном руководстве мы будем использовать две платы в этой роли. Но вы, конечно, можете использовать и больше;
– Отправители получают сообщение о состоянии отправки сообщения;
– Плата-получатель определяет, с какой платы поступило сообщение.
Примечание: В документации к ESP-NOW нет таких понятий, как «отправитель/ведущий» и «получатель/ведомый», так как каждая плата может выполнять обе эти роли.
Что нам нужно для проекта?
Платы мы будем программировать в среде Arduino, а следовательно перед тем, как начать работу, убедитесь, что дополнения для ESP8266 установлены в вашей среде Arduino.
Получение МАС-адреса платы
Для того, чтобы отправлять данные по протоколу ESP-NOW вам необходимо знать МАС-адрес получателя (у каждой платы он уникален).
Загрузите следующий код на свою плату ESP8266 для получения МАС-адреса:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
#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, после перезапуска платы МАС-адрес выведется в монитор порта.
Код для платы-отправителя
Плата-получатель должна уметь определять отправителя по его МАС-адресу. Тем не менее, сам процесс идентификации может быть весьма мудреным.
Чтобы упростить ситуацию, мы присвоим каждой плате идентификационный номер, начиная с 1. ID будет отправляться наряду с другими переменными.
В качестве примера, мы отправим структуру, которая содержит ID и два рандомных числа, как показано на картинке ниже:
Загрузите следующий код на каждую из плат-отправителей. Не забудьте, что нужно изменять ID отправителя для каждой платы.
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 |
/* Rui Santos Complete project details at https://RandomNerdTutorials.com/esp-now-many-to-one-esp8266-nodemcu/ 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> // ЗАМЕНИТЕ МАС-АДРЕСОМ ПОЛУЧАТЕЛЯ uint8_t broadcastAddress[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; // ОПРЕДЕЛЯЕМ ID ПЛАТЫ #define BOARD_ID 2 // Определяем структуру сообщения // Она должна совпадать со структурой у платы-получателя typedef struct struct_message { int id; int x; int y; } struct_message; // Создаем сообщение с заданной структурой struct_message myData; unsigned long lastTime = 0; unsigned long timerDelay = 10000; // Callback-функция при отправке сообщения void OnDataSent(uint8_t *mac_addr, uint8_t sendStatus) { Serial.print("\r\nLast Packet Send Status: "); if (sendStatus == 0){ Serial.println("Delivery success"); } else{ Serial.println("Delivery fail"); } } void setup() { // Запускаем монитор порта Serial.begin(115200); // Выставляем режим работы Wi-Fi WiFi.mode(WIFI_STA); WiFi.disconnect(); // Инициализируем ESP-NOW if (esp_now_init() != 0) { Serial.println("Error initializing ESP-NOW"); return; } // Указываем роль платы в ESP-NOW esp_now_set_self_role(ESP_NOW_ROLE_CONTROLLER); // После запуска протокола получаем обратную связь о состоянии отправки esp_now_register_send_cb(OnDataSent); // Регистрируем пиры esp_now_add_peer(broadcastAddress, ESP_NOW_ROLE_SLAVE, 1, NULL, 0); } void loop() { if ((millis() - lastTime) > timerDelay) { // Выставляем значения, которые будем отправлять myData.id = BOARD_ID; myData.x = random(1, 50); myData.y = random(1, 50); // Отправляем сообщение esp_now_send(0, (uint8_t *) &myData, sizeof(myData)); lastTime = millis(); } } |
Как работает код?
Сначала подключаем необходимые библиотеки:
1 2 3 |
#include <ESP8266WiFi.h> #include <espnow.h> |
Вставляем МАС-адреса получателя в следующую строку:
1 |
uint8_t broadcastAddress[] = {0x5C, 0xCF, 0x7F, 0x99, 0xA1, 0x70}; |
Указываем ID платы отправителя. Не забудьте, что у других плат ID тоже должны быть другими.
1 |
#define BOARD_ID 2 |
Создаем структуру, которая будет содержать данные для отправки. Мы назвали ее struct_message и она содержит три целочисленные переменные, ID платы, x и y. Тип данных легко изменить, однако не забывайте, что на стороне получателя структуру тоже надо изменить.
1 2 3 4 5 6 7 8 9 |
typedef struct struct_message { int id; int x; int y; } struct_message; |
Создаем переменную struct_message с именем myData, которая будет хранить значения переменных.
1 |
struct_message myData; |
Создаем callback-функцию
Определяем функцию OnDataSent(). Эта callback-функция должна выполняться при отправке сообщения и, в нашем случае, выводить статус отправки.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
void OnDataSent(uint8_t *mac_addr, uint8_t sendStatus) { Serial.print("\r\nLast Packet Send Status: "); if (sendStatus == 0){ Serial.println("Delivery success"); } else{ Serial.println("Delivery fail"); } } |
setup()
В функции setup() запускаем монитор порта.
1 |
Serial.begin(115200); |
Выставляем режим работы Wi-Fi (приёмник).
1 2 3 |
WiFi.mode(WIFI_STA); WiFi.disconnect(); |
Запускаем протокол ESP-NOW
1 2 3 4 5 6 7 |
if (esp_now_init() != 0) { Serial.println("Error initializing ESP-NOW"); return; } |
Указываем выполняемую платой роль.
1 |
esp_now_set_self_role(ESP_NOW_ROLE_CONTROLLER); |
После запуска протокола регистрируем callback-функцию, которая будет вызываться после отправки сообщения. В данном случае, регистрируем функцию OnDataSent(), которую мы создали ранее.
1 |
esp_now_register_send_cb(OnDataSent); |
Добавляем пиров
Для отправки данных на другую плату (получатель), требуется подключить пиры. Следующая строка регистрирует нового пира.
1 |
esp_now_add_peer(broadcastAddress, ESP_NOW_ROLE_SLAVE, 1, NULL, 0); |
loop()
Здесь мы отправляем сообщение каждые 10 секунд (задержку вы можете изменить в переменной timerDelay).
Присваиваем значение каждой переменной.
1 2 3 4 5 |
myData.id = BOARD_ID; myData.x = random(1, 50); myData.y = random(1, 50); |
Не забываем менять ID для каждой платы!
Помните, что myData является структурой.
Отправляем сообщение.
1 |
esp_now_send(0, (uint8_t *) &myData, sizeof(myData)); |
Код для платы получателя
Загрузите следующий код на плату-получатель. Данный код нужно будет изменить, если у вас больше чем две платы-отправителя.
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 |
/* Rui Santos Complete project details at https://RandomNerdTutorials.com/esp-now-many-to-one-esp8266-nodemcu/ 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 struct_message { int id; int x; int y; } struct_message; // Создаем struct_message с именем myData struct_message myData; // Создаем структуры для хранения данных с разных плат struct_message board1; struct_message board2; // Создаем массив со структурами struct_message boardsStruct[2] = {board1, board2}; // Callback-функция, выполняемая при получении данных void OnDataRecv(uint8_t * mac_addr, uint8_t *incomingData, uint8_t len) { char macStr[18]; Serial.print("Packet received 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.println(macStr); memcpy(&myData, incomingData, sizeof(myData)); Serial.printf("Board ID %u: %u bytes\n", myData.id, len); // Ввод полученных данных в структуры boardsStruct[myData.id-1].x = myData.x; boardsStruct[myData.id-1].y = myData.y; Serial.printf("x value: %d \n", boardsStruct[myData.id-1].x); Serial.printf("y value: %d \n", boardsStruct[myData.id-1].y); Serial.println(); } void setup() { // Запускаем монитор порта Serial.begin(115200); // Выставляем режим работы Wi-Fi WiFi.mode(WIFI_STA); WiFi.disconnect(); // Запускаем протокол ESP-NOW if (esp_now_init() != 0) { Serial.println("Error initializing ESP-NOW"); return; } // Выставляем роль платы и регистрируем callback-функцию esp_now_set_self_role(ESP_NOW_ROLE_SLAVE); esp_now_register_recv_cb(OnDataRecv); } void loop(){ // Записываем данные с плат в переменные /*int board1X = boardsStruct[0].x; int board1Y = boardsStruct[0].y; int board2X = boardsStruct[1].x; int board2Y = boardsStruct[1].y; */ } |
Как работает код?
Идентично отправителю, начинаем с подключения библиотек.
1 2 3 |
#include <ESP8266WiFi.h> #include <espnow.h> |
Создаем структуру для получения данных. Она должна совпадать со структурой в скетче для платы-отправителя.
1 2 3 4 5 6 7 8 9 |
typedef struct struct_message { int id; int x; int y; } struct_message; |
Создаем переменную для хранения полученных данных.
1 |
struct_message myData; |
Затем создаем struct_message для каждой платы, таким образом мы сможем определять с какой платы поступили данные. В данном случае, создаем структуры для данных с двух плат, если у вас больше – нужно создать больше структур.
1 2 3 |
struct_message board1; struct_message board2; |
Создаем массив, содержащий все эти структуры.
1 |
struct_message boardsStruct[2] = {board1, board2}; |
Создаем callback-функцию
Функция OnDataRecv() должна выполняться при получении сообщения платой, у нее должны быть следующие параметры:
1 |
void OnDataRecv(uint8_t * mac_addr, uint8_t *incomingData, uint8_t len) { |
Получаем МАС-адрес платы-отправителя
1 2 3 4 5 6 7 |
Serial.print("Packet received 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.println(macStr); |
Записываем полученные данные из переменной incomingData в myData:
1 |
memcpy(&myData, incomingData, sizeof(myData)); |
Итак, мы записали полученные данные, которые можем идентифицировать по ID отправителя. Теперь разбираем полученные данные:
1 2 3 |
boardsStruct[myData.id-1].x = myData.x; boardsStruct[myData.id-1].y = myData.y; |
Для понимания представим что мы получили данные от платы с ID 2, то есть значение переменной myData ID – 2.
Теперь нужно записать значения в переменные соответствующей структуры. Но плата с ID=2 имеет индекс 1 (в Си массивы начинаются с 0), поэтому мы и пишем [myData.id-1].
setup()
Запускаем монитор порта.
1 |
Serial.begin(115200); |
Выставляем режим работы Wi-Fi и отключаемся от него.
1 2 3 |
WiFi.mode(WIFI_STA); WiFi.disconnect(); |
Запускаем протокол ESP-NOW:
1 2 3 4 5 6 7 |
if (esp_now_init() != ESP_OK) { Serial.println("Error initializing ESP-NOW"); return; } |
Указываем роль платы. т.к. это получатель, то выставляем:
1 |
esp_now_set_self_role(ESP_NOW_ROLE_SLAVE); |
Регистрируем callback-функцию, выполняемую при получении данных. В данном случае, это ранее созданная функция OnDataRecv().
1 |
esp_now_register_recv_cb(OnDataRecv); |
Следующие строки, закомментированные в loop(), написаны для того, чтобы показать пример записи полученных данных для дальнейшего использования.
1 |
int board1X = boardsStruct[0].x; |
Демонстрация
Загрузите код для платы-отправителя на соответствующие платы. Не забудьте, что у плат должны быть уникальные ID.
На мониторе порта плат-отправителей вы должны получить сообщение “Delivery success”, если сообщения успешно доставлены.
Плата-получатель должна начать получать пакеты данных с других плат.
Заключение
В этом руководстве мы рассмотрели, как организовать обмен данными по схеме “one-slave-multiple-masters” на платах ESP8266. Данный код достаточно легко изменить для отправки команд или, например, показаний датчиков.
Вопросы по прошивке и работе с кодом лучше писать напрямую автору в комментариях к статье (на англ. языке)
2 комментария. Оставить новый
Возможно ли обновление прошивки по ESP-NOW ?
К примеру на одной ESP32 с WEB мордой выбираем прошивку и выбираем на какое устройство отправить и прошивка передаётся на нужную плату ESP по протоколу ESP-NOW ?
Да, в статье говорится о возможности обновления прошивки по протоколу ESP-NOW. На одной из ESP32 с WEB-мордой можно выбрать необходимую прошивку и передать ее на другую плату ESP через ESP-NOW. В статье также приводится пример кода на языке Arduino, который демонстрирует, как отправить файл прошивки на другое устройство по протоколу ESP-NOW. Таким образом, обновление прошивки по ESP-NOW возможно.Вот пример кода на языке Arduino для передачи файла прошивки с одной платы ESP32 на другую по протоколу ESP-NOW:
#include
#include
// MAC адрес получателя
uint8_t broadcastAddress[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
// Callback функция при получении данных
void OnDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) {
Serial.print(“Packet to: “);
for (int i = 0; i < 6; i++) { Serial.printf("%02X", mac_addr[i]); if (i < 5) { Serial.print(":"); } } Serial.printf(" send %s\n", status == ESP_NOW_SEND_SUCCESS ? "success" : "fail"); } void setup() { // Инициализация серийного порта для вывода результатов Serial.begin(115200); while (!Serial); // Инициализация WiFi модуля WiFi.mode(WIFI_STA); // Инициализация протокола ESP-NOW if (esp_now_init() != ESP_OK) { Serial.println("Error initializing ESP-NOW"); return; } // Регистрация callback функции при отправке данных esp_now_register_send_cb(OnDataSent); // Добавление получателя esp_now_peer_info_t peerInfo; memcpy(peerInfo.peer_addr, broadcastAddress, 6); peerInfo.channel = 0; peerInfo.encrypt = false; if (esp_now_add_peer(&peerInfo) != ESP_OK) { Serial.println("Failed to add peer"); return; } // Открытие файла прошивки File f = SPIFFS.open("/firmware.bin", "r"); if (!f) { Serial.println("Failed to open file for reading"); return; } // Получение размера файла прошивки int fileSize = f.size(); // Отправка файла прошивки по протоколу ESP-NOW uint8_t data[fileSize]; f.readBytes(data, fileSize); esp_err_t result = esp_now_send(broadcastAddress, data, fileSize); if (result == ESP_OK) { Serial.println("Data sent successfully"); } else { Serial.println("Error sending data"); } // Закрытие файла прошивки f.close(); } void loop() { // do nothing } Этот код читает файл прошивки "firmware.bin" из файловой системы SPIFFS и отправляет его на другую плату ESP32 с указанным MAC-адресом. Вам нужно будет добавить обработку запросов на выбор файла прошивки для передачи и выбор устройства, на которое прошивка должна быть отправлена, для реализации требуемого функционала.