Bezdrátová komunikace ESP-Now s ESP32

ESP-Now je komunikační protokol vyvinutý firmou Espressif, který nám umožňuje bezdrátově komunikovat mezi čipy ESP32 či ESP8266. Tento protokol pro svou funkci využívá WiFi na ESP čipech, ale je jednodušší a díky tomu umožňuje rychlejší komunikaci mezi zařízeními.

Po instalaci podpory pro ESP32 si můžete všimnout, že po zvolení ESP32 desky se nám mezi Příklady v Arduino IDE objeví také jednoduché ukázky komunikace přes ESP-Now, kdy jsou sketche rozděleny na 2 strany – Master a Slave, tedy Hlavní a Podřízené zařízení. V těchto příkladech je ale vždy ukázána pouze jednostranná komunikace. Já jsem ale chtěl pro svůj projekt používat ESP-Now obousměrně a pro tyto účely jsem dal dohromady následující kód.

Ukázkový kód nebudu komentovat úplně do detailů, komentáře se nacházejí přímo v kódu.
Kód využívá několika funkcí, které obstarávají jeho správný chod. První funkcí je InitESPNow, která zajišťuje zavolání připravené funkce a kontrolu správné inicializace. Při neúspěšné inicializaci je proveden restart.

void InitESPNow() {
  if (esp_now_init() == ESP_OK) {
    Serial.println("ESPNow Init Success");
  }
  else {
    Serial.println("ESPNow Init Failed");
    // Retry InitESPNow, add a counte and then restart?
    // InitESPNow();
    // or Simply Restart
    ESP.restart();
  }
}

Další funkcí je ScanForSlave, kdy tato funkce hledá ostatní ESP v okolí. Správné ESP je pak to, které v názvu obsahuje řetězec „ESPNOW“ (řádek 55). Pokud tedy nalezneme nějaké zařízení se správným názvem, uložíme si jeho MAC adresu. Tato funkce je zavolána po každém zapnutí či restartu ve fázi setup.

void ScanForSlave() {
  int8_t scanResults = WiFi.scanNetworks();
  //reset slaves
  memset(slaves, 0, sizeof(slaves));
  SlaveCnt = 0;
  Serial.println("");
  if (scanResults == 0) {
    Serial.println("No WiFi devices in AP Mode found");
  } else {
    Serial.print("Found "); Serial.print(scanResults); Serial.println(" devices ");
    for (int i = 0; i < scanResults; ++i) {
      // Print SSID and RSSI for each device found
      String SSID = WiFi.SSID(i);
      int32_t RSSI = WiFi.RSSI(i);
      String BSSIDstr = WiFi.BSSIDstr(i);

      if (PRINTSCANRESULTS) {
        Serial.print(i + 1); Serial.print(": "); Serial.print(SSID); Serial.print(" ["); Serial.print(BSSIDstr); Serial.print("]"); Serial.print(" ("); Serial.print(RSSI); Serial.print(")"); Serial.println("");
      }
      delay(10);
      // Check if the current device starts with `Slave`
      if (SSID.indexOf("ESPNOW") == 0) {
        // SSID of interest
        Serial.print(i + 1); Serial.print(": "); Serial.print(SSID); Serial.print(" ["); Serial.print(BSSIDstr); Serial.print("]"); Serial.print(" ("); Serial.print(RSSI); Serial.print(")"); Serial.println("");
        // Get BSSID => Mac Address of the Slave
        int mac[6];

        if ( 6 == sscanf(BSSIDstr.c_str(), "%02x:%02x:%02x:%02x:%02x:%02x",  &mac[0], &mac[1], &mac[2], &mac[3], &mac[4], &mac[5] ) ) {
          for (int ii = 0; ii < 6; ++ii ) {
            slaves[SlaveCnt].peer_addr[ii] = (uint8_t) mac[ii];
          }
        }
        slaves[SlaveCnt].channel = CHANNEL_MASTER; // pick a channel
        slaves[SlaveCnt].encrypt = 0; // no encryption
        SlaveCnt++;
      }
    }
  }

  if (SlaveCnt > 0) {
    Serial.print(SlaveCnt); Serial.println(" Slave(s) found, processing..");
  } else {
    Serial.println("No Slave Found, trying again.");
  }

  // clean up ram
  WiFi.scanDelete();
}

Funkce manageSlave obstarává kontrolu spojení a párování se zařízeními, které byly uloženy pomocí funkce ScanForSlave. Pro všechny možné varianty je pak vždy vypsána informační hláška po sériové lince.

