Czech English

ESP-NOW

bezdrátová komunikace

Kromě standardní komunikace pomocí WiFi a Bluetooth implementoval Esprressif jednoduchý komunikační protokol ESP-NOW. Využívá stejný hardware jako WiFi (2,4 GHz), ale je podstatně jednodušší, má menší režii, tedy i spotřebu. Hodí se tedy pro přenos menšího množství dat mezi zařízeními s mikrokontrolery ESP.


K čemu to je?

Vše začalo tím, že jsem pro robota potřeboval realizovat dálkové ovládání. U předchozího robota používám jako řídicí procesor STM32 a k němu připojené dálkové ovládání (přijímač) z RC autíčka. Nechtělo se mi kupovat další dálkové ovládání a místa v robotovi také není nikdy nazbyt.
Sáhl jsem tedy po ESP32 (se kterým už nějaké zkušenosti mám). Už jsem zkoušel realizovat dálkové řízení přes wifi. Funguje to, ale buď je potřeba wifi síť, nebo esp musí jet v režimu AP. Jako ovladač jde použít mobil (tablet), ale je to takové neohrabané.
Proto jsem začal zkoumat esp-now s tím, že bych si ovladač udělal svůj. Informačních zdrojů a příkladů je na internetu nepřeberně, ale z mého pohledu (jako obvykle), vše zbytečně složité pro počáteční pochopení, jak to celé funguje.
Postupně jsem se tím, trošku prokousal a zde prezentuji své výsledky. Je to maximálně zjednodušené (aby to pochopil každý láma), bez nároku na technickou přesnost. Cílem je funkční dálkové ovládání (ne dokonalé ovládnutí této technologie).
Dále bych upozornil, že v sdk od Espressif se podpora ESP-NOW dost mění. Protože používám poměrně starou verzi sdk (razím stále heslo, když to funguje, tak na to nesahej), možná příklady s použitím novější verze budou potřeba trochu upravit. Příklady jsou jen příklady a jsou psány, co nejjednodušeji, aby byla zřejmá funkčnost bez ošetření všech chyb.

První komunikace

Nejjednodušší je jednosměrná komunikace mezi dvěma esp mikrokontrolery. Jeden vysílá a druhý přijímá. V nejjednodušší variantě vysílač používá tzv. broadcast (posílá všem posluchačům) a každý přijímač v dosahu tato data dostává. Komunikace tedy není nijak směrovaná ani zabezpečená.
Zdrojový kód vysílače:

Adresná jednosměrná komunikace

Mírné vylepšení je tom, že vysílací strana nepoužívá broadcast, ale vysílání je určeno jen pro přijímač se zadanou MAC adresou. Výhodou je, že případné další přijímače si těchto dat nevšímají. Druhým benefiten je, že vysílač se dozví, zda byla jeho zpráva doručena. Přijímač po přijetí adresné zprávy automaticky potvrdí její přijetí. Tím vysílací strana ví, zda byla data doručena.
Zdrojový kód vysílače:
Simplex2-Tx.ino
#include <Arduino.h>
#include <esp_now.h>
#include <WiFi.h>

uint8_t destinationAddress[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
//uint8_t destinationAddress[] = {0x94, 0xB5, 0x55, 0x0A, 0x17, 0xA8};

// Callback when data is sent
void OnDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) {
  Serial.print("    Status send: ");
  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;
  }

  // Registrace callback po odeslani dat/potvrzeni doruceni dat
  esp_now_register_send_cb(OnDataSent);
  
  esp_now_peer_info_t peerInfo;
  memcpy(peerInfo.peer_addr, destinationAddress, 6);
  peerInfo.channel = 0;  
  peerInfo.encrypt = false; 

  if (esp_now_add_peer(&peerInfo) != ESP_OK){
    Serial.println("Failed to add peer");
    return;
  } else {
    Serial.println("Add peer ok");
  }
}
 
void loop() {
  char s[64];
  sprintf(s, "Time: %4ld", millis() / 1000);
  esp_err_t result = esp_now_send(destinationAddress, (uint8_t*)s, strlen(s)+1);
  Serial.println(s);

  if (result == ESP_OK) {
    Serial.print("Data sent");
  } else {
    Serial.println("Error sending the data");
  }

  delay(1000);  
}
Zdrojový kód přijímače:

Adresná obousměrná komunikace

