ESP-NOW

Über den Beitrag

In meinem Beitrag WLAN mit ESP8266 und ESP32 hatte ich gezeigt, wie ihr ESP-Boards über WLAN miteinander kommunizieren lasst und wie ihr sie über den Browser eures PCs oder Smartphones steuert. Wenn ihr jedoch einfach nur Daten zwischen zwei oder mehreren ESP-Boards austauschen wollt, dann gibt es eine einfachere Methode und die heißt ESP-NOW.

Ich werde schrittweise erklären, wie ihr eure ESP32- oder ESP8266-basierten Boards mit ESP-NOW als Receiver, Transmitter oder Transceiver einsetzt. Dabei verwende ich primär den ESP32 als Anschauungsobjekt. Zum Schluss zeige ich aber auch, wie ihr den Code für ESP8266-Boards modifiziert.

Folgendes kommt auf euch zu:

Einführung / Vorbereitungen (ESP32 und ESP8266)

ESP-NOW ist ein Protokoll, mit dem ihr Nachrichten von bis zu 250 Bytes zwischen bis zu zwanzig ESP32- oder ESP8266-Boards austauschen könnt. ESP-NOW ist sehr flexibel. Es erlaubt euch, jedes Board als Transmitter, Receiver oder Transceiver einzurichten und Nachrichten an einzelne oder mehrere Mitglieder eures Netzes zu senden. Dabei ist es auch problemlos möglich, ESP32- und ESP8266-Boards zu mischen.

Um ESP-NOW nutzen zu können, müsst ihr nichts zusätzlich installieren. Die benötigten Bibliotheken sind Teil der „Standardausstattung“ der ESP32- und ESP8266-Pakete.  

MAC-Adresse ermitteln

Die Mitglieder eines Netzwerkes müssen eindeutig identifizierbar sein. Bei ESP-NOW wird dazu die MAC-Adresse eurer ESP-Boards verwendet, die ihr mit dem folgenden kleinen Sketch auslesen könnt:

#include <WiFi.h> // for ESP32 boards
//#include <ESP8266WiFi.h> // for ESP8266 boards

void setup(){
  Serial.begin(115200);
  // delay(1000); // uncomment if your serial monitor is empty
  Serial.print("MAC-Address: ");
  Serial.println(WiFi.macAddress());
}
 
void loop(){}

Ein paar Erklärungen zum Sketch

Um die WiFi-Funktionen nutzen zu können, müsst ihr für ESP32-basierte Boards WiFi.h einbinden. Für ESP8266-basierte Boards bindet ihr ESP8266WiFi.h ein.

Die MAC-Adresse besteht aus sechs Bytes. Üblicherweise werden die Bytes im Hexadezimalsystem angegeben und durch Doppelpunkte getrennt, also z.B. 94:3C:C6:33:68:98.

Ihr müsst also von jedem Board, das an eurem ESP-NOW Netzwerk teilnehmen soll, die MAC-Adresse ermitteln und aufschreiben. Es empfiehlt sich, die Boards in irgendeiner Weise zu kennzeichnen, um nicht durcheinanderzukommen.

Hinweis: Wenn ihr eure Sketche auf ESP-Boards ladet, dann kann es passieren, dass die ersten Serial.print() Anweisungen beim ersten Programmdurchlauf „verschluckt“ werden. Ein while(!Serial) nach Serial.begin(), das für bestimmte Boards empfohlen wird, beseitigt das Problem (sofern ihr es habt) nicht. Deswegen findet ihr in meinen Sketchen ein auskommentiertes delay(), das ihr ggf. entkommentieren könnt. 

MAC-Adresse ändern

Falls es euch zu mühsam ist, alle MAC-Adressen auszulesen, dann könnt auch einfach eine neue festlegen. Die folgenden beiden Sketche zeigen, wie ihr das für den ESP32 und den ESP8266 bewerkstelligt.

ESP32

#include <WiFi.h>
#include <esp_wifi.h>

const uint8_t newMacAddress[] = {0x94, 0x3C, 0xC6, 0x33, 0x68, 0x01}; // customize as you wish  

void setup(){
      Serial.begin(115200);
      //delay(1000); // uncomment if your serial monitor is empty
      Serial.print("Default ESP Board MAC Address: ");
      Serial.println(WiFi.macAddress());
      
      WiFi.mode(WIFI_STA);
      esp_wifi_set_mac(WIFI_IF_STA, newMacAddress);
      
      Serial.print("New ESP Board MAC Address:     ");
      Serial.println(WiFi.macAddress());     
}
 
void loop(){}

Ein paar Erklärungen:

  • Wir verwenden in diesem Beispiel eine Funktion aus der ESP-API (Application Programming Interface). Diese Funktionen erkennt ihr daran, dass sie mit „esp_“ beginnen. Um Zugriff darauf zu haben, müsst ihr die entsprechenden Bibliotheken einbinden. Hier ist das esp_wifi.h.
  • Die MAC-Adresse wird als Array (uint8_t) definiert.
  • Ein ESP-Board kann sich entweder in ein Netzwerk einklinken (Station Modus) oder selbst als Zugangspunkt dienen (Access Point Modus). Wir stellen für ESP-NOW mit WiFi.mode(WIFI_STA) den Station Modus ein.
  • Mit esp_wifi_set_mac() legt ihr die MAC-Adresse fest. Die Funktion erwartet zwei Argumente. Das erste ist die verwendete Schnittstelle (Interface). Da wir im Station Modus arbeiten, wählen wir das Station Interface ( = WIFI_IF_STA). Das zweite Argument ist die neue MAC-Adresse.
    • Die Dokumentation zur esp_wifi_set_mac() Funktion findet ihr hier.
  • Das Bit 0 des ersten Byte der MAC-Adresse muss 0 sein, oder einfacher ausgedrückt: Das erste Byte muss eine gerade Zahl sein. Sonst gibt es diesbezüglich meines Wissens keine weitere Einschränkung.
  • Die Änderung der MAC-Adresse durch esp_wifi_set_mac() ist nicht permanenter Natur, d.h. der ESP „vergisst“ die Einstellung bei einem Reset.