void manageSlave() {
  if (SlaveCnt > 0) {
    for (int i = 0; i < SlaveCnt; i++) {
      const esp_now_peer_info_t *peer = &slaves[i];
      const uint8_t *peer_addr = slaves[i].peer_addr;
      Serial.print("Processing: ");
      for (int ii = 0; ii < 6; ++ii ) {
        Serial.print((uint8_t) slaves[i].peer_addr[ii], HEX);
        if (ii != 5) Serial.print(":");
      }
      Serial.print(" Status: ");
      // check if the peer exists
      bool exists = esp_now_is_peer_exist(peer_addr);
      if (exists) {
        // Slave already paired.
        Serial.println("Already Paired");
      } else {
        // Slave not paired, attempt pair
        esp_err_t addStatus = esp_now_add_peer(peer);
        if (addStatus == ESP_OK) {
          // Pair success
          Serial.println("Pair success");
        } else if (addStatus == ESP_ERR_ESPNOW_NOT_INIT) {
          // How did we get so far!!
          Serial.println("ESPNOW Not Init");
        } else if (addStatus == ESP_ERR_ESPNOW_ARG) {
          Serial.println("Add Peer - Invalid Argument");
        } else if (addStatus == ESP_ERR_ESPNOW_FULL) {
          Serial.println("Peer list full");
        } else if (addStatus == ESP_ERR_ESPNOW_NO_MEM) {
          Serial.println("Out of memory");
        } else if (addStatus == ESP_ERR_ESPNOW_EXIST) {
          Serial.println("Peer Exists");
        } else {
          Serial.println("Not sure what happened");
        }
        delay(100);
      }
    }
  } else {
    // No slave found to process
    Serial.println("No Slave found to process");
  }
}

Funkce sendData odpovídá svému názvu – tato funkce je používána pro posílání dat z jednoho zařízení do druhého. A jako proměnný element je v ní vždy inkrementována hodnota proměnné pos.

void sendData() {
  pos++;
  sprintf((char *)data, "espnow %lld", pos);
  for (int i = 0; i < SlaveCnt; i++) {
    const uint8_t *peer_addr = slaves[i].peer_addr;
    if (i == 0) { // print only for first slave
      Serial.print("Sending: ");
      Serial.println((char *)data);
    }
    esp_err_t result = esp_now_send(peer_addr, data, DATASIZE);
    Serial.print("Send Status: ");
    if (result == ESP_OK) {
      Serial.println("Success");
    } else if (result == ESP_ERR_ESPNOW_NOT_INIT) {
      // How did we get so far!!
      Serial.println("ESPNOW not Init.");
    } else if (result == ESP_ERR_ESPNOW_ARG) {
      Serial.println("Invalid Argument");
    } else if (result == ESP_ERR_ESPNOW_INTERNAL) {
      Serial.println("Internal Error");
    } else if (result == ESP_ERR_ESPNOW_NO_MEM) {
      Serial.println("ESP_ERR_ESPNOW_NO_MEM");
    } else if (result == ESP_ERR_ESPNOW_NOT_FOUND) {
      Serial.println("Peer not found.");
    } else {
      Serial.println("Not sure what happened");
    }
    delay(100);
  }
}

Další dvě funkce OnDataSent a OnDataRecv jsou použity jako takzvané callbacky, tedy funkce, které jsou zavolané při různých událostech. Konkrétně se jedná o funkci volanou při odesílání (OnDataSent) a při příjmu (OnDataRecv). A v obou případech budou opět vytištěny informace o stavu dané operace po sériové lince.

void OnDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) {
  char macStr[18];
  unsigned long realTime;
  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("Last Packet Sent to: "); Serial.println(macStr);
  Serial.print("Last Packet Send Status: "); Serial.println(status == ESP_NOW_SEND_SUCCESS ? "Delivery Success" : "Delivery Fail");
  realTime = millis();
  Serial.print("Send delay: "); Serial.print(realTime - lastSentTime); Serial.println(" ms.");
  lastSentTime = realTime;
}
void OnDataRecv(const uint8_t *mac_addr, const uint8_t *data, int data_len) {
  char macStr[18];
  unsigned long realTime;
  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("ttLast Packet Recv from: "); Serial.println(macStr);
  Serial.print("ttLast Packet Recv Data: "); Serial.println((char *)data);
  Serial.println("");
  realTime = millis();
  Serial.print("Recv delay: "); Serial.print(realTime - lastRecvTime); Serial.println(" ms.");
  lastRecvTime = realTime;
}