Pokud potřebujeme předávat data oběma směry, tak doplníme chybějící části kódu z předchozího příkladu, tj. ve výsledku máme na obou stranách stejný kód, kromě cílové MAC adresy.
Zdrojový kód vysílače:
Duplex-Tx.ino
#include <Arduino.h>
#include <esp_now.h>
#include <WiFi.h>

uint8_t destinationAddress[] = {0x94, 0xB5, 0x55, 0x0A, 0x17, 0xA8};

// Callback volany po prijeti dat
void OnDataRecv(const uint8_t* mac, const uint8_t* incomingData, int len) {
  Serial.print("Data received: ");
  Serial.println((char*)incomingData);
}

// Callback when data is sent
void OnDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) {
  Serial.print("    Status send: ");
  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;
  }

  // Registrace callback po prijeti dat
  esp_now_register_recv_cb(OnDataRecv);

  // Registrace callback po odeslani dat/potvrzeni doruceni dat
  esp_now_register_send_cb(OnDataSent);
  
  esp_now_peer_info_t peerInfo;
  memcpy(peerInfo.peer_addr, destinationAddress, 6);
  peerInfo.channel = 0;  
  peerInfo.encrypt = false; 

  if (esp_now_add_peer(&peerInfo) != ESP_OK){
    Serial.println("Failed to add peer");
    return;
  } else {
    Serial.println("Add peer ok");
  }
}
 
void loop() {
  char s[32];
  sprintf(s, "Received: %4ld", millis() / 1000);
  esp_err_t result = esp_now_send(destinationAddress, (uint8_t*)s, strlen(s));
  
  if (result == ESP_OK) {
    Serial.print("Data sent");
  } else {
    Serial.println("Error sending the data");
  }

  delay(1000);  
}
Zdrojový kód přijímače:
Duplex-Rx.ino
#include <Arduino.h>
#include <esp_now.h>
#include <WiFi.h>

uint8_t destinationAddress[] = {0xCC, 0xDB, 0xA7, 0x96, 0xD9, 0x4C};

// Callback volany po prijeti dat
void OnDataRecv(const uint8_t* mac, const uint8_t* incomingData, int len) {
  Serial.print("Data received: ");
  Serial.println((char*)incomingData);
}
  
// Callback when data is sent
void OnDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) {
  Serial.print("    Status send: ");
  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) {
    return;
  }

  // Registrace callback po prijeti dat
  esp_now_register_recv_cb(OnDataRecv);

  // Registrace callback po odeslani dat/potvrzeni doruceni dat
  esp_now_register_send_cb(OnDataSent);
  
  esp_now_peer_info_t peerInfo;
  memcpy(peerInfo.peer_addr, destinationAddress, 6);
  peerInfo.channel = 0;  
  peerInfo.encrypt = false; 

  if (esp_now_add_peer(&peerInfo) != ESP_OK){
    return;
  }

  Serial.println("Ready");
}
 
void loop() {
  char s[32];
  sprintf(s, "Answer: %4ld", millis() / 1000);
  esp_err_t result = esp_now_send(destinationAddress, (uint8_t*)s, strlen(s));
  
  if (result == ESP_OK) {
    Serial.print("Data sent");
  } else {
    Serial.println("Error sending the data");
  }

  delay(1000);  
}

Automatické "párování"

Přechozí příklad už má jen jeden nedostatek, je nutné znát MAC adresu/y daného zařízení. Pokud realizujeme komunikaci mezi dvěma zařízeními, celkem to nevadí. Ale pokud potřebujeme zaměňovat komunikační zařízení, např. různé přijímače dálkového ovládání s jedním vysílačem, pak je to nepříjemné.
V ukázce je realizováno automatické "napárování" zařízení po zapnutí. Kód je připraven i pro možnost "trvalého" uložení adresy v přijímací straně (bude pak reagovat jen s napárovaným vysílačem).
Průběh párování:
1. Po zapnutí vysílač vysílá broadcast zprávu "PAIR"
2. Pokud přijímací strana zachytí tuto zprávu, uloží si MAC adresu vysílače a odpoví na tuto adresu zprávou "ACK"
3. Vysílač po si obržení potvrzovací zprávy zapamatuje MAC adresu přijímače, který mu odpověděl
4. Vysílač přestane vysílat broadcast a začně vysílat na adresu přijímače
5. Pokud vysílač nedostane informaci o úspěšném doručení zprávy (přijímač je vypnut nebo je mimo dosah), začńe opět vysílat broadcast "PAIR"
6. Pokud přijímací strana po delší dobu nedostane zprávu nebo se mu nepodaří úspěšně odeslat zprávu vysílači (vysílač je vypnut nebo mimo dosah) čeká na opětovné spárování.
Zdrojový kód vysílače:
Full-Tx.ino debug.h espnow-tx.cpp espnow-tx.h
#include "espnow-tx.h"