Ich ändere die MAC-Adresse nur in einem der Beispielsketche. Wenn ihr die Option in den anderen Beispielen nutzen wollt, müsst ihr die Sketche entsprechend erweitern.

ESP8266

Und so sieht das Gegenstück für den ESP8266 aus:

#include <ESP8266WiFi.h>

uint8_t newMacAddress[] = {0x94, 0x3C, 0xC6, 0x33, 0x68, 0x01}; // customize as you wish

void setup(){
      Serial.begin(115200);
      // delay(1000); // uncomment if your serial monitor is empty
      Serial.print("Default ESP Board MAC Address: ");
      Serial.println(WiFi.macAddress());
      
      WiFi.mode(WIFI_STA);
      wifi_set_macaddr(STATION_IF, newMacAddress);
      
      Serial.print("New ESP Board MAC Address:     ");
      Serial.println(WiFi.macAddress());     
}
 
void loop(){}

Übersicht der wichtigsten ESP-NOW Funktionen (ESP32 und ESP8266)

Um eure Boards per ESP-NOW kommunizieren zu lassen, benötigt ihr nur wenige Funktionen, die ich hier in einer Übersicht zusammengefasst habe:

Die wichtigsten ESP-NOW Funktionen im Überblick
Die wichtigsten ESP-NOW Funktionen im Überblick

Ein vollständige Auflistung der Funktionen findet ihr hier in der API-Doku für ESP-NOW.

Ein Transmitter, ein Receiver – Bare Minimum (ESP32)

Da ESP-NOW für Einsteiger ein wenig verwirrend sein kann, starten wir mit einem Minimalbeispiel, auf dem wir dann weiter aufbauen. In diesem Beispiel übernimmt ein Board die Rolle des Transmitters, das andere dient als Receiver. Die Nachricht besteht nur aus Text.

Und noch einmal der Hinweis: Der Code ist ESP32-spezifisch. Zur Übertragung auf den ESP8266 komme ich am Ende des Beitrages.

Transmitter

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

uint8_t receiverAddress[] = {0x94, 0x3C, 0xC6, 0x33, 0x68, 0x98};
esp_now_peer_info_t peerInfo;

void setup(){
    Serial.begin(115200);
    // delay(1000); // uncomment if your serial monitor is empty
    WiFi.mode(WIFI_STA);
    
    esp_now_init();
    
    memcpy(peerInfo.peer_addr, receiverAddress, 6);
    peerInfo.channel = 0;
    peerInfo.encrypt = false;

    esp_now_add_peer(&peerInfo);
}
 
void loop(){
    char message[] = "Hi, this is a message from the transmitting ESP";
    esp_now_send(receiverAddress, (uint8_t *) message, sizeof(message));
    delay(5000);
}

Erklärungen:

  • Mit #include<esp_now.h> binden wir die ESP-NOW Bibliothek ein.
  • peerInfo enthält Informationen über das Modul, mit dem wir kommunizieren wollen. Dabei handelt es sich um eine Struktur vom Typ esp_now_peer_info_t. Die Dokumentation findet ihr hier. Für uns sind die folgenden Elemente der Struktur relevant:
    • peer_addr enthält die MAC-Adresse des Empfängermoduls.
      • Eine Zuordnung peerInfo.peer_addr = receiverAddress ist nicht möglich, da man Arrays nicht so ohne Weiteres kopieren kann. Deswegen der Umweg über memcpy().
    • channel ist der WLAN-Kanal. Ihr könnt die Kanäle 1 – 13 wählen. Bei „0“ greift die Voreinstellung, nämlich „1“. Wie ihr den WLAN-Kanal eures Moduls ändert, beschreibe ich am Ende des Beitrages.
    • encrypt gibt an, ob ihr die Nachricht verschlüsseln wollt. Darauf gehe ich nicht ein.
  • esp_now_init() initialisiert ESP-NOW.
  • esp_now_add_peer(&peerInfo) fügt das ESP-Modul, das ihr zuvor mit peerInfo definiert habt, zum Netzwerk des aktuellen Moduls hinzu. 
  • esp_now_send() sendet eine Nachricht. Ihr übergebt der Funktion die MAC-Adresse des Empfängers, die Nachricht und ihre Länge. Die Nachricht ist vom Datentyp uint8_t und wird in esp_now_send() als Zeiger übergeben. Entsprechend muss message noch explizit mittels (uint8_t *) umgewandelt werden.

Receiver

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

void messageReceived(const uint8_t* macAddr, const uint8_t* incomingData, int len){
    for(int i=0; i<len; i++){
        Serial.print((char)incomingData[i]);
    }
    Serial.println();
}

void setup(){
    Serial.begin(115200);
    // delay(1000); // uncomment if your serial monitor is empty
    WiFi.mode(WIFI_STA);
    
    esp_now_init();
    esp_now_register_recv_cb(messageReceived);
}
 
void loop(){}

Auch hierzu einige Erklärungen:

  • Um Nachrichten nur zu empfangen, müsst ihr das sendende Modul nicht als „Peer“ hinzufügen.
  • Die einzige neue Funktion ist esp_now_register_recv_cb(). Mit ihr meldet ihr eine Funktion an (hier: messageReceived), die automatisch aufgerufen wird, wenn eine Nachricht eingeht. Das „cb“ seht dabei für „call back“. Da die Funktion automatisch aufgerufen wird, kann loop() leer bleiben. Das Prinzip kennt ihr von Interrupt Service Routinen.
  • Die Parameter der aufzurufenden Funktion sind die MAC-Adresse des Absenders, die Nachricht selbst und die Länge der Nachricht.
  • Da wir einen Text verschickt haben, die empfangene Nachricht aber vom Datentyp uint8_t ist, müssen wir sie explizit in char zurück umwandeln. 

Ausgabe auf dem seriellen Monitor