Poslední uživatelská funkce configDeviceAP slouží pro úvodní nastavení jména a hesla pro obě zařízení. Jak si můžete všimnout, jméno zařízení se skládá vždy z názvu „ESPNOW“ a MAC adresy WiFi rozhraní daného čipu. Heslo je pak nastaveno na jednoduchý řetězec, ale pro komunikaci ESP-Now nemá žádný užitek.

void configDeviceAP() {
  String Prefix = "ESPNOW:";
  String Mac = WiFi.macAddress();
  String SSID = Prefix + Mac;
  String Password = "123456789";
  bool result = WiFi.softAP(SSID.c_str(), Password.c_str(), CHANNEL_SLAVE, 0);
  if (!result) {
    Serial.println("AP Config failed.");
  } else {
    Serial.println("AP Config Success. Broadcasting with AP: " + String(SSID));
  }
}

Ve funkci setup provedeme vždy zahájení komunikace po sériové lince a nastavení WiFi na kombinovaný AP+STA mód. Poté zavoláme funkci pro nakonfigurování WiFi rozhraní a poté inicializaci ESP-Now. Následně zaregistrujeme oba zmíněné callbacky a provedeme hledání Slave zařízení.

void setup() {
  Serial.begin(115200);
  //Set device in AP+STA mode to begin with
  WiFi.mode(WIFI_AP_STA);
  // configure device AP mode
  configDeviceAP();
  // This is the mac address of the Master in Station Mode
  Serial.print("STA MAC: "); Serial.println(WiFi.macAddress());
  Serial.print("AP MAC: "); Serial.println(WiFi.softAPmacAddress());
  // Init ESPNow with a fallback logic
  InitESPNow();
  // Once ESPNow is successfully Init, we will register callbacks
  // get the status of Sent packet
  esp_now_register_send_cb(OnDataSent);
  // get the status of Trasnmitted packet
  esp_now_register_recv_cb(OnDataRecv);
  // Scan for slave
  ScanForSlave();
}

V nekonečné smyčce loop zkontrolujeme, jestli jsme už ve funkci setup objevili nějaké okolní Slave zařízení. Pokud ano, provedeme jeho párování a odešleme mu data. Pokud bychom žádné zařízení neobjevili, provedeme nové skenování. A před novým během smyčky loop počkáme 2 sekundy.

void loop() {
  // If Slave is found, it would be populate in `slave` variable
  // We will check if `slave` is defined and then we proceed further
  if (SlaveCnt > 0) { // check if slave channel is defined
    // `slave` is defined
    // Add slave as peer if it has not been added already
    manageSlave();
    // pair success or already paired
    // Send data to device
    sendData();
  }
  else {
    // No slave found to process
    // Scan for slave
    ScanForSlave();
  }
  // wait for 2 seconds to run the logic again
  delay(2000);
}

Celý ukázkový kód je zde:

// ESPNOW - Basic communication - Bidirectional
// navody.dratek.cz

#include <esp_now.h>
#include <WiFi.h>

// Global copy of slave
#define NUMSLAVES 20
esp_now_peer_info_t slaves[NUMSLAVES] = {};
int SlaveCnt = 0;
unsigned long lastSentTime = 0;
unsigned long lastRecvTime = 0;

#define CHANNEL_MASTER 3
#define CHANNEL_SLAVE 1
#define PRINTSCANRESULTS 0
#define DATASIZE 48

// Init ESP Now with fallback
void InitESPNow() {
  if (esp_now_init() == ESP_OK) {
    Serial.println("ESPNow Init Success");
  }
  else {
    Serial.println("ESPNow Init Failed");
    // Retry InitESPNow, add a counte and then restart?
    // InitESPNow();
    // or Simply Restart
    ESP.restart();
  }
}