EspNow espnow;

void setup() {
  Serial.begin(115200);
  espnow.init();
  Serial.println("Ready");
}
 
void loop() {
  char s[32];
  sprintf(s, "%4ld", millis() / 1000);
  espnow.sendData(s);
//  Serial.println(s);

  delay(1000);
}
#ifndef DEBUG_H
#define DEBUG_H

#define DEBUG 

#ifdef DEBUG
  #define DEBUG_BEGIN(...) { Serial.begin(__VA_ARGS__); }
  #define DEBUG_PRINT(...) { Serial.print(__VA_ARGS__); }
  #define DEBUG_PRINTF(...) { Serial.printf(__VA_ARGS__); }
  #define DEBUG_PRINTLN(...) { Serial.println(__VA_ARGS__); }
#else
  #define DEBUG_BEGIN(...) {}
  #define DEBUG_PRINT(...) {}
  #define DEBUG_PRINTF(...) {}
  #define DEBUG_PRINTLN(...) {}
#endif  

#endif
#include "espnow-tx.h"

EspNow* _espnow;

const uint8_t broadcastAddress[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
const char ACK[]  = "ACK";
const char PAIR[] = "PAIR";

void PrintMAC(const uint8_t* addr) {
  DEBUG_PRINTF("%02x:%02x:%02x:%02x:%02x:%02x\n\r", addr[0], addr[1], addr[2], addr[3], addr[4], addr[5]);  
}

// Callback when data is received
void OnDataRecv(const uint8_t* mac, const uint8_t* incomingData, int len) {  
  _espnow->receiveData(mac, incomingData, len);
}

// Callback when data is sent
void OnDataSent(const uint8_t* mac_addr, esp_now_send_status_t s) {
  DEBUG_PRINT("        Send status: ");
  DEBUG_PRINTLN(s == ESP_NOW_SEND_SUCCESS ? "Delivery Success" : "Delivery Fail");
  if (s != ESP_NOW_SEND_SUCCESS) _espnow->status = EspNow::NOT_CONNECTED;
}

void EspNow::init() {
  EEPROM.begin(16);
  EEPROM.readBytes(MAC_ADDR, destinationAddress, MAC_LEN);
  PrintMAC(destinationAddress);

  status = NOT_CONNECTED;
  dataReady = NO_DATA;

  WiFi.mode(WIFI_STA);
  if (esp_now_init() != ESP_OK) {
    return;
  }

  // Registrace callback po prijeti dat
  esp_now_register_recv_cb(esp_now_recv_cb_t(OnDataRecv));
  // Registrace callback po odeslani dat
  esp_now_register_send_cb(OnDataSent);

  addPeer(broadcastAddress);

  _espnow = this;
}
 
void EspNow::receiveData(const uint8_t* mac, const uint8_t* incomingData, int len) {
  if (memcmp(incomingData, ACK, sizeof(ACK)) == 0) { 
    memcpy(destinationAddress, mac, MAC_LEN);
    if (status == NOT_CONNECTED) {
      addPeer(destinationAddress);
      DEBUG_PRINT("Paired to ");
      PrintMAC(destinationAddress);
      status = CONNECTED;
    }
  } else {
    if (status == CONNECTED && memcmp(destinationAddress, mac, MAC_LEN) == 0) {
      memcpy((void*)data, incomingData, (len > MAX_LEN) ? MAX_LEN : len);
      dataReady = DATA_READY;
    }
  }
}

int EspNow::getData(void* payload, int len) {
  if (dataReady == DATA_READY) {
    memcpy(payload, (const void*)data, len);
    dataReady = NO_DATA;
    return 1;
  } else {
    return 0;
  }
}

int EspNow::sendData(void* payload, int len) {
  esp_err_t result;
  if (status == CONNECTED) {
    result = esp_now_send(destinationAddress, (uint8_t*)payload, len);
    DEBUG_PRINT("Send data: ");
    DEBUG_PRINTLN((char*)payload);
  } else {
    result = esp_now_send(broadcastAddress, (uint8_t*)PAIR, sizeof(PAIR));
    DEBUG_PRINTLN("Send broadcast PAIR");
  } 
  if (result == ESP_OK) {
    DEBUG_PRINT("Data sent");
  } else {
    DEBUG_PRINTLN("Error sending the data");
    status = NOT_CONNECTED;
  }  
  return status;
}

int EspNow::sendData(char* s) {
  return sendData((uint8_t*)s, strlen(s));
}  

void EspNow::addPeer(const uint8_t* mac) {
  if (!esp_now_is_peer_exist(mac)) {
    esp_now_peer_info_t peerInfo = {};
    memcpy(peerInfo.peer_addr, mac, MAC_LEN);
    peerInfo.channel = 0;  
    peerInfo.encrypt = false;   
    if (esp_now_add_peer(&peerInfo) != ESP_OK){
      DEBUG_PRINTLN("Failed to add peer");
      return;
    } else {
      DEBUG_PRINTLN("Add peer ok");
    }
  }
}
#ifndef ESPNOW_H
#define ESPNOW_H

#include <Arduino.h>
#include <esp_now.h>
#include <esp_wifi.h>
#include <WiFi.h>
#include <EEPROM.h>
#include "debug.h"

#define MAC_LEN   6
#define MAC_ADDR  0

class EspNow {
  public:
    enum {NOT_CONNECTED = -1, CONNECTED = 0, PAIRING = 1};
    enum {NO_DATA = 0, DATA_READY = 1};
    volatile int     status;

    void init();
    int  sendData(char*);
    int  sendData(void* = NULL, int = 0);
    int  getStatus();
    int  getData(void*, int);
    void receiveData(const uint8_t* mac, const uint8_t* incomingData, int len);
  private:    
    static const int MAX_LEN = 32;
    volatile uint8_t data[MAX_LEN];
    volatile int     dataReady;
    uint8_t          destinationAddress[MAC_LEN];
    void  addPeer(const uint8_t* mac);
};

#endif
Zdrojový kód přijímače:
Full-Rx.ino debug.h espnow-rx.cpp espnow-rx.h
#include "espnow-rx.h"

EspNow espnow;

void setup() {
  Serial.begin(115200);
  espnow.init();
  Serial.println("Ready");
}
 
void loop() {
  char s[32];
  if (espnow.getData(s, 32)) {
    Serial.printf("Received: %s\r\n", s);
  }
}
#ifndef DEBUG_H
#define DEBUG_H

#define DEBUG 

#ifdef DEBUG
  #define DEBUG_BEGIN(...) { Serial.begin(__VA_ARGS__); }
  #define DEBUG_PRINT(...) { Serial.print(__VA_ARGS__); }
  #define DEBUG_PRINTF(...) { Serial.printf(__VA_ARGS__); }
  #define DEBUG_PRINTLN(...) { Serial.println(__VA_ARGS__); }
#else
  #define DEBUG_BEGIN(...) {}
  #define DEBUG_PRINT(...) {}
  #define DEBUG_PRINTF(...) {}
  #define DEBUG_PRINTLN(...) {}
#endif  

#endif
#include "espnow-rx.h"

EspNow* _espnow;

const uint32_t TIMEOUT  = 2000;
const uint8_t broadcastAddress[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
const char ACK[]  = "ACK";
const char PAIR[] = "PAIR";

void PrintMAC(const uint8_t* addr) {
  DEBUG_PRINTF("%02x:%02x:%02x:%02x:%02x:%02x\n\r", addr[0], addr[1], addr[2], addr[3], addr[4], addr[5]);  
}

// Callback volany po prijeti dat
void OnDataRecv(const uint8_t* mac, const uint8_t* incomingData, int len) {  
  _espnow->receiveData(mac, incomingData, len);
}

// Callback when data is sent
void OnDataSent(const uint8_t *mac_addr, esp_now_send_status_t s) {
  DEBUG_PRINT("        Send status: ");
  DEBUG_PRINTLN(s == ESP_NOW_SEND_SUCCESS ? "Delivery Success" : "Delivery Fail");
  if (s != ESP_NOW_SEND_SUCCESS) _espnow->status = EspNow::NOT_CONNECTED;
}

void EspNow::init() {
  EEPROM.begin(16);
  EEPROM.readBytes(MAC_ADDR, destinationAddress, MAC_LEN);
  PrintMAC(destinationAddress);

//  status = NOT_CONNECTED;
  status = PAIRING;
  dataReady = NO_DATA;

  WiFi.mode(WIFI_STA);
  if (esp_now_init() != ESP_OK) {
    return;
  }

  // Registrace callback po prijeti dat
  esp_now_register_recv_cb(esp_now_recv_cb_t(OnDataRecv));
  // Registrace callback po odeslani dat
  esp_now_register_send_cb(OnDataSent);

  _espnow = this;
}
 
void EspNow::receiveData(const uint8_t* mac, const uint8_t* incomingData, int len) {
  if (/*memcmp(esp_now_recv_info.des_addr, broadcastAddress, MAC_LEN) == 0 &&*/ memcmp(incomingData, (uint8_t*)PAIR, sizeof(PAIR)) == 0) { 
    if (status == PAIRING) {
      memcpy(destinationAddress, mac, MAC_LEN);
//      EEPROM.writeBytes(MAC_ADDR, destinationAddress, MAX_LEN);
      status = NOT_CONNECTED;
    }
    if (status == NOT_CONNECTED) {
      if (memcmp(mac, destinationAddress, MAC_LEN) == 0) { 
        addPeer(destinationAddress);

        esp_err_t result = esp_now_send(destinationAddress, (uint8_t*)ACK, sizeof(ACK));
        if (result == ESP_OK) {
          DEBUG_PRINT("Pair ok");
          lastData = millis();
          status = CONNECTED;
        } else {
          DEBUG_PRINTLN("Error sending the data");
        }
      }
    }
  } else {
    if (status == CONNECTED && memcmp(destinationAddress, mac, MAC_LEN) == 0) {
      memcpy((void*)data, incomingData, (len > MAX_LEN) ? MAX_LEN : len);
      dataReady = DATA_READY;
      lastData = millis();
    }
  }
}

int EspNow::getData(void* payload, int len) {
  if (dataReady == DATA_READY) {
    memcpy(payload, (const void*)data, len);
    dataReady = NO_DATA;
    return 1;
  } else {
    if (millis() - lastData > TIMEOUT && status == CONNECTED) {
      status = NOT_CONNECTED;
    }
    return 0;
  }
}

void EspNow::pair() {
  status = PAIRING;
  memset(destinationAddress, 0, MAC_LEN);
}

int EspNow::sendData(void* payload, int len) {
  if (status == CONNECTED) {
    esp_err_t result = esp_now_send(destinationAddress, (uint8_t*)payload, len);
    DEBUG_PRINT("Send data: ");
    DEBUG_PRINTLN((char*)payload);
    if (result == ESP_OK) {
      DEBUG_PRINT("Data sent");
    } else {
      DEBUG_PRINTLN("Error sending the data");
      status = NOT_CONNECTED;
    }  
  } 
  return status;
}

int EspNow::sendData(char* s) {
  return sendData((uint8_t*)s, strlen(s));
}  

void EspNow::addPeer(uint8_t* mac) {
  if (!esp_now_is_peer_exist(mac)) {
    esp_now_peer_info_t peerInfo = {};
    memcpy(peerInfo.peer_addr, mac, MAC_LEN);
    peerInfo.channel = 0;  
    peerInfo.encrypt = false;   
    if (esp_now_add_peer(&peerInfo) != ESP_OK){
      DEBUG_PRINTLN("Failed to add peer");
      return;
    } else {
      DEBUG_PRINTLN("Add peer ok");
    }
  }
}
#ifndef ESPNOW_H
#define ESPNOW_H

#include <Arduino.h>
#include <esp_now.h>
#include <esp_wifi.h>
#include <WiFi.h>
#include <EEPROM.h>
#include "debug.h"

#define MAC_LEN   6
#define MAC_ADDR  0

class EspNow {
  public:
    enum {NOT_CONNECTED = -1, CONNECTED = 0, PAIRING = 1};
    enum {NO_DATA = 0, DATA_READY = 1};
    volatile int     status;

    void init();
    void pair();
    int  sendData(char*);
    int  sendData(void* = NULL, int = 0);
    int  getStatus();
    int  getData(void*, int);
    void receiveData(const uint8_t* mac, const uint8_t* incomingData, int len);
  private:    
    static const int MAX_LEN = 32;
    volatile uint8_t data[MAX_LEN];
    volatile int     dataReady;
    uint8_t   destinationAddress[MAC_LEN];
    uint32_t  lastData;
    void addPeer(uint8_t* mac);
};

#endif

Použitá literatura a odkazy na internetu