Wenn alles gut gegangen ist, dann sollte die Nachricht des Transmitters alle fünf Sekunden erneut auf dem seriellen Monitor des Empfängermoduls angezeigt werden.

Ausgabe von receiver_bare_minimum.ino

Ein Transmitter, ein Receiver – Advanced (ESP32)

Die „Bare Minimum“ Sketche sind noch nicht besonders komfortabel. Es gibt keine Fehlermeldungen, wenn etwas schiefläuft, keine Sendebestätigung und keine Anzeige der MAC-Adresse des Senders. Außerdem haben wir bisher nur einfachen Text gesendet. In der Praxis wird man eher Daten, wie beispielsweise Sensorwerte, übermitteln wollen. Diese Defizite werden wir jetzt beseitigen.

Transmittersketch

Zunächst zum Transmitter:

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

uint8_t receiverAddress[] = {0x94, 0x3C, 0xC6, 0x33, 0x68, 0x98};
esp_now_peer_info_t peerInfo;

typedef struct message {
    char text[64];
    int intVal;
    float floatVal;
} message;

message myMessage; 

void messageSent(const uint8_t *macAddr, esp_now_send_status_t status) {
    Serial.print("Send status: ");
    if(status == ESP_NOW_SEND_SUCCESS){
        Serial.println("Success");
    }
    else{
        Serial.println("Error");
    }
}

void setup(){
    Serial.begin(115200);
    // delay(1000); // uncomment if your serial monitor is empty
    WiFi.mode(WIFI_STA);
    
    if (esp_now_init() == ESP_OK) {
        Serial.println("ESPNow Init success");
    }
    else {
        Serial.println("ESPNow Init fail");
        return;
    }
    
    esp_now_register_send_cb(messageSent);   

    memcpy(peerInfo.peer_addr, receiverAddress, 6);
    peerInfo.channel = 0;
    peerInfo.encrypt = false;

    if (esp_now_add_peer(&peerInfo) != ESP_OK) {
        Serial.println("Failed to add peer");
        return;
    }
}
 
void loop(){
    char textMsg[] = "Hi Receiver, here's my data for you: ";
    memcpy(&myMessage.text, textMsg, sizeof(textMsg));
    myMessage.intVal = 4242;
    myMessage.floatVal = 42.42;
    esp_err_t result = esp_now_send(receiverAddress, (uint8_t *) &myMessage, sizeof(myMessage));
    if (result != ESP_OK) {
        Serial.println("Sending error");
    }
    delay(5000);
}

 

Was ist anders im Vergleich zum „Bare Minimum“?

  • Als Format für unsere Nachricht verwenden wir eine Struktur (message). Damit sind wir sehr flexibel hinsichtlich der in der Nachricht enthaltenen Datentypen. In diesem Beispiel besteht die Struktur aus einem Character Array, einem Integer und einem Float. Für das Character Arrays müssen wir die maximal zu erwartende Länge spezifizieren.
  • Strukturen wie message sind abgespeckte Klassen. Mit message myMessage; erzeugt ihr daraus das Objekt myMessage.
  • Der Rückgabewert von esp_now_init() verrät uns, ob der Vorgang fehlerfrei abgeschlossen wurde. 
  • In gleicher Weise nutzen wir den Rückgabewert von esp_now_add_peer(). Wichtig: die Funktion prüft nicht, ob der Peer wirklich vorhanden bzw. erreichbar ist. Eine Fehlermeldung würdet ihr beispielsweise bekommen, wenn die maximale Anzahl Peers überschritten ist. Weitere Informationen findet ihr in der API-Doku.
  • Um das Array textMsg in das Array myMessage.text zu kopieren, müsst ihr aus den schon zuvor genannten Gründen memcpy() nutzen. Die anderen Elemente könnt ihr direkt zuordnen. 
  • Mit esp_now_register_send_cb(messageSent); registrieren wir die Funktion messageSent, die aufgerufen wird, wenn eine Nachricht versendet wurde. Auch hier sind die Parameter wieder vorgegeben.
  • Die Prüfung von status in der Funktion messageSent und die Prüfung von result als Rückgabewert von esp_now_send() mag als unnötige Verdopplung erscheinen, allerdings werden jeweils andere Kriterien geprüft. Für Details schaut in die API-Doku.

Receiversketch

Auch der Receiversketch lässt sich komfortabler gestalten:

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

typedef struct message {
    char text[64];
    int intVal;
    float floatVal;
} message;

message myMessage;

void messageReceived(const uint8_t* macAddr, const uint8_t* incomingData, int len){
    memcpy(&myMessage, incomingData, sizeof(myMessage));
    Serial.printf("Transmitter MAC Address: %02X:%02X:%02X:%02X:%02X:%02X \n\r", 
            macAddr[0], macAddr[1], macAddr[2], macAddr[3], macAddr[4], macAddr[5]);    
    Serial.print("Message: ");
    Serial.println(myMessage.text);
    Serial.print("Integer Value: ");
    Serial.println(myMessage.intVal);
    Serial.print("Float Value: ");
    Serial.println(myMessage.floatVal);
    Serial.println();
}

void setup(){
    Serial.begin(115200);
    // delay(1000); // uncomment if your serial monitor is empty
    WiFi.mode(WIFI_STA);
    
    if (esp_now_init() == ESP_OK) {
        Serial.println("ESPNow Init success");
    }
    else {
        Serial.println("ESPNow Init fail");
        return;
    }

    esp_now_register_recv_cb(messageReceived);
}
 
void loop(){}

 

Was ist hier anders im Vergleich zum „Bare Minimum“Sketch?

  • Wir implementieren dieselbe Struktur message wie im Transmittersketch.
  • Die eingehende Nachricht kopieren wir mit memcpy() in myMessage und können dann bequem auf die Elemente der Struktur myMessage zugreifen.
  • Wir lassen uns die MAC-Adresse des Transmitters anzeigen. Für die Formatierung (Hex-Zahlen, Doppelpunkte) sorgt die printf() Funktion.  

