Беспроводное управление без роутера: Кроссплатформенный скетч ESP-NOW для ESP32 и ESP8266.

Время чтения: 15 минут | Технология: IoT Development | Платформы: ESP8266, ESP32


В предыдущих статьях мы заложили фундамент: разобрали Введение в ESP-NOW: Беспроводная связь на ESP8266, научились писать кроссплатформенный код в Arduino IDE и рассмотрели базовое управление светодиодом с кнопки.

Однако при переносе теории в практику разработчики часто сталкиваются с тремя критическими проблемами:

  1. Разные версии ядер Arduino ESP32 Core (v2.x и новые v3.x) имеют разный синтаксис функций.
  2. Рассинхронизация Wi-Fi каналов приводит к тому, что устройства просто "не видят" друг друга.
  3. Различия в обработке ошибок (0 против ESP_OK) вызывают ошибки компиляции.

В предыдущих статьях мы заложили фундамент: разобрали Введение в ESP-NOW: Беспроводная связь на ESP8266, научились писать кроссплатформенный код в Arduino IDE

В этой статье мы публикуем полностью протестированную, исправленную и рабочую версию кроссплатформенного скетча. Этот код учитывает все аппаратные и программные нюансы, позволяя использовать ESP8266 и ESP32 в одной связке без единой ошибки.


Аббревиатуры и термины:

  • ESP-NOW — протокол связи от Espressif для прямого обмена пакетами до 250 байт без Wi-Fi роутера.
  • MAC-адрес — уникальный физический идентификатор сетевого модуля платы (например, A0:20:A6:1A:D3:86).
  • Arduino Core v2.x / v3.x — базовые пакеты программного обеспечения, которые позволяют Arduino IDE понимать команды для ESP32. Версия 3.0 внесла серьезные изменения в типы данных.
  • Callback (функция обратного вызова) — функция-обработчик, которая запускается автоматически в момент прихода данных или после их отправки.
  • Debouce (Антидребезг) — алгоритм программной фильтрации дребезга контактов механической кнопки.

Полный кроссплатформенный скетч.

Скопируйте этот код в файл main.ino в Arduino IDE. Ниже мы подробно разберем каждый блок.

/*
 * Беспроводное управление LED по ESP-NOW
 * КРОСС-ПЛАТФОРМЕННАЯ ВЕРСИЯ: Полная совместимость ESP32 и ESP8266
 * ИСПРАВЛЕННАЯ ВЕРСИЯ: Совместима с Arduino ESP32 Core v2.x и v3.x
 */
// ==========================================
// 1. УПРАВЛЕНИЕ РЕЖИМОМ
// ==========================================
// Для прошивки ПРИЕМНИКА: Закомментируйте эту строку ( // #define IS_SENDER )
// Для прошивки ОТПРАВИТЕЛЯ: Раскомментируйте эту строку ( #define IS_SENDER )
//#define IS_SENDER 
// MAC-адрес ПРИЕМНИКА. 
uint8_t receiverMac[] = {0xA0, 0x20, 0xA6, 0x1A, 0xD3, 0x86}; // <-- ЗАМЕНИТЕ НА СВОЙ!
// ==========================================
// 2. ПОДКЛЮЧЕНИЕ БИБЛИОТЕК И ПИНОВ
// ==========================================
#ifdef ESP32
  #include <WiFi.h>
  #include <esp_wifi.h> 
  #include <esp_now.h>
  #define BOARD_NAME "ESP32"
  #define LED_PIN    2             
  #define BTN_PIN    4             
  #define LED_ON     HIGH
  #define LED_OFF    LOW 
#else
  #include <ESP8266WiFi.h>
  #include <espnow.h>
  #define BOARD_NAME "ESP8266"
  #define LED_PIN    LED_BUILTIN   
  #define BTN_PIN    0             
  #define LED_ON     LOW           
  #define LED_OFF    HIGH
