В данном руководстве мы рассмотрим организацию двусторонней передачи данных между двумя платами ESP8266 с помощью протокола ESP-NOW. Рассмотрим работу нашего проекта на примере обмена данными с датчика влажности и температуры (DHT).
У нас также есть и другие руководства по ESP8266:
Что такое ESP-NOW?
ESP-NOW – это протокол беспроводной связи от Espressif для обмена небольшими пакетами данных. Он позволяет нескольким устройствам взаимодействовать друг с другом без подключения к Wi-Fi.
Скорость обмена данными достаточно высокая, однако размер пакета ограничен 250 байтами, кроме того, протокол делает возможным как односторонний, так и двусторонний обмен данными. Последнее мы и рассмотрим в данном руководстве.
Обзор проекта
В данном проекте используется две платы ESP8266, к каждой и которых подключен датчик влажности и температуры. Каждая плата снимает показания с соответствующих датчиков и отправляет их другой плате по протоколу ESP-NOW.
Получив данные, плата выводит их в монитор порта (хотя можно и подключить OLED-дисплей для визуализации). При отправке сообщения реализуется callback-функция, которая отображает статус отправки. Также для организации взаимодействия нам потребуются MAC-адреса плат.
В примере мы используем две платы, однако, вы можете подключить и больше.
Что нужно для проекта?
Перед началом работы убедитесь, что в среде Arduino установлена плата ESP8266. Также нам потребуется установить библиотеки для датчиков DHT.
Установка требуемых библиотек
Мы используем библиотеку Adafruit для работы с датчиками влажности и температуры. Однако, для работы с ней требуется также установить библиотеку Adafruit Unified Sensor. Для их установки выполните следующие действия:
- Откройте менеджер библиотек (Скетч>Подключить библиотеку…>Менеджер библиотек);
- Найдите «DHT» и установите данную библиотеку:
- После установки введите в поле поиска «Adafruit Unified Sensor», пролистайте до самого низа и установите библиотеку:
После установки требуется перезагрузка среды Arduino.
Требуемые компоненты
Для проекта потребуются следующие детали:
-
- 2 платы ESP8266;
- 2 датчика температуры и влажности DHT22 или DHT11
- 2 резистора на 4,7 кОм;
- 2 макетные платы;
- Провода DuPont.
Получение MAC-адреса платы
Чтобы отправлять данные через ESP-NOW, вам нужно знать MAC-адрес плат. Каждая плата имеет уникальный MAC-адрес
Загрузите следующий код на платы:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// Complete Instructions to Get and Change ESP MAC Address: https://RandomNerdTutorials.com/get-change-esp32-esp8266-mac-address-arduino/ #include "WiFi.h" void setup(){ Serial.begin(115200); WiFi.mode(WIFI_MODE_STA); Serial.println(WiFi.macAddress()); } void loop(){ } |
После загрузки кода нажмите кнопку RST/EN на плате, МАС-адрес отобразится в мониторе порта.
Стоит записать MAC-адрес платы на бирке, чтобы было удобнее идентифицировать каждую плату
Принципиальная схема
Подключим датчик к плате следующим образом:
Здесь мы используем GPIO 5(D1), но вы также можете использовать любой другой подходящий пин.
Скетч для обмена данными
Загрузите следующий код на все платы. Помните, что потребуется изменить указанный в скетче МАС-адрес платы (той, на которую отправляем данные).
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 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 |
/* Rui Santos Complete project details at https://RandomNerdTutorials.com/esp-now-two-way-communication-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> #include <Adafruit_Sensor.h> #include <DHT.h> // ЗАМЕНИТЕ МАС-АДРЕС платы, на которую отправляем данные. uint8_t broadcastAddress[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; // Определяем пин, к которому подключен датчик #define DHTPIN 5 // Раскомментируйте ту строку, которая соответствует вашему датчику: //#define DHTTYPE DHT11 // DHT 11 #define DHTTYPE DHT22 // DHT 22 (AM2302) //#define DHTTYPE DHT21 // DHT 21 (AM2301) DHT dht(DHTPIN, DHTTYPE); // Вводим переменные для хранения отправляемых данных float temperature; float humidity; // Вводим переменные для хранения принимаемых данных float incomingTemp; float incomingHum; // Обновляем показания датчика каждые 10 секунд const long interval = 10000; unsigned long previousMillis = 0; // время последнего обновления // Переменная для хранения состояния отправки String success; //Пример структуры для отправки //Должна совпадать со структурой на плате-приемнике typedef struct struct_message { float temp; float hum; } struct_message; // Создаем переменную для хранения отправляемого сообщения struct_message DHTReadings; // То же, но для принимаемого сообщения struct_message incomingReadings; // Callback-функция для получения состояния отправки void OnDataSent(uint8_t *mac_addr, uint8_t sendStatus) { Serial.print("Last Packet Send Status: "); if (sendStatus == 0){ Serial.println("Delivery success"); } else{ Serial.println("Delivery fail"); } } // То же, для индикации состояния приема данных void OnDataRecv(uint8_t * mac, uint8_t *incomingData, uint8_t len) { memcpy(&incomingReadings, incomingData, sizeof(incomingReadings)); Serial.print("Bytes received: "); Serial.println(len); incomingTemp = incomingReadings.temp; incomingHum = incomingReadings.hum; } void getReadings(){ // Снимаем значение температуры temperature = dht.readTemperature(); if (isnan(temperature)){ Serial.println("Failed to read from DHT"); temperature = 0.0; } humidity = dht.readHumidity(); if (isnan(humidity)){ Serial.println("Failed to read from DHT"); humidity = 0.0; } } void printIncomingReadings(){ // Отображаем показания в мониторе порта Serial.println("INCOMING READINGS"); Serial.print("Temperature: "); Serial.print(incomingTemp); Serial.println(" ºC"); Serial.print("Humidity: "); Serial.print(incomingHum); Serial.println(" %"); } void setup() { // Запускаем монитор порта Serial.begin(115200); // Включаем датчик dht.begin(); // Выставляем режим работы Wi-Fi WiFi.mode(WIFI_STA); WiFi.disconnect(); // Инициализируем протокол ESP-NOW if (esp_now_init() != 0) { Serial.println("Error initializing ESP-NOW"); return; } // Указываем роль платы в сети esp_now_set_self_role(ESP_NOW_ROLE_COMBO); // Регистрируем callback-функцию для получения статуса отправки esp_now_register_send_cb(OnDataSent); // Регистрируем пиры esp_now_add_peer(broadcastAddress, ESP_NOW_ROLE_COMBO, 1, NULL, 0); // Регистрируем callback-функцию для получения статуса приема esp_now_register_recv_cb(OnDataRecv); } void loop() { unsigned long currentMillis = millis(); if (currentMillis - previousMillis >= interval) { // Сохраняем время последнего обновления показаний previousMillis = currentMillis; //Запрашиваем показания датчика getReadings(); //Записываем их в переменные DHTReadings.temp = temperature; DHTReadings.hum = humidity; // Отправляем сообщение esp_now_send(broadcastAddress, (uint8_t *) &DHTReadings, sizeof(DHTReadings)); // Выводим входящие данные printIncomingReadings(); } } |
Как работает код?
Начинаем с подключения библиотек:
1 2 3 |
#include <ESP8266WiFi.h> #include <espnow.h> |
Для корректной работы убедитесь, что у вас выбрана именно ESP8266 в меню «Инструменты>Плата»
Подключаем библиотеки датчика DHT:
1 2 3 |
#include <Adafruit_Sensor.h> #include <DHT.h> |
Далее указываем МАС-адрес платы-получателя:
1 |
uint8_t broadcastAddress[] = {0x2C, 0x3A, 0xE8, 0x0E, 0xBB, 0xED}; |
Определяем пин, к которому подключен датчик. В нашем случае – GPIO 5 (D1):
1 |
#define DHTPIN 5 |
Выбираем тип используемого датчика. Мы используем DHT22. Если вы используете другой, то раскомментируйте нужную строку и закомментируйте остальные.
1 2 3 4 5 |
//#define DHTTYPE DHT11 // DHT 11 #define DHTTYPE DHT22 // DHT 22 (AM2302) //#define DHTTYPE DHT21 // DHT 21 (AM2301) |
Создаем экземпляр класса DHT на выбранном пине с выбранным типом датчика:
1 |
DHT dht(DHTPIN, DHTTYPE); |
Задаем переменные для хранения отправляемых данных:
1 2 3 |
float temperature; float humidity; |
Создаем еще две для хранения принимаемых показаний:
1 2 3 |
float incomingTemp; float incomingHum; |
Мы будем отправлять показания датчика каждые 10 секунд, этот интервал можно изменить.
1 2 3 |
const long interval = 10000; unsigned long previousMillis = 0; |
Следующая переменная хранит данные о состоянии отправки на другую плату:
1 |
String success; |
Создаем структуру для хранения показаний датчика
1 2 3 4 5 6 7 |
typedef struct struct_message { float temp; float hum; } struct_message; |
Помните, что она должна совпадать со структурой на плате-приемнике.
Создаем объект для хранения отправляемого сообщения:
1 |
struct_message DHTReadings; |
И еще один для хранения принимаемого сообщения:
1 |
struct_message incomingReadings; |
Далее, определяем callback-функцию, которая выводит в монитор порта информацию о состоянии отправки. Если функция возвращает 0 – сообщение успешно доставлено:
1 2 3 4 5 6 7 8 9 |
void OnDataSent(uint8_t *mac_addr, uint8_t sendStatus) { Serial.print("Last Packet Send Status: "); if (sendStatus == 0){ Serial.println("Delivery success"); } |
Если она возвращает 1 – сообщение не отправлено:
1 2 3 4 5 |
else { success = "Delivery Fail :("; } |
Также создаем аналогичную функцию для получения состояние приема данных:
1 |
void OnDataRecv(uint8_t * mac, uint8_t *incomingData, uint8_t len) { |
Сохраняем полученный пакет данных в созданную ранее структуру incomingReadings:
1 |
memcpy(&incomingReadings, incomingData, sizeof(incomingReadings)); |
И выводим сообщение в монитор порта. Помните, что размер сообщения ограничен 250 байтами.
1 2 3 |
Serial.print("Bytes received: "); Serial.println(len); |
Разбираем полученный пакет и записываем показания влажности и температуры в соответствующие переменные:
1 2 3 |
incomingTemp = incomingReadings.temp; incomingHum = incomingReadings.hum; |
Определяем функцию getReadings() для снятия показаний датчика:
1 |
temperature = dht.readTemperature(); |
Если показания снять не удалось, датчик возвращает неопределенный результат (nan). В таком случае выставляем нулевое значение влажности.
1 2 3 4 5 6 7 8 9 |
humidity = dht.readHumidity(); if (isnan(humidity)){ Serial.println("Failed to read from DHT"); humidity = 0.0; } |
Функция printIncomingReadings() выводит полученные данные в монитор порта:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
void printIncomingReadings(){ Serial.println("INCOMING READINGS"); Serial.print("Temperature: "); Serial.print(incomingTemp); Serial.println(" ºC"); Serial.print("Humidity: "); Serial.print(incomingHum); Serial.println(" %"); } |
В setup() мы запускаем монитор порта и датчик, а также выставляем режим работы Wi-Fi и затем отключаемся от него:
1 2 3 4 5 6 7 |
Serial.begin(115200); dht.begin(); 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; } |
Указываем роль платы в сети. В нашем случае ESP8266 получает и отправляет данные, поэтому выставляем значение COMBO:
1 |
esp_now_set_self_role(ESP_NOW_ROLE_COMBO); |
Регистрируем callback-функцию отправки:
1 |
esp_now_register_send_cb(OnDataSent); |
Для отправки данных другой плате вы должны указать ее в списке пиров. Для этого прописывается следующая строка:
1 |
esp_now_add_peer(broadcastAddress, ESP_NOW_ROLE_COMBO, 1, NULL, 0); |
Регистрируем callback-функцию для приема:
1 |
esp_now_register_recv_cb(OnDataRecv); |
В loop() проверяем, когда нам нужно обновить показания датчика:
1 2 3 4 5 6 7 |
unsigned long currentMillis = millis(); if (currentMillis - previousMillis >= interval) { // save the last time you updated the DHT values previousMillis = currentMillis; |
Когда время последней отправки становится большим (или равным) заданному интервалу. вызывается функция getReadings():
1 |
getReadings(); |
Затем обновляем переменные, хранящие показания:
1 2 3 |
DHTReadings.temp = temperature; DHTReadings.hum = humidity; |
И отправляем сообщение:
1 |
esp_now_send(broadcastAddress, (uint8_t *) &DHTReadings, sizeof(DHTReadings)); |
Наконец, выводим полученные данные в монитор порта:
1 |
printIncomingReadings(); |
Демонстрация
После загрузки кода на каждую из плат откройте монитор порта. Также вы можете открыть два последовательных соединения используя PuTTY, чтобы одновременно наблюдать за работой обеих плат.
Если все работает как надо, каждая плата показывает полученные с других плат показания датчиков.
Заключение
В данном руководстве мы рассмотрели, как организовать двусторонний обмен данными между платами ESP8266 с использованием протокола ESP-NOW.
Мы показали пример, где использовали две платы, но вы можете подключить и больше.
Вопросы по прошивке и работе с кодом лучше писать напрямую автору в комментариях к статье (на англ. языке)