Ausgabe receiver_basic.ino

Ausgabe von receiver_basic.ino
Ausgabe von receiver_basic.ino

Mehrere Transmitter, ein Receiver (ESP32)

In der Praxis setzt man häufig mehrere Transmitter und einen Receiver ein. Ein typisches Anwendungsbeispiel wäre eine Wetterstation mit Sensoren an unterschiedlichen Orten, die ihre Daten an eine zentrale Station übermitteln.

Für diese Konfiguration benötigt ihr keine zusätzlichen ESP-NOW Funktionen. Die einzige Herausforderung ist die Zuordnung der eingehenden Daten zu den Transmittern. Naheliegend wäre ein Abgleich der Transmitter MAC-Adresse mit einer im Receiver-Sketch hinterlegten Liste. Allerdings ist eine Identitätsprüfung von Arrays recht rechenintensiv. Alternativ könnte die Nachricht des Transmitters einen Identifier enthalten.  

Ich habe mich für eine andere Methode entschieden. Alle am Netzwerk beteiligten ESP-Module bekommen eine neue MAC-Adresse. Dabei unterscheiden sich die MAC-Adressen lediglich im letzten Byte, das zugleich als Nummerierung dient. In meinem Beispiel kommen drei Transmitter zum Einsatz und dafür habe ich die folgenden MAC-Adressen vergeben:

  • Transmitter 0 Adresse = 94:3C:C6:33:68:00,
  • Transmitter 1 Adresse = 94:3C:C6:33:68:01,
  • Transmitter 2 Adresse = 94:3C:C6:33:68:02,
  • Receiver Adresse = 94:3C:C6:33:68:05

Transmitter Example

Hier der Sketch für den Transmitter 0:

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

uint8_t receiverAddress[] = {0x94, 0x3C, 0xC6, 0x33, 0x68, 0x05};
uint8_t myAddress[] = {0x94, 0x3C, 0xC6, 0x33, 0x68, 0x00};
esp_now_peer_info_t peerInfo;

typedef struct data {
    int humidity;
    float temperature;
} data;

data myMessage; 

void messageSent(const uint8_t *macAddr, esp_now_send_status_t status) {
    Serial.print("Send status: ");
    if(status == ESP_NOW_SEND_SUCCESS){
        Serial.println("Success");
    }
    else{
        Serial.println("Error");
    }
}

void setup(){
    Serial.begin(115200);
    delay(1000);
    WiFi.mode(WIFI_STA);
    esp_wifi_set_mac(WIFI_IF_STA, myAddress);
    Serial.print("New ESP Board MAC Address:  ");
    Serial.println(WiFi.macAddress());  
    
    if (esp_now_init() == ESP_OK) {
        Serial.println("ESPNow Init success");
    }
    else {
        Serial.println("ESPNow Init fail");
        return;
    }
    
    esp_now_register_send_cb(messageSent);   

    memcpy(peerInfo.peer_addr, receiverAddress, 6);
    peerInfo.channel = 0;
    peerInfo.encrypt = false;

    if (esp_now_add_peer(&peerInfo) != ESP_OK) {
        Serial.println("Failed to add peer");
        return;
    }
}
 
void loop(){
    myMessage.humidity = 42;
    myMessage.temperature = 16.9;
    esp_err_t result = esp_now_send(receiverAddress, (uint8_t *) &myMessage, sizeof(myMessage));
    if (result != ESP_OK) {
        Serial.println("Sending error");
    }
    delay(3000);
}

 

Als Daten versenden wir die Luftfeuchte und die Temperatur, die wir beispielsweise mit einem DHT22 ermittelt haben könnten. Die Daten werden in der Struktur data „verpackt“.

Receiver

Auf der Receiverseite kopieren wir die eingehenden Nachrichten erst in die „Hilfsstruktur“ stationMsg und übernehmen sie von da in das Array weatherStation[]. Der Zähler für die Elemente des Arrays ist dabei einfach die letzte Stelle der MAC-Adresse, also macAddr[5]:

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

uint8_t myAddress[] = {0x94, 0x3C, 0xC6, 0x33, 0x68, 0x05};

typedef struct data {
    int humidity;
    float temperature;
} data;

data stationMsg;
data weatherStation[3] = {0};

void messageReceived(const uint8_t* macAddr, const uint8_t* incomingData, int len){
    memcpy(&stationMsg, incomingData, sizeof(stationMsg));
    weatherStation[macAddr[5]].humidity = stationMsg.humidity;
    weatherStation[macAddr[5]].temperature = stationMsg.temperature;
}

void setup(){
    Serial.begin(115200);
    delay(1000);
    WiFi.mode(WIFI_STA);
    esp_wifi_set_mac(WIFI_IF_STA, myAddress);
    
    if (esp_now_init() == ESP_OK) {
        Serial.println("ESPNow Init success");
    }
    else {
        Serial.println("ESPNow Init fail");
        return;
    }

    esp_now_register_recv_cb(messageReceived);
}
 
void loop(){
    for(int i=0; i<3; i++){
        Serial.print("Weather Station ");
        Serial.print(i);
        Serial.println(":");
        Serial.print("Humidity [%]    : ");
        Serial.println(weatherStation[i].humidity);
        Serial.print("Temperature [°C]: ");
        Serial.println(weatherStation[i].temperature,1);
        Serial.println();
    }
    Serial.println();
    delay(5000);   
}

 

Hier die Ausgabe:

Ausgabe von multi_transm_one_recv_receiver.ino

Ein Transmitter, mehrere Receiver (ESP32)

Genauso einfach ist es, einen Transmitter und mehrere Receiver zu vernetzen. Diese Konfiguration könnte beispielsweise bei Smart Home Anwendungen zum Einsatz kommen. Der Transmitter muss dabei alle MAC-Adressen der Peers, also der Receiver, vorhalten und sich mit allen verbinden.

Der Einfachheit und besseren Übersicht halber bekommen in meinem Beispiel alle Receiver dieselben Datentypen zugesendet, sodass wir nur eine Struktur definieren müssen. Bei Bedarf ließe sich das einfach ändern.