// Scan for slaves in AP mode
void ScanForSlave() {
  int8_t scanResults = WiFi.scanNetworks();
  //reset slaves
  memset(slaves, 0, sizeof(slaves));
  SlaveCnt = 0;
  Serial.println("");
  if (scanResults == 0) {
    Serial.println("No WiFi devices in AP Mode found");
  } else {
    Serial.print("Found "); Serial.print(scanResults); Serial.println(" devices ");
    for (int i = 0; i < scanResults; ++i) {
      // Print SSID and RSSI for each device found
      String SSID = WiFi.SSID(i);
      int32_t RSSI = WiFi.RSSI(i);
      String BSSIDstr = WiFi.BSSIDstr(i);

      if (PRINTSCANRESULTS) {
        Serial.print(i + 1); Serial.print(": "); Serial.print(SSID); Serial.print(" ["); Serial.print(BSSIDstr); Serial.print("]"); Serial.print(" ("); Serial.print(RSSI); Serial.print(")"); Serial.println("");
      }
      delay(10);
      // Check if the current device starts with `Slave`
      if (SSID.indexOf("ESPNOW") == 0) {
        // SSID of interest
        Serial.print(i + 1); Serial.print(": "); Serial.print(SSID); Serial.print(" ["); Serial.print(BSSIDstr); Serial.print("]"); Serial.print(" ("); Serial.print(RSSI); Serial.print(")"); Serial.println("");
        // Get BSSID => Mac Address of the Slave
        int mac[6];

        if ( 6 == sscanf(BSSIDstr.c_str(), "%02x:%02x:%02x:%02x:%02x:%02x",  &mac[0], &mac[1], &mac[2], &mac[3], &mac[4], &mac[5] ) ) {
          for (int ii = 0; ii < 6; ++ii ) {
            slaves[SlaveCnt].peer_addr[ii] = (uint8_t) mac[ii];
          }
        }
        slaves[SlaveCnt].channel = CHANNEL_MASTER; // pick a channel
        slaves[SlaveCnt].encrypt = 0; // no encryption
        SlaveCnt++;
      }
    }
  }

  if (SlaveCnt > 0) {
    Serial.print(SlaveCnt); Serial.println(" Slave(s) found, processing..");
  } else {
    Serial.println("No Slave Found, trying again.");
  }

  // clean up ram
  WiFi.scanDelete();
}

// Check if the slave is already paired with the master.
// If not, pair the slave with master
void manageSlave() {
  if (SlaveCnt > 0) {
    for (int i = 0; i < SlaveCnt; i++) {
      const esp_now_peer_info_t *peer = &slaves[i];
      const uint8_t *peer_addr = slaves[i].peer_addr;
      Serial.print("Processing: ");
      for (int ii = 0; ii < 6; ++ii ) {
        Serial.print((uint8_t) slaves[i].peer_addr[ii], HEX);
        if (ii != 5) Serial.print(":");
      }
      Serial.print(" Status: ");
      // check if the peer exists
      bool exists = esp_now_is_peer_exist(peer_addr);
      if (exists) {
        // Slave already paired.
        Serial.println("Already Paired");
      } else {
        // Slave not paired, attempt pair
        esp_err_t addStatus = esp_now_add_peer(peer);
        if (addStatus == ESP_OK) {
          // Pair success
          Serial.println("Pair success");
        } else if (addStatus == ESP_ERR_ESPNOW_NOT_INIT) {
          // How did we get so far!!
          Serial.println("ESPNOW Not Init");
        } else if (addStatus == ESP_ERR_ESPNOW_ARG) {
          Serial.println("Add Peer - Invalid Argument");
        } else if (addStatus == ESP_ERR_ESPNOW_FULL) {
          Serial.println("Peer list full");
        } else if (addStatus == ESP_ERR_ESPNOW_NO_MEM) {
          Serial.println("Out of memory");
        } else if (addStatus == ESP_ERR_ESPNOW_EXIST) {
          Serial.println("Peer Exists");
        } else {
          Serial.println("Not sure what happened");
        }
        delay(100);
      }
    }
  } else {
    // No slave found to process
    Serial.println("No Slave found to process");
  }
}

uint8_t data[DATASIZE];
uint64_t pos = 0;

// send data
void sendData() {
  pos++;
  sprintf((char *)data, "espnow %lld", pos);
  for (int i = 0; i < SlaveCnt; i++) {
    const uint8_t *peer_addr = slaves[i].peer_addr;
    if (i == 0) { // print only for first slave
      Serial.print("Sending: ");
      Serial.println((char *)data);
    }
    esp_err_t result = esp_now_send(peer_addr, data, DATASIZE);
    Serial.print("Send Status: ");
    if (result == ESP_OK) {
      Serial.println("Success");
    } else if (result == ESP_ERR_ESPNOW_NOT_INIT) {
      // How did we get so far!!
      Serial.println("ESPNOW not Init.");
    } else if (result == ESP_ERR_ESPNOW_ARG) {
      Serial.println("Invalid Argument");
    } else if (result == ESP_ERR_ESPNOW_INTERNAL) {
      Serial.println("Internal Error");
    } else if (result == ESP_ERR_ESPNOW_NO_MEM) {
      Serial.println("ESP_ERR_ESPNOW_NO_MEM");
    } else if (result == ESP_ERR_ESPNOW_NOT_FOUND) {
      Serial.println("Peer not found.");
    } else {
      Serial.println("Not sure what happened");
    }
    delay(100);
  }
}