#endif
#include <string.h> // Для memcpy и memset
// Структура данных
typedef struct { 
  bool ledState; 
} Message;
Message myData;
// Переменные для антидребезга кнопки
bool lastBtnState = HIGH;
bool stableBtnState = HIGH;
unsigned long lastDebounceTime = 0;
const unsigned long debounceDelay = 50; 
// ==========================================
// 3. CALLBACK-ФУНКЦИИ
// ==========================================
#ifdef ESP32
  void OnDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) {
    Serial.println(status == ESP_NOW_SEND_SUCCESS ? " Доставлено" : " Ошибка");
  }
  // Функция приема данных (Совместимость с ядром ESP32 v3.x.x)
  // Если у вас старое ядро (2.x.x), замените строку ниже на: void OnDataRecv(const uint8_t * mac, const uint8_t *incomingData, int len)
  void OnDataRecv(const uint8_t * mac, const uint8_t *incomingData, int len) {
    memcpy(&myData, incomingData, sizeof(myData));
    digitalWrite(LED_PIN, myData.ledState ? LED_ON : LED_OFF);
    Serial.println(myData.ledState ? " Прием: LED ON" : " Прием: LED OFF");
  }
#else
  // Функции для ESP8266
  void OnDataSent(uint8_t *mac_addr, uint8_t status) {
    Serial.println(status == 0 ? " Доставлено" : " Ошибка");
  }
  void OnDataRecv(uint8_t *mac, uint8_t *incomingData, uint8_t len) {
    memcpy(&myData, incomingData, sizeof(myData));
    digitalWrite(LED_PIN, myData.ledState ? LED_ON : LED_OFF);
    Serial.println(myData.ledState ? " Прием: LED ON" : " Прием: LED OFF");
  }