Transmitter

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

uint8_t receiverAddress[3][6] = {{0x94, 0x3C, 0xC6, 0x33, 0x68, 0x98},
                                 {0x78, 0x21, 0x84, 0xDE, 0x2E, 0x3C},
                                 {0x30, 0xAE, 0xA4, 0x7B, 0x79, 0x90}};
esp_now_peer_info_t peerInfo[3];

typedef struct message {
    char text[32];
    int intVal;
    float floatVal;
} message;

message myMessage[3]; 

void messageSent(const uint8_t *macAddr, esp_now_send_status_t status) {
    Serial.printf("Send status to receiver %02X:%02X:%02X:%02X:%02X:%02X : ", 
            macAddr[0], macAddr[1], macAddr[2], macAddr[3], macAddr[4], macAddr[5]);        
    if(status == ESP_NOW_SEND_SUCCESS){
        Serial.println("Success");
    }
    else{
        Serial.println("Error");
    }
}

void setup(){
    Serial.begin(115200);
    // delay(1000); // uncomment if your serial monitor is empty
    WiFi.mode(WIFI_STA);
    
    if (esp_now_init() == ESP_OK) {
        Serial.println("ESPNow Init success");
    }
    else {
        Serial.println("ESPNow Init fail");
        return;
    }
    
    esp_now_register_send_cb(messageSent);   

    for(int i=0; i<3; i++){
        memcpy(peerInfo[i].peer_addr, receiverAddress[i], 6);
        peerInfo[i].channel = 0;
        peerInfo[i].encrypt = false;

        if (esp_now_add_peer(&peerInfo[i]) != ESP_OK) {
            Serial.println("Failed to add peer");
            return;
        }
    }
}
 
void loop(){
    char textMsg0[] = "Hi Receiver 0!";
    memcpy(&myMessage[0].text, textMsg0, sizeof(textMsg0));
    myMessage[0].intVal = 4242;
    myMessage[0].floatVal = 42.42;
    
    char textMsg1[] = "Ciao Receiver 1";
    memcpy(&myMessage[1].text, textMsg1, sizeof(textMsg1));
    myMessage[1].intVal = 1234;
    myMessage[1].floatVal = 12.34;
    
    char textMsg2[] = "Hola Receiver 2";
    memcpy(&myMessage[2].text, textMsg2, sizeof(textMsg2));
    myMessage[2].intVal = 4711;
    myMessage[2].floatVal = 47.11;
    
    for(int i=0; i<3; i++){
        esp_err_t result = esp_now_send(receiverAddress[i], (uint8_t *) &myMessage[i], sizeof(myMessage[i]));
        if (result != ESP_OK) {
            Serial.print("Sending error module ");
            Serial.println(i);
        }
    }
     delay(10000);
}

 

Ich denke, der Code braucht keine weiteren Erläuterungen, oder?

Auch diesen Sketch könntet ihr etwas vereinfachen, indem ihr die MAC-Adressen der Receiver so ändert, dass sie sich nur im letzten Byte unterscheiden. Ihr braucht dann nur ein eindimensionales Array für die receiverAddress, und wenn ihr Receiver Nr. i ansprechen wollt, dann müsst ihr lediglich die Adresse mit receiverAddress[5] = 0x0i einstellen.

Vielleicht noch ganz nützlich: Wenn ihr in der Funktion esp_now_send() die Receiver-Adresse durch NULL (so ausgeschrieben, also nicht „0“) ersetzt, dann geht die Nachricht an alle angemeldeten Peers.

Ausgabe

So sieht die Ausgabe auf der Transmitterseite aus:

Ausgabe von one_transm_multi_recv_transmitter.ino
Ausgabe von one_transm_multi_recv_transmitter.ino

Receiver

Auch der Sketch für die Receiver sollte ohne weitere Erläuterungen verständlich sein:

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

typedef struct message {
    char text[32];
    int intVal;
    float floatVal;
} message;

message myMessage;

void messageReceived(const uint8_t* macAddr, const uint8_t* incomingData, int len){
    memcpy(&myMessage, incomingData, sizeof(myMessage));
    Serial.print("Message: ");
    Serial.println(myMessage.text);
    Serial.print("Integer Value: ");
    Serial.println(myMessage.intVal);
    Serial.print("Float Value: ");
    Serial.println(myMessage.floatVal);
    Serial.println();
}

void setup(){
    Serial.begin(115200);
    // delay(1000); // uncomment if your serial monitor is empty
    WiFi.mode(WIFI_STA);
    
    if (esp_now_init() == ESP_OK) {
        Serial.println("ESPNow Init success");
    }
    else {
        Serial.println("ESPNow Init fail");
        return;
    }

    esp_now_register_recv_cb(messageReceived);
}
 
void loop(){}

 

Ausgabe

Und hier noch beispielhaft die Ausgabe von Receiver 1:

Ausgabe von one_transm_multi_recv_receiver.ino
Ausgabe von one_transm_multi_recv_receiver.ino

ESP als Transceiver (ESP32)

Eine Konstellation fehlt noch, nämlich der Einsatz von ESP-Boards als Transceiver, also als kombinierten Sender und Empfänger. Die gute Nachricht ist, dass ihr dazu keine neuen Funktionen braucht. Ihr müsst auch nicht zwischen Empfangs- und Sendemodus umschalten o. ä. Nehmt einfach einen der Transmitter-Sketche und fügt die für einen Receiver benötigten Elemente hinzu (oder andersherum).

In meinem Beispiel dazu sendet ein Transceiver (der „Lead“) in bestimmten Abständen Daten an einen anderen Transceiver (der „Follower“), der sich dafür beim Lead-Transceiver bedankt und seine Laufzeit in Sekunden zurückmeldet. Macht zugegeben wenig Sinn, aber es soll ja auch nur das Prinzip veranschaulichen.