// callback when data is sent from Master to Slave
void OnDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) {
  char macStr[18];
  unsigned long realTime;
  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("Last Packet Sent to: "); Serial.println(macStr);
  Serial.print("Last Packet Send Status: "); Serial.println(status == ESP_NOW_SEND_SUCCESS ? "Delivery Success" : "Delivery Fail");
  realTime = millis();
  Serial.print("Send delay: "); Serial.print(realTime - lastSentTime); Serial.println(" ms.");
  lastSentTime = realTime;
}

// callback when data is recv from Master
void OnDataRecv(const uint8_t *mac_addr, const uint8_t *data, int data_len) {
  char macStr[18];
  unsigned long realTime;
  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("ttLast Packet Recv from: "); Serial.println(macStr);
  Serial.print("ttLast Packet Recv Data: "); Serial.println((char *)data);
  Serial.println("");
  realTime = millis();
  Serial.print("Recv delay: "); Serial.print(realTime - lastRecvTime); Serial.println(" ms.");
  lastRecvTime = realTime;
}

// config AP SSID
void configDeviceAP() {
  String Prefix = "ESPNOW:";
  String Mac = WiFi.macAddress();
  String SSID = Prefix + Mac;
  String Password = "123456789";
  bool result = WiFi.softAP(SSID.c_str(), Password.c_str(), CHANNEL_SLAVE, 0);
  if (!result) {
    Serial.println("AP Config failed.");
  } else {
    Serial.println("AP Config Success. Broadcasting with AP: " + String(SSID));
  }
}

void setup() {
  Serial.begin(115200);
  //Set device in AP+STA mode to begin with
  WiFi.mode(WIFI_AP_STA);
  // configure device AP mode
  configDeviceAP();
  // This is the mac address of the Master in Station Mode
  Serial.print("STA MAC: "); Serial.println(WiFi.macAddress());
  Serial.print("AP MAC: "); Serial.println(WiFi.softAPmacAddress());
  // Init ESPNow with a fallback logic
  InitESPNow();
  // Once ESPNow is successfully Init, we will register callbacks
  // get the status of Sent packet
  esp_now_register_send_cb(OnDataSent);
  // get the status of Trasnmitted packet
  esp_now_register_recv_cb(OnDataRecv);
  // Scan for slave
  ScanForSlave();
}

void loop() {
  // If Slave is found, it would be populate in `slave` variable
  // We will check if `slave` is defined and then we proceed further
  if (SlaveCnt > 0) { // check if slave channel is defined
    // `slave` is defined
    // Add slave as peer if it has not been added already
    manageSlave();
    // pair success or already paired
    // Send data to device
    sendData();
  }
  else {
    // No slave found to process
    // Scan for slave
    ScanForSlave();
  }
  // wait for 2 seconds to run the logic again
  delay(2000);
}

Po nahrání uvedeného ukázkového kódu do dvou ESP32 desek můžeme pozorovat například tento výsledek.

Processing: 30:AE:A4:25:6D:31 Status: Already Paired
Sending: espnow 5
Send Status: Success
Last Packet Sent to: 30:ae:a4:25:6d:31
Last Packet Send Status: Delivery Success
Send delay: 2100 ms.
		Last Packet Recv from: 30:ae:a4:25:6d:30
		Last Packet Recv Data: espnow 4

Recv delay: 2098 ms.

ESP-Now komunikace je velice zajímavou alternativou k externím komunikačním čipům pro zasílání dat mezi dvěma Arduiny. Vzhledem k tomu, že WiFi čip mají všechny ESP už vestavěný, můžeme ho takto využít a nemusíme připojovat nic dalšího. A z vlastních zkušeností mohu říci, že tento protokol je velmi spolehlivý a je skvělé, že už v základu obsahuje informace o probíhající komunikaci. Pokud se Vám tedy například při delší vzdálenosti začnou ztrácet informace, získáte i poměrně přesnou chybovou hlášku o typu chyby.
A co se týká zmíněné podpory ESP-Now pro ESP8266, tam bohužel nejsou příklady dodávané v IDE, ale doporučuji například tento repozitář.

Seznam použitých komponent:
https://dratek.cz/arduino/1581-esp-32s-esp32-development-board-2-4ghz-dual-mode-wifi-bluetooth-antenna-module-1493028819.html

Další podobné články