#endif
// ==========================================
// 4. НАСТРОЙКА (setup)
// ==========================================
void setup() {
  Serial.begin(115200);
  pinMode(LED_PIN, OUTPUT);
  pinMode(BTN_PIN, INPUT_PULLUP); 
  digitalWrite(LED_PIN, LED_OFF);
  // --- КРИТИЧЕСКИЙ БЛОК ДЛЯ ESP-NOW ---
  WiFi.mode(WIFI_STA);
  WiFi.disconnect(); 
  #ifdef ESP8266
    wifi_set_channel(1); 
  #else
    esp_wifi_set_channel(1, WIFI_SECOND_CHAN_NONE); 
  #endif
  // ------------------------------------
  Serial.println("=================================");
  Serial.print("Запуск платы: "); Serial.println(BOARD_NAME);
  Serial.print(" Мой MAC-адрес: "); Serial.println(WiFi.macAddress()); 
  // --- ИСПРАВЛЕННАЯ ПРОВЕРКА ИНИЦИАЛИЗАЦИИ ---
  // ESP32 использует esp_err_t (ESP_OK), а ESP8266 возвращает int (0)
  #ifdef ESP32
    if (esp_now_init() != ESP_OK) {
  #else
    if (esp_now_init() != 0) {
  #endif
      Serial.println(" Ошибка инициализации ESP-NOW!");
      return; // Прерываем setup, если инициализация не удалась
    }
  Serial.println(" ESP-NOW инициализирован успешно.");
  // -------------------------------------------
  #ifdef IS_SENDER
    Serial.println(" РЕЖИМ: ОТПРАВИТЕЛЬ");
    #ifdef ESP32
      esp_now_register_send_cb(OnDataSent);
      esp_now_peer_info_t peerInfo;
      memset(&peerInfo, 0, sizeof(peerInfo)); 
      memcpy(peerInfo.peer_addr, receiverMac, 6);
      peerInfo.channel = 1;       
      peerInfo.encrypt = false;
      if (esp_now_add_peer(&peerInfo) != ESP_OK) {
        Serial.println(" Ошибка добавления пира!");
      }
    #else
      // Настройка для ESP8266
      esp_now_set_self_role(ESP_NOW_ROLE_CONTROLLER);
      esp_now_register_send_cb(OnDataSent);
      esp_now_add_peer(receiverMac, ESP_NOW_ROLE_SLAVE, 1, NULL, 0); 
    #endif
  #else
    Serial.println(" РЕЖИМ: ПРИЕМНИК (Ожидание данных...)");
    #ifdef ESP32
      esp_now_register_recv_cb(OnDataRecv);
    #else
      esp_now_set_self_role(ESP_NOW_ROLE_SLAVE);
      esp_now_register_recv_cb(OnDataRecv);
    #endif
  #endif
  Serial.println("=================================");
}
// ==========================================
// 5. ОСНОВНОЙ ЦИКЛ (loop)
// ==========================================
void loop() {
  #ifdef IS_SENDER
    bool currentBtnState = digitalRead(BTN_PIN);
    if (currentBtnState != lastBtnState) {
      lastDebounceTime = millis();
    }
    if ((millis() - lastDebounceTime) > debounceDelay) {
      if (currentBtnState != stableBtnState) {
        stableBtnState = currentBtnState;
        if (stableBtnState == LOW) { 
          myData.ledState = !myData.ledState; 
          Serial.print(" Отправка: ");
          Serial.println(myData.ledState ? "ON" : "OFF");
          esp_now_send(receiverMac, (uint8_t *) &myData, sizeof(myData));
        }
      }
    }
    lastBtnState = currentBtnState;
  #endif
}

Подробный разбор настроек и исправлений:

ESP-NOW работает на физическом уровне Wi-Fi.

1. Переключение ролей через #define IS_SENDER

Вместо того чтобы писать два разных скетча (один для передатчика, другой для приемника), мы используем директиву препроцессора.

  • Если строка #define IS_SENDER раскомментирована (без слешей // в начале), компилятор соберет код для платы-кнопки (передатчика). Функция loop() будет опрашивать кнопку и отправлять пакеты.
  • Если строка закомментирована (//#define IS_SENDER), компилятор проигнорирует весь код отправки. Плата превратится в приемник, который просто ждет пакеты в фоновом режиме через callback-функции.

2. Решение проблемы "Невидимости" (Синхронизация Wi-Fi каналов).

ESP-NOW работает на физическом уровне Wi-Fi. Главное правило: оба устройства должны находиться на одном радиоканале. При включении ESP-платы могут автоматически выбрать разные каналы (например, одна на 1, другая на 6). В таком случае связь не установится, и вы будете видеть Ошибка при отправке. В блоке setup() мы принудительно отключаем платы от любых сетей и жестко задаем 1-й канал:

  • Для ESP8266: wifi_set_channel(1);
  • Для ESP32: esp_wifi_set_channel(1, WIFI_SECOND_CHAN_NONE); При добавлении пира (приемника) на стороне ESP32 мы также указываем: peerInfo.channel = 1;. Это гарантирует 100% стабильность связи.

3. Совместимость с Arduino ESP32 Core v3.x.

Недавно вышло ядро ESP32 версии 3.0, которое сломало обратную совместимость многих старых скетчей ESP-NOW. Изменились типы данных в функциях обратного вызова (Callback). В нашем коде для ESP32 используется современный формат: void OnDataRecv(const uint8_t * mac, const uint8_t *incomingData, int len) Если по какой-то причине вы используете старую версию ядра (2.0.x) и получаете ошибку компиляции, просто уберите ключевые слова const в аргументах функции. Для ESP8266 формат остается неизменным годами (uint8_t *mac).

4. Правильная инициализация и проверка ошибок.

ESP32 и ESP8266 по-разному сообщают об успехе:

  • ESP8266 возвращает целое число 0 при успехе.
  • ESP32 использует перечисление ESP_OK. Блок #ifdef ESP32 ... #else ... #endif при проверке esp_now_init() позволяет коду компилироваться без ошибок на обеих платформах, выдавая понятные логи в Serial-порт. Аналогично, при добавлении пира на ESP32 используется функция memset(&peerInfo, 0, sizeof(peerInfo));. Она очищает оперативную память от "мусора" перед записью MAC-адреса. Без этого могут происходить случайные сбои шифрования или передачи.

5. Аппаратная абстракция (Инверсия логики).

Обратите внимание на макросы:

#define LED_ON     HIGH // Для ESP32
#define LED_ON     LOW  // Для ESP8266

Это избавляет вас от необходимости писать сложный код. Вы просто говорите микроконтроллеру digitalWrite(LED_PIN, LED_ON);, а макрос сам подставляет нужное значение напряжения в зависимости от выбранной платы.


Пошаговая инструкция: Как запустить систему.

Для работы вам понадобятся две платы (например, одна ESP32 и одна ESP8266, либо две одинаковых).

Пошаговая инструкция: Как запустить систему.

Шаг 1: Подготовка и прошивка ПРИЕМНИКА.

  1. Откройте код в Arduino IDE.
  2. Убедитесь, что строка #define IS_SENDER закомментирована: //#define IS_SENDER
  3. Подключите плату, которая будет Приемником (например, ESP32), к компьютеру.
  4. В меню выберите: Инструменты -> Плата -> ESP32 (или вашу модель).
  5. Нажмите кнопку Загрузка (Upload).
  6. Откройте Монитор порта (Скорость: 115200 бод).
  7. Нажмите кнопку EN (Reset) на плате. В мониторе порта появится текст:
    =================================
    Запуск платы: ESP32
     Мой MAC-адрес: 3C:71:BF:F9:52:3C
     ESP-NOW инициализирован успешно.
     РЕЖИМ: ПРИЕМНИК (Ожидание данных...)
    =================================
        
  8. Скопируйте MAC-адрес. В данном примере это 3C:71:BF:F9:52:3C.

Для работы вам понадобятся две платы (например, одна ESP32 и одна ESP8266, либо две одинаковых).

Шаг 2: Настройка ОТПРАВИТЕЛЯ

  1. Отключите Приемник и подключите к ПК плату, которая будет Отправителем (например, ESP8266).
  2. В коде найдите массив receiverMac и замените его значения на MAC-адрес, полученный на Шаге 1. Важно: замените двоеточия : на запятые , и добавьте префикс 0x к каждому байту. Было: A0:20:A6:1A:D3:86 Стало: uint8_t receiverMac[] = {0x3C, 0x71, 0xBF, 0xF9, 0x52, 0x3C};<br>
  3. Раскомментируйте строку режима: #define IS_SENDER
  4. В меню Инструменты -> Плата выберите вашу плату-отправитель (например, NodeMCU 1.0 / ESP8266).
  5. Нажмите кнопку Загрузка.

Шаг 3: Тестирование связи.

  1. Откройте Монитор порта для Отправителя (ESP8266). Вы увидите:
    =================================
    Запуск платы: ESP8266
     Мой MAC-адрес: A0:20:A6:1A:D3:86
     ESP-NOW инициализирован успешно.
     РЕЖИМ: ОТПРАВИТЕЛЬ
    =================================
        
  2. Подайте питание на плату-Приемник (можно от powerbank или USB-зарядки).
  3. Нажмите кнопку на плате-Отправителе (На ESP8266 это встроенная кнопка FLASH, на ESP32 подключите кнопку между GPIO 4 и GND).
  4. В мониторе порта Отправителя вы увидите: Отправка: ON Доставлено
  5. Если вы подключите монитор порта к Приемнику (или просто посмотрите на плату), вы увидите, как загорелся светодиод, а в порте появится надпись: Прием: LED ON
  6. При повторном нажатии кнопки светодиод погаснет, а в лог отправится статус OFF.

Откройте Монитор порта для Отправителя (ESP8266). Вы увидите

Заключение.

Используя этот универсальный шаблон, вы получаете промышленный стандарт разработки. Директивы препроцессора #ifdef берут на себя всю рутину по адаптации кода, жесткая фиксация Wi-Fi канала исключает потерю пакетов, а аппаратный антидребезг обеспечивает мгновенную и четкую реакцию системы. Этот код готов стать основой для ваших умных выключателей, пультов управления и систем сбора данных.


Дополнительные материалы:

Понравилась статья Беспроводное управление без роутера: Идеальный кроссплатформенный скетч ESP-NOW для ESP32 и ESP8266? Не забудь поделиться с друзьями в соц. сетях.

А также подписаться на наш канал на VK Видео, вступить в группу Вконтакте.

Спасибо за внимание!

Технологии начинаются с простого!

Фотографии к статье

Файлы для скачивания

Код ESP8266 + ESP32 Код ESP8266 + ESP32.ino6 Kb 20 Скачать

Комментарии

Ваше Имя*


Разработка проектов
EasyHMI
Умный Дом