Den Lead-Transceiver habe ich so genannt, weil er in dieser Konstellation den Takt vorgibt. Genauso gut könnten sich die beiden Transceiver ihre Nachrichten gleichberechtigt und ereignisbasiert zusenden, z. B. in bestimmten Zeitabständen, bei einem bestimmten Sensorwert, beim Drücken eines Tasters oder was euch auch immer in den Sinn kommt.

Sketch für den „Lead-Transceiver“

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

uint8_t receiverAddress[] = {0x94, 0x3C, 0xC6, 0x33, 0x68, 0x98};
esp_now_peer_info_t peerInfo;

typedef struct messageToBeSent {
    char text[64];
    int intVal;
    float floatVal;
} messageToBeSent;

typedef struct receivedMessage {
    char text[64];
    long runTime;
} receivedMessage;

messageToBeSent myMessageToBeSent; 
receivedMessage myReceivedMessage; 

void messageSent(const uint8_t *macAddr, esp_now_send_status_t status) {
    Serial.print("Send status: ");
    if(status == ESP_NOW_SEND_SUCCESS){
        Serial.println("Success");
    }
    else{
        Serial.println("Error");
    }
}

void messageReceived(const uint8_t* macAddr, const uint8_t* incomingData, int len){
    memcpy(&myReceivedMessage, incomingData, sizeof(myReceivedMessage));
    Serial.printf("Incoming Message from: %02X:%02X:%02X:%02X:%02X:%02X \n\r", 
            macAddr[0], macAddr[1], macAddr[2], macAddr[3], macAddr[4], macAddr[5]);
    Serial.print("Message: ");
    Serial.println(myReceivedMessage.text);
    Serial.print("RunTime [s]: ");
    Serial.println(myReceivedMessage.runTime);
    Serial.println();
}

void setup(){
    Serial.begin(115200);
    // delay(1000); // uncomment if your serial monitor is empty
    WiFi.mode(WIFI_STA);
    
    if (esp_now_init() == ESP_OK) {
        Serial.println("ESPNow Init success");
    }
    else {
        Serial.println("ESPNow Init fail");
        return;
    }
    
    esp_now_register_send_cb(messageSent);  
    esp_now_register_recv_cb(messageReceived); 

    memcpy(peerInfo.peer_addr, receiverAddress, 6);
    peerInfo.channel = 0;
    peerInfo.encrypt = false;

    if (esp_now_add_peer(&peerInfo) != ESP_OK) {
        Serial.println("Failed to add peer");
        return;
    }
}
 
void loop(){
    char textMsg[] = "Hi, here's my data for you: ";
    memcpy(&myMessageToBeSent.text, textMsg, sizeof(textMsg));
    myMessageToBeSent.intVal = 4242;
    myMessageToBeSent.floatVal = 42.42;
    esp_err_t result = esp_now_send(receiverAddress, (uint8_t *) &myMessageToBeSent, sizeof(myMessageToBeSent));
    if (result != ESP_OK) {
        Serial.println("Sending error");
    }
    delay(5000);
}

 

Wie ihr seht – es ist einfach nur eine Kombi aus Transmitter- und Receiversketch.

Sketch für den „Following Transceiver“

Hier, auf der Follower-Seite, werden die Nachrichten entgegengenommen und mit einer Antwort quittiert.

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

uint8_t receiverAddress[] = {0x30, 0xAE, 0xA4, 0x7B, 0x79, 0x90};
esp_now_peer_info_t peerInfo;

typedef struct messageToBeSent{
    char text[64];
    long runTime;
} messageToBeSent;

typedef struct receivedMessage {
    char text[64];
    int intVal;
    float floatVal;
} receivedMessage;

messageToBeSent myMessageToBeSent; 
receivedMessage myReceivedMessage; 

void messageReceived(const uint8_t* macAddr, const uint8_t* incomingData, int len){
    memcpy(&myReceivedMessage, incomingData, sizeof(myReceivedMessage));
    Serial.printf("Incoming Message from: %02X:%02X:%02X:%02X:%02X:%02X \n\r", 
                macAddr[0], macAddr[1], macAddr[2], macAddr[3], macAddr[4], macAddr[5]);
    Serial.print("Message: ");
    Serial.println(myReceivedMessage.text);
    Serial.print("Integer Value: ");
    Serial.println(myReceivedMessage.intVal);
    Serial.print("Float Value: ");
    Serial.println(myReceivedMessage.floatVal);
    Serial.println();
    Serial.println("Sending answer...");
    Serial.println(); 

    char textMsg[] = "Thanks for the data!";
    memcpy(&myMessageToBeSent.text, textMsg, sizeof(textMsg));
    myMessageToBeSent.runTime = millis()/1000;
    esp_err_t result = esp_now_send(receiverAddress, (uint8_t *) &myMessageToBeSent, sizeof(myMessageToBeSent));
    if (result != ESP_OK) {
        Serial.println("Sending error");
    }    
}

void setup(){
    Serial.begin(115200);
    // delay(1000); // uncomment if your serial monitor is empty
    WiFi.mode(WIFI_STA);
    
    if (esp_now_init() == ESP_OK) {
        Serial.println("ESPNow Init success");
    }
    else {
        Serial.println("ESPNow Init fail");
        return;
    }

    esp_now_register_recv_cb(messageReceived);
    
    memcpy(peerInfo.peer_addr, receiverAddress, 6);
    peerInfo.channel = 0;
    peerInfo.encrypt = false;

    if (esp_now_add_peer(&peerInfo) != ESP_OK) {
        Serial.println("Failed to add peer");
        return;
    }
}
 
void loop(){}

 

Ausgaben

Hier die Ausgabe für den Lead:

Ausgabe von transceiver_lead.ino

Und hier die Ausgabe für den Follower.

Ausgabe von transceiver_follower.ino
Ausgabe von transceiver_follower.ino

Ich spare mir ein Beispiel für das Vernetzen mehrerer Transceiver. Das solltet ihr problemlos aus den vorherigen Beispielen aufbauen können.

Übertragung der Sketche auf den ESP8266

Die ESP8266 Implementierung von ESP-NOW unterscheidet sich nicht gravierend von der Implementierung auf dem ESP32. Der Unterschied ist aber groß genug, dass vereinheitlichte Sketche (über Kommentierungen oder #ifdef ESP32...#else Konstruktionen) recht unübersichtlich geworden wären. 

Hier die wesentlichen Unterschiede:

  • Die Namen der einzubindenden Bibliotheken.
  • Die Parameter der Callback Funktionen. 
  • Für den ESP82866 muss seine Rolle als Receiver, Transmitter oder Transceiver mit esp_now_set_self_role() ausdrücklich festgelegt werden.
  • Rückgabewerte einiger Funktionen.

Als Beispiel habe ich die Transceiver-Sketche für den ESP8266 „übersetzt“.

Lead

#include <espnow.h>
#include "ESP8266WiFi.h"

uint8_t receiverAddress[] = {0xA4, 0xCF, 0x12, 0xDF, 0x5D, 0x89};

typedef struct messageToBeSent {
    char text[64];
    int intVal;
    float floatVal;
} messageToBeSent;

typedef struct receivedMessage {
    char text[64];
    long runTime;
} receivedMessage;

messageToBeSent myMessageToBeSent; 
receivedMessage myReceivedMessage; 

void messageSent(uint8_t *macAddr, uint8_t status) {
    Serial.print("Send status: ");
    if(status == 0){
        Serial.println("Success");
    }
    else{
        Serial.println("Error");
    }
}

void messageReceived(uint8_t* macAddr, uint8_t* incomingData, uint8_t len){
    memcpy(&myReceivedMessage, incomingData, sizeof(myReceivedMessage));
    Serial.printf("Incoming Message from: %02X:%02X:%02X:%02X:%02X:%02X \n\r", 
            macAddr[0], macAddr[1], macAddr[2], macAddr[3], macAddr[4], macAddr[5]);    
    Serial.print("Message: ");
    Serial.println(myReceivedMessage.text);
    Serial.print("RunTime [s]: ");
    Serial.println(myReceivedMessage.runTime);
    Serial.println();
}

void setup(){
    Serial.begin(115200);
    // delay(1000); // uncomment if your serial monitor is empty
    WiFi.mode(WIFI_STA);
    
    if (esp_now_init() == 0) {
        Serial.println("ESPNow Init success");
    }
    else {
        Serial.println("ESPNow Init fail");
        return;
    }

    esp_now_set_self_role(ESP_NOW_ROLE_COMBO);
    uint8_t result = esp_now_add_peer(receiverAddress, ESP_NOW_ROLE_COMBO, 0, NULL, 0);
    if(result != 0){
        Serial.println("Failed to add peer");
    }
    
    esp_now_register_send_cb(messageSent);  
    esp_now_register_recv_cb(messageReceived); 

   
}
 
void loop(){
    char textMsg[] = "Hi, here's my data for you: ";
    memcpy(&myMessageToBeSent.text, textMsg, sizeof(textMsg));
    myMessageToBeSent.intVal = 4242;
    myMessageToBeSent.floatVal = 42.42;
    esp_now_send(receiverAddress, (uint8_t *) &myMessageToBeSent, sizeof(myMessageToBeSent));
    delay(5000);
}

 

Follower

#include <espnow.h>
#include "ESP8266WiFi.h"

uint8_t receiverAddress[] = {0x44, 0x17, 0x93, 0x0E, 0x2E, 0xED};

typedef struct messageToBeSent{
    char text[64];
    long runTime;
} messageToBeSent;

typedef struct receivedMessage {
    char text[64];
    int intVal;
    float floatVal;
} receivedMessage;

messageToBeSent myMessageToBeSent; 
receivedMessage myReceivedMessage; 

void messageReceived(uint8_t* macAddr, uint8_t* incomingData, uint8_t len){
    memcpy(&myReceivedMessage, incomingData, sizeof(myReceivedMessage));
    Serial.printf("Incoming Message from: %02X:%02X:%02X:%02X:%02X:%02X \n\r", 
            macAddr[0], macAddr[1], macAddr[2], macAddr[3], macAddr[4], macAddr[5]);
    Serial.print("Message: ");
    Serial.println(myReceivedMessage.text);
    Serial.print("Integer Value: ");
    Serial.println(myReceivedMessage.intVal);
    Serial.print("Float Value: ");
    Serial.println(myReceivedMessage.floatVal);
    Serial.println();
    Serial.println("Sending answer...");
    Serial.println(); 

    char textMsg[] = "Thanks for the data!";
    memcpy(&myMessageToBeSent.text, textMsg, sizeof(textMsg));
    myMessageToBeSent.runTime = millis()/1000;
    esp_now_send(receiverAddress, (uint8_t *) &myMessageToBeSent, sizeof(myMessageToBeSent));    
}    

void setup(){
    Serial.begin(115200);
    // delay(1000); // uncomment if your serial monitor is empty
    WiFi.mode(WIFI_STA);
    
    if (esp_now_init() == 0) {
        Serial.println("ESPNow Init success");
    }
    else {
        Serial.println("ESPNow Init fail");
        return;
    }

    esp_now_set_self_role(ESP_NOW_ROLE_COMBO);
    uint8_t result = esp_now_add_peer(receiverAddress, ESP_NOW_ROLE_COMBO, 0, NULL, 0);
    if(result != 0){
        Serial.println("Failed to add peer");
    }
    
    esp_now_register_recv_cb(messageReceived);
}
 
void loop(){}

 

ESP8266 und ESP32 mischen

Einer der Vorzüge von ESP-NOW ist, dass ihr in eurem Netzwerk ESP8266- und ESP32-Boards problemlos mischen könnt. Ihr könnt das testen, indem ihr beispielsweise den Lead-Transceiver Sketch für den ESP32 auf ein ESP32-Board ladet und den Follower-Transceiver Sketch für den ESP8266 auf ein ESP8266-Board. Lediglich die MAC-Adressen sind natürlich anzupassen.

Kanal wechseln (ESP32 und ESP8266)

Falls Ihr Reichweitenprobleme aufgrund der Wechselwirkung mit anderen WLAN Netzwerken habt, dann könnte ein Wechsel auf einen anderen Kanal helfen. Die folgenden zwei Sketche zeigen, wie ihr den Kanal für ein ESP32- und ein ESP8266-Board wechselt. Alle Teilnehmer in einem Netz müssen natürlich auf denselben Kanal eingestellt werden.

ESP32

#include <esp_now.h>
#include <esp_wifi.h>
#include <WiFi.h>
#define CHANNEL 13

void setup(){
    Serial.begin(115200);
    // delay(1000); // uncomment if your serial monitor is empty
    WiFi.mode(WIFI_STA);
    Serial.print("WiFi-Channel Default: ");
    Serial.println(WiFi.channel());
    
    esp_wifi_set_promiscuous(true);
    esp_wifi_set_channel(CHANNEL, WIFI_SECOND_CHAN_NONE);
    esp_wifi_set_promiscuous(false);
    
    Serial.print("WiFi-Channel Update : ");
    Serial.println(WiFi.channel());
}
     
void loop(){}

ESP8266

#include <espnow.h>
#include "ESP8266WiFi.h"
#define CHANNEL 13

void setup(){
    Serial.begin(115200);
    // delay(1000); // uncomment if your serial monitor is empty
    WiFi.mode(WIFI_STA);
    Serial.print("WiFi-Channel Default: ");
    Serial.println(WiFi.channel());
    
    wifi_promiscuous_enable(true);
    wifi_set_channel(CHANNEL);
    wifi_promiscuous_enable(false); 

    Serial.print("WiFi Channel Update : ");
    Serial.println(WiFi.channel());
}
 
void loop(){}

Reichweite

Wenn ihr nach Reichweitentests für ESP-NOW googelt, werdet ihr auf Maximalwerte zwischen 100 und 250 Meter stoßen, sofern die Versuche im Freien, ohne Hindernisse und ohne externe Antennen durchgeführt wurden. Wie weit ihr in eurer Wohnung oder eurem Haus kommt, hängt vor allem von der Bausubstanz ab.

Bei meinen eigenen Reichweitentests kamen jeweils zwei Wemos D1 Mini Boards und zwei ESP32-WROOM-32 basierte Development Boards zum Einsatz. Beide Paarungen erzielten identische Reichweiten. Im Nachbarzimmer, also durch eine Wand, gingen mir keine Nachrichten verloren. Ein Zimmer weiter, also durch zwei Wände, hatte ich nur noch in einigen Bereichen Empfang. Das ist ungefähr vergleichbar mit der Reichweite meiner Fritz!Box 7590.

Falls euch die Reichweite nicht ausreichen sollte, könntet ihr ein Modul als Repeater zwischen die zu verbindenden Module setzen. Oder ihr greift zu Modulen, die es erlauben, externe Antennen anzuschließen.

Danksagung

Den Zettel mit dem „NOW“ auf dem Beitragsbild habe ich S K auf Pixabay zu verdanken.

14 thoughts on “ESP-NOW

  1. Wow! Ich bin erst vor kurzem auf diese Seite gestoßen und bin schlichtweg begeistert von der Fülle und vor allem Qualität der Infos.

    Ich löte/bastle/baue selber seit >20 Jahren aber wer kennt es nicht: man kann einfach nicht alles wissen, also googelt man nach gewissen Dingen. In meinem Fall nach ESP deep sleep modes.

    Fantastisch!

    Viele Grüße, Jan

  2. Wieder mal eine super Anleitung, Danke dafür.
    In Englisch gibt es von randomnerdtutorials dot com auch sowas, aber in Deutsch und so gut auf gearbeitet, respekt !!

  3. Hallo Wolfgang,
    bin ein blutiger Anfänger und stolpere unter Anderem über das Thema „Reichweiten“.
    Wo bekommt man Module mit „externen“ Antennen oder wie liesen sich welche anschließen?
    Vielen Dank.

    1. Hallo Reinhard, wenn du einfach mal „esp32 mit externer antenne“ googlest, dann wirst du auf Module mit einem IPEX-Antennenanschluss stoßen. Das ist ein kleiner runder Stecker von wenigen Millimeter Durchmesser und kleinem Pin in der Mitte. Oder wenn du ein ESP8266 Modul bevorzugst, dann könntest du ein Wemos D1 Mini Pro Board nehmen. Für die Antenne google einach „2.4 GHz Antenne“. Diese Antennen habe für gewöhnlich einen SMA-Anaschluss. Dafür gibt es Adapter. Einfach nach „IPEX SAM Adpater“ suchen. Das alles gibt es zum Beispiel auf Amazon oder eBay. VG, Wolfgang

    1. Hallo Wolfgang,

      vielleicht kannst Du mir auf die Sprünge helfen, ich bekomme die Fehlermeldung :C:\Myfiles_Toshiba\Arduino\Test\Test.ino:1:10: fatal error: esp_now.h: No such file or directory
      1 | #include
      | ^~~~~~~~~~~
      compilation terminated.

      exit status 1

      Compilation error: esp_now.h: No such file or directory

      Irgendwie finde ich nicht die passende Library…..

      Vielen Dank für den „richtigen“ Hinweis…

      Gruß

      Karsten

      1. Hi Karsten,
        welchen ESP verwendest du? Wenn es ein ESP8266 ist (z.B. ein Wemos D1 Mini Board), dann heißt die einzubindende Bibliothek espnow.h und nicht esp_now.h. Das wäre der naheliegendste Fehler.
        VG, Wolfgang

        1. Hallo Wolfgang,

          der naheliegeste Fehler war es, 😉 peinlich, peinlich.

          Vielen Dank für die Unterstützung, jetzt funktioniert es.
          Wer lesen kann ist nartürlich klar im Vorteil!

          Gruß

          Karsten

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert