Základy komunikace ESP-NOW
ESP-NOW je specializovaný bezdrátový komunikační protokol vyvinutý společností Espressif, který je přímo integrován do mikrokontrolerů rodiny ESP32 a ESP8266. Na rozdíl od běžného Wi-Fi připojení umožňuje tento protokol zařízením komunikovat přímo mezi sebou (peer-to-peer) bez potřeby centrálního routeru nebo přístupového bodu. Svou architekturou se podobá nízkovýkonovým bezdrátovým připojením v pásmu 2,4 GHz a je navržen pro maximální efektivitu při přenosu menších objemů dat.
Základní vlastnosti protokolu:
- Práce na linkové vrstvě: ESP-NOW funguje na 2. vrstvě OSI modelu, což znamená, že zařízení se navzájem identifikují pomocí svých unikátních MAC adres a nepotřebují složité síťové protokoly jako DHCP nebo TCP/IP.
- Omezená velikost zprávy: Jedna datová zpráva (payload) může mít velikost maximálně 250 bajtů.
- Flexibilní topologie: Podporuje různé režimy komunikace, včetně jednosměrného a obousměrného přenosu, režimu jeden-k-mnoha (broadcast), nebo sběru dat z mnoha senzorů do jednoho centra.
- Souběžný provoz: Protokol může běžet paralelně s klasickým Wi-Fi připojením, což umožňuje jednomu čipu přijímat data přes ESP-NOW a zároveň je odesílat do internetu přes běžný router.
Výhody a nevýhody ESP-NOW
Mezi hlavní výhody patří především extrémně nízká latence a rychlost přenosu, kdy od probuzení čipu z hlubokého spánku (deep sleep) po odeslání dat uběhne pouze několik milisekund. Díky absenci nutnosti udržovat trvalé spojení s routerem má protokol také velmi nízkou spotřebu energie, což dramaticky prodlužuje životnost bateriově napájených projektů.
Naopak hlavní nevýhodou je omezený dosah, který se v ideálních podmínkách pohybuje kolem 220 metrů, ale v budovách s překážkami výrazně klesá. Dalším limitem je malá kapacita jednotlivých zpráv a náchylnost k rušení v silně zarušeném pásmu 2,4 GHz. Protokol také nenabízí vestavěnou infrastrukturu pro složité síťové topologie (mesh) bez dodatečného programování.
Jak bylo řečeno, ESP-NOW je ideálním řešením pro projekty, kde je prioritou rychlost a úspora energie. V praxi ho bastlíři nejčastěji využívají pro:
- Vzdálené bateriové senzory: Teploměry nebo čidla vlhkosti, která se jednou za čas probudí, odešlou data a ihned usnou.
- Dálkové ovládání: Díky minimálnímu zpoždění je perfektní pro ovládání robotů, RC autíček nebo dronů.
- Domácí automatizaci: Propojování vypínačů, ovládání oken nebo osvětlení v rámci bytu bez zatěžování domácí Wi-Fi sítě.
- Sběr dat (Gateway): Centrální jednotka může sbírat informace z mnoha rozprostřených modulů a následně je hromadně odesílat do cloudu nebo databáze.
Všesměrové vysílání
Jako první ukázku si ukážeme nejjednodušší možný způsob komunikace, kterým je tzv. broadcast (všesměrové vysílání). V tomto režimu jeden modul odesílá data „do éteru“ a všechna zařízení v dosahu, která naslouchají na stejném kanálu, tuto zprávu přijmou.
Výhodou broadcastu je, že nemusíte znát unikátní MAC adresy ostatních modulů. Používá se k tomu speciální rezervovaná adresa FF:. Nevýhodou je, že odesílatel nedostává žádné potvrzení o doručení, takže neví, zda zprávu někdo skutečně zachytil.
Vysílač (Sender)
Vysílací modul musí nejprve nastavit WiFi do režimu stanice, inicializovat protokol a přidat „všesměrového peera“.
#include <esp_now.h>
#include <WiFi.h>
// Rezervovaná MAC adresa pro broadcast
uint8_t broadcastAddress[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
void setup() {
Serial.begin(115200);
// WiFi musí být v režimu STA (stanice)
WiFi.mode(WIFI_STA);
WiFi.disconnect(); // Doporučeno v Core 3+ před inicializací ESP-NOW
// Inicializace ESP-NOW
if (esp_now_init() != ESP_OK) {
Serial.println("Chyba při inicializaci ESP-NOW");
return;
}
// Registrace peera (příjemce)
esp_now_peer_info_t peerInfo = {}; // Důležité: vynuluje celou strukturu
memcpy(peerInfo.peer_addr, broadcastAddress, 6);
peerInfo.channel = 0; // Použije aktuální kanál
peerInfo.encrypt = false; // Broadcast nelze šifrovat
if (esp_now_add_peer(&peerInfo) != ESP_OK) {
Serial.println("Nepodařilo se přidat peera");
return;
}
}
void loop() {
const char* zprava = "AHOJ, jsem tu!";
// Odeslání zprávy všem v okolí
esp_err_t vysledek = esp_now_send(broadcastAddress, (uint8_t *)zprava, strlen(zprava) + 1);
if (vysledek == ESP_OK) {
Serial.println("Zpráva odeslána");
} else {
Serial.println("Chyba při odesílání");
}
delay(5000); // Opakování každých 5 sekund
}
- DŮLEŽITÉ:
- V novějších verzích ESP32 Arduino Core (verze 3.x a vyšší) je nutné strukturu peera vždy explicitně vynulovat (např. pomocí
= {}při deklaraci). Pokud v paměti zůstanou náhodná data, funkceesp_now_add_peer()selže a vyhlásí chybu.
Abychom mohli ESP-NOW vůbec probudit k životu, potřebujeme přidat potřebné knihovny esp_now a i klasickou WiFi.h:
#include <esp_now.h>
#include <WiFi.h>
Hned na začátku funkce setup() přepneme Wi-Fi rozhraní do režimu stanice (STA) pomocí: WiFi.. ESP-NOW sice nevyžaduje připojení k routeru ani zadávání SSID a hesla, ale rádiový čip ESP32 musí běžet v definovaném režimu, aby dokázal odesílat i přijímat Wi-Fi pakety.
WiFi.mode(WIFI_STA)
WiFi.disconnect();
- Upozornění:
- Všimněte si jednoho docela důležitého detailu – příkazu
WiFi.disconnect(). - V novějších verzích rozhraní (Arduino ESP32 Core 3.x) se po přepnutí do režimu stanice doporučuje Wi-Fi explicitně odpojit. Zajistíme tím, že se čip nebude na pozadí pokoušet připojovat k dříve uloženým přístupovým bodům nebo vyhledávat sítě, což by zbytečně měnilo Wi-Fi kanál a způsobovalo výpadky v příjmu zpráv.
Samotný protokol startujeme voláním:
esp_now_init()
Pokud funkce vrátí cokoliv jiného než návratový kód ESP_OK, znamená to, že se inicializace nezdařila. V takovém případě běh programu přerušíme, protože bez funkčního protokolu nemá smysl pokračovat.
Registrace příjemce (Peer) a broadcastová adresa
Před odesláním prvního bajtu musíte ESP32 říct, komu má data adresovat. ESP-NOW funguje na principu peer-to-peer spojení, kde je každý příjemce registrován jako takzvaný peer. V tomto příkladu definujeme pole broadcastAddress naplněné hodnotami 0xFF. To je rezervovaná adresa, která funguje jako všesměrové vysílání – zprávu zachytí jakékoliv ESP32 v dosahu, které naslouchá na stejném Wi-Fi kanálu.
uint8_t broadcastAddress[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
Následuje nastavení struktury esp_now_peer_info_t peerInfo = {};. Zápis = {}, jak bylo v poznámce výše zmíněno, je pro správný chod zásadní, protože vynuluje veškerou paměť alokovanou pro tuto strukturu a zabrání tomu, aby se neidentifikované smetí v paměti přepsalo jako výchozí hodnota. Do struktury zkopírujeme naši cílovou MAC adresu, nastavíme Wi-Fi kanál (hodnota 0 znamená využití aktuálně nastaveného kanálu stanice) a vypneme šifrování (encrypt = false), protože broadcastové pakety z principu šifrovat nelze. Kompletní profil následně zaregistrujeme do systému pomocí esp_.
esp_now_peer_info_t peerInfo = {};
memcpy(peerInfo.peer_addr, broadcastAddress, 6);
peerInfo.channel = 0;
peerInfo.encrypt = false;
Odesílání dat
Ve funkci loop() dochází k samotnému odbavení zprávy. Funkce esp_now_send() přijímá tři základní argumenty:
- Cílovou MAC adresu,
- ukazatel na samotná data, přetypovaný na generický ukazatel (
uint8_t *), - délku odesílaného datového bloku v bajtech.
U délky textového řetězce za pozornost jistě stojí výraz strlen(. Přičtením jedničky zajistíme, že se po síti přenese i ukončovací nulový znak (\0). Příjemce díky tomu dostane korektně ukončený C-string, který může rovnou vypsat do sériového rozhraní bez rizika přetečení paměti.
esp_err_t vysledek = esp_now_send(broadcastAddress, (uint8_t *)zprava, strlen(zprava) + 1);
Návratová hodnota z esp_now_send() předává informaci o tom, zda byl paket úspěšně předán Wi-Fi vrstvě k odeslání. Vše se děje asynchronně a bleskově – bez jakéhokoliv zdržování spojeného s navazováním klasického TCP/IP spojení.
Heuréka! Jednoduchý broadcastový ESP-NOW maják máme na světě.
Přijímač (Receiver)
Modul, který má zprávu přijmout, nepotřebuje registrovat žádného peera. Musí však mít zaregistrovanou tzv. callback funkci, která se automaticky spustí ve chvíli, kdy čip zachytí data.
#include <esp_now.h>
#include <WiFi.h>
// Callback funkce pro příjem dat (kompatibilní s Arduino Core 3.x)
void OnDataRecv(const esp_now_recv_info_t *info, const uint8_t *incomingData, int len) {
Serial.print("Přijatá zpráva: ");
Serial.println((char*)incomingData);
}
void setup() {
Serial.begin(115200);
// Nastavení WiFi na Station mód
WiFi.mode(WIFI_STA);
WiFi.disconnect(); // Doporučeno v Core 3+ před inicializací ESP-NOW
// Inicializace ESP-NOW
if (esp_now_init() != ESP_OK) {
Serial.println("Chyba při inicializaci ESP-NOW");
return;
}
// Registrace callback funkce pro příjem dat
esp_now_register_recv_cb(OnDataRecv);
Serial.println("ESP-NOW přijímač je připraven");
}
void loop() {
// Hlavní smyčka zůstává prázdná
}
- My nyní používáme nejnovější verzi jádra ESP32 (3.0 a novější), signatura funkce
OnDataRecv(často používaná ve starších tutoriálech) se v této verzi změnila a vyžaduje místo prostého ukazatele na MAC adresuesp_now_recv_info_t * macnovou komplexnější strukturuesp_now_recv_info_t *info. - Přidání
WiFi.disconnect()po nastavení wifi móduWiFi.mode(WIFI_STA)(prý) pomáhá předejít občasným chybám při startu ESP-NOW v novějších verzích ESP32 Arduina. - Aby broadcast fungoval spolehlivě, musí všechna zařízení pracovat na stejném WiFi kanálu (standardně kanál
1) - WiFi přenosy mají krátkodobě vysoký odběr proudu. Pokud komunikace selhává při napájení z baterie, může to být způsobeno nedostatečným proudem.
- Pokud používáme WiFi (včetně ESP-NOW), nemůžeme na ESP32 používat piny připojené k převodníku
ADC2, protože tyto piny sdílejí hardware s bezdrátovým modulem. Pro čtení senzorů vždy volme piny z blokuADC1. - I když posíláme jen krátký text, pamatujme, že limit jedné zprávy je jen 250 bajtů
- Pokud bychom místo textu posílali složitější data (např. teplotu a vlhkost), musí být struktura dat na straně vysílače i příjemce naprosto shodná – ukážeme si dále.
Přípravná část v setup() vychází ze stejného základu jako u vysílače. To nejdůležitější u přijímače se (na rozdíl od vysílače) odehrává zcela mimo hlavní smyčku. ESP-NOW staví na asynchronním zpracování – jakmile anténa zachytí platný paket, systém okamžitě vyvolá vaši připravenou obslužnou funkci, tzv. callback (OnDataRecv).
void OnDataRecv(const esp_now_recv_info_t *info, const uint8_t *incomingData, int len) {...
Opět se zaměřme na parametry této funkce, které zde odpovídají modernímu rozhraní ESP-IDF / Core 3.x:
const esp_: Ukazatel na strukturu s metadaty. Odtud můžete snadno vytáhnout například MAC adresu odesílatele nebo sílu signálu (RSSI).now_ recv_ info_t *info const uint8_t *incomingData: Ukazatel na surový paměťový blok s přijatými bajty.int len: Přesná délka doručených dat.
Jelikož víme, že vysílač poslal textový řetězec včetně ukončovacího nulového znaku (\0), stačí ukazatel přetypovat na (char*). Příkaz Serial.println() pak data korektně zobrazí jako běžný text bez rizika, že by vyčetl náhodné smetí z paměti.
Serial.println((char*)incomingData);
Aby Wi-Fi vrstva věděla, kam má příchozí data předat, musíte callback zaregistrovat v setup() pomocí:
esp_now_register_recv_cb(OnDataRecv);
Od tohoto okamžiku běží odchytávání paketu zcela automaticky na úrovni systémových přerušení.
Největší výhoda tohoto konceptu je vidět na první pohled: funkce loop() zůstává úplně prázdná. Nemusíme neustále dokola kontrolovat stav rozhraní. Hlavní program tedy může provádět jiné výpočty, obsluhovat displej apod. – přijímač přesto zareaguje na příchozí zprávu s minimální latencí.
MAC adresa
Předtím, než se pustíme do adresné komunikace mezi dvěma konkrétními moduly, musíme se seznámit s pojmem MAC adresa. Ta je pro protokol ESP-NOW naprosto zásadní, protože slouží jako jediný identifikátor zařízení v síti.
MAC adresa (Media Access Control) představuje unikátní hardwarový identifikátor, který je každému síťovému zařízení (včetně čipů ESP32 a ESP8266) přidělen již při výrobě. Zatímco v běžné síti se setkáváme s IP adresami, protokol ESP-NOW pracuje na nižší, tzv. linkové vrstvě, kde se zařízení identifikují právě a jen pomocí těchto fyzických MAC adres. Každý modul má svou vlastní a jedinečnou adresu, což umožňuje odesílateli přesně zacílit data konkrétnímu příjemci.
Abychom mohli MAC adresu svého modulu zjistit, je potřeba do něj nahrát jednoduchý pomocný program:
#include <esp_now.h>
#include <WiFi.h>
void readMacAddress(){
uint8_t baseMac[6];
esp_err_t ret = esp_wifi_get_mac(WIFI_IF_STA, baseMac);
if (ret == ESP_OK) {
Serial.printf("%02x:%02x:%02x:%02x:%02x:%02x\n",
baseMac[0], baseMac[1], baseMac[2],
baseMac[3], baseMac[4], baseMac[5]);
} else {
Serial.println("Failed to read MAC address");
}
}
void setup(){
Serial.begin(115200);
WiFi.mode(WIFI_STA);
WiFi.STA.begin();
Serial.print("[DEFAULT] ESP32 Board MAC Address: ");
readMacAddress();
}
void loop(){
}
Funkce readMacAddress() využívá příkaz WiFi.macAddress(), který adresu přečte z paměti čipu a následně ji vypíše v sériovém monitoru v obvyklém šestnáctkovém formátu (např. 30:AE:A4:07:0D:64)
Obrázek nám ukazuje zobrazenou MAC adresu v sériovém monitoru pomocí výše uvedeného programu:
Postupně určíme MAC adresy u všech modulů ESP32, se kterými budeme podnikat všechny následující experimenty. Získanou adresu si vždy pečlivě pro daný modul ESP32 poznamenáme, protože ji budeme muset vložit do kódu vysílače/přijímače, aby věděl, se kterým „peerem“ má komunikovat.
Jednosměrná komunikace
Pokud jsme zjistili MAC adresu přijímače a nahráli jsme do něj zpět kód programu pro přijetí zprávy, můžeme se vrhnout na stranu vysílače. Nyní si ukážeme, jak z jednoho modulu ESP32 můžeme pomocí ESP-NOW zasílat cíleně data na druhý modul ESP32.
Vysílač (Sender)
Vysílač musí přesně vědět, komu data posílá, a musí mít tohoto příjemce zaregistrovaného ve svém seznamu „peerů“.
- MAC adresa příjemce: Do kódu vysílače je nutné vložit dříve zjištěnou MAC adresu cílového modulu.
- Registrace peera: Před odesláním první zprávy musí vysílač zavolat funkci
esp_now_add_peer(). Tím protokolu oznámíte parametry spojení (MAC adresa, kanál, případné šifrování). - Callback odeslání (volitelné): Je velmi užitečné zaregistrovat i funkci
esp_. Ta vysílači oznámí, zda byla zpráva úspěšně doručena. Protože adresná zpráva vyžaduje potvrzení (ACK) z druhé strany, vysílač hned ví, zda je příjemce v dosahu a zprávu přijal.now_ register_ send_cb - Samotné odeslání: Data se odesílají pomocí funkce
esp_now_send()s uvedením MAC adresy konkrétního příjemce.
V následujícím kódu musíme před překladem a nahráním v poli prijimacMAC nastavit skutečnou MAC adresou, kterou jste zjistili u svého modulu přijímače.
#include <esp_now.h>
#include <WiFi.h>
// MAC adresa příjemce
uint8_t prijimacMAC[] = {0x24, 0x62, 0xAB, 0x00, 0x00, 0x00};
// Callback pro potvrzení odeslání (Arduino Core 3+)
void OnDataSent(const esp_now_send_info_t *info, esp_now_send_status_t status) {
Serial.print("Stav odeslání poslední zprávy: ");
Serial.println(status == ESP_NOW_SEND_SUCCESS ?
"Doručeno v pořádku" :
"Chyba doručení");
// Volitelně vypíše MAC adresu příjemce
if (info != nullptr) {
Serial.printf("Příjemce: %02X:%02X:%02X:%02X:%02X:%02X\n",
info->des_addr[0], info->des_addr[1], info->des_addr[2],
info->des_addr[3], info->des_addr[4], info->des_addr[5]);
}
}
void setup() {
Serial.begin(115200);
// Nastavení Wi-Fi do režimu stanice
WiFi.mode(WIFI_STA);
WiFi.disconnect();
// Inicializace ESP-NOW
if (esp_now_init() != ESP_OK) {
Serial.println("Chyba při inicializaci ESP-NOW");
return;
}
// Registrace callbacku
esp_now_register_send_cb(OnDataSent);
// Nastavení peera
esp_now_peer_info_t peerInfo = {};
memcpy(peerInfo.peer_addr, prijimacMAC, 6);
peerInfo.channel = 0;
peerInfo.encrypt = false;
// Přidání peera
if (esp_now_add_peer(&peerInfo) != ESP_OK) {
Serial.println("Nepodařilo se přidat příjemce");
return;
}
Serial.println("ESP-NOW připraveno");
}
void loop() {
const char *text = "AHOJ, posilam data!";
esp_err_t vysledek = esp_now_send(
prijimacMAC,
(const uint8_t *)text,
strlen(text) + 1);
if (vysledek == ESP_OK) {
Serial.println("Zpráva odeslána ze strany ESP32");
} else {
Serial.print("Kritická chyba při odesílání: ");
Serial.println(vysledek);
}
delay(5000);
}
Následující obrázek č. 2 ukazuje výstup programu v sériovém monitoru prostředí Arduino IDE. Na obrázku vidíme úspěšné odeslání, včetně potvrzení „Doručeno v pořádku“.
Příjemce (Receiver)
Modul příjemce naopak „naslouchá“ a čeká na zprávy, které jsou adresovány jeho MAC adrese. I když bychom mohli pro příjem zprávy využít předchozí „naslouchací kód“, můžeme si zde ukázat možné jeho vylepšení. Uděláme drobnou změnu v callback funkci, kde kromě přijaté zprávy vypíšeme i MAC adresu vysílače.
// Callback funkce pro příjem dat (kompatibilní s Arduino Core 3.x)
void OnDataRecv(const esp_now_recv_info_t *info, const uint8_t *incomingData, int len) {
// Získání MAC adresy odesílatele
char macStr[18];
snprintf(macStr, sizeof(macStr), "%02x:%02x:%02x:%02x:%02x:%02x",
info->src_addr[0], info->src_addr[1], info->src_addr[2],
info->src_addr[3], info->src_addr[4], info->src_addr[5]);
Serial.print("Příjem z MAC: ");
Serial.print(macStr);
Serial.print(" | Zpráva: ");
// Bezpečné vytvoření řetězce z příchozích dat
char zprava[len + 1];
memcpy(zprava, incomingData, len);
zprava[len] = '\0';
Serial.println(zprava);
}
Programátor tak může v kódu přijímače pomocí jednoduché podmínky (např. příkazu if nebo switch) rozpoznat, zda data přišla z teploměru v obývacím pokoji, nebo z čidla ve sklepě, a podle toho je uložit nebo zobrazit.
Výsledné vypsání zachycené zprávy vidíme (viz obr. č. 3) v okně sériového monitoru prostředí Arduino IDE, ke kterému je modul přijímače připojen.
Odeslání složitější struktury
Pro odesílání více hodnot různých typů (například teplotu, vlhkost, ID modulu a stav sepnutí…) v jedné zprávě je v protokolu ESP-NOW nejlepším řešením použití datové struktury (struct). Tato metoda umožňuje zabalit různé proměnné do jednoho balíčku, který se odešle jako jeden celek o maximální velikosti 250 bajtů.
Klíčem k úspěchu je definovat naprosto stejnou strukturu v kódu vysílače i příjemce! Pokud by se struktury lišily (např. jiné pořadí proměnných nebo jiné datové typy), příjemce data špatně interpretuje a výsledkem budou nesmyslné hodnoty.
// Příklad struktury pro přenos různých typů dat
typedef struct struct_message {
char a[8]; // Textové pole
int b; // Celé číslo (např. ID nebo stav)
float c; // Desetinné číslo (např. teplota)
bool d; // Logická hodnota (např. stav relé)
} struct_message;
// Vytvoření instance struktury
struct_message myData;
Na straně vysílače naplníte instanci struktury konkrétními hodnotami a následně ji odešlete pomocí funkce esp_now_send().
- Přiřazení hodnot: K jednotlivým prvkům přistupujete pomocí tečkové notace (např.
myData.b = 123;). - Odeslání: Při volání funkce
esp_now_send()musíte adresu struktury přetypovat na ukazateluint8_t*a určit její velikost pomocí operátorusizeof().
// Naplnění dat
strcpy(myData.a, "Senzor 1");
myData.b = random(1, 100);
myData.c = 24.5;
myData.d = false;
// Odeslání celé struktury
esp_err_t result = esp_now_send(broadcastAddress, (uint8_t *) &myData, sizeof(myData));
Příjemce zachytí příchozí data v callback funkci (obvykle OnDataRecv). Zde se přijatý proud bajtů zkopíruje zpět do připravené struktury.
- Kopírování dat: K bezpečnému překlopení dat z vyrovnávací paměti do struktury se používá funkce
memcpy(). - Přístup k datům: Po zkopírování můžete s daty pracovat jako s běžnými proměnnými.
void OnDataRecv(const uint8_t * mac, const uint8_t *incomingData, int len) {
// Zkopírování příchozích dat do lokální struktury myData
memcpy(&myData, incomingData, sizeof(myData));
// Nyní můžete k hodnotám přistupovat
Serial.print("Přijato od: ");
Serial.println(myData.a);
Serial.print("Teplota: ");
Serial.println(myData.c);
}
Příjem z více modulů
V bastlířské praxi se tato topologie často doplňuje o funkci brány (gateway), která data posílá dále. Centrální ESP32 přijímá data ze senzorů úsporným protokolem ESP-NOW a následně je díky schopnosti čipu pracovat v paralelním režimu odesílá přes běžnou Wi-Fi síť do internetu, například do databáze, cloudu nebo systému Home Assistant. Jeden přijímač dokáže tímto způsobem obsloužit až 20 nešifrovaných zařízení, což plně postačuje pro většinu domácích automatizačních projektů.
My se zatím ale podíváme na jednoduchý scénář, ve kterém bude centrální jednotka „sbírat“ data z několika čidel (teploměrů). Pro přenos hodnoty z teploměru využijeme následující datovou strukturu (struct), která tuto hodnotu odešle na centrální jednotku (modul ESP32).
// Definice struktury pro přenos dat z teploměru
typedef struct struct_teplomer {
int id; // ID senzoru (např. 1 pro obývák)
float teplota; // Naměřená hodnota
} struct_teplomer;
struct_teplomer mojeData;
Vysílač (Teploměr)
Dále uvedený program simuluje senzor, který v pravidelných intervalech odesílá data (zde napevno nastavenou hodnotu) na konkrétní MAC adresu přijímače.
#include <esp_now.h>
#include <WiFi.h>
// MAC adresa příjemce
uint8_t adresaPrijimace[] = {0x24, 0x62, 0xAB, 0x00, 0x00, 0x00};
// --------------------------------------------------
// Struktura přenášených dat
// --------------------------------------------------
typedef struct struct_teplomer {
int id; // ID senzoru
float teplota; // Naměřená teplota
} struct_teplomer;
// Instance struktury
struct_teplomer mojeData;
// Callback po odeslání (Arduino Core 3+)
void OnDataSent(const esp_now_send_info_t *info, esp_now_send_status_t status) {
Serial.print("Stav doručení: ");
Serial.println(status == ESP_NOW_SEND_SUCCESS ? "Úspěch" : "Chyba");
}
void setup() {
Serial.begin(115200);
// WiFi musí být v režimu stanice
WiFi.mode(WIFI_STA);
WiFi.disconnect();
// Inicializace ESP-NOW
if (esp_now_init() != ESP_OK) {
Serial.println("Chyba inicializace ESP-NOW");
return;
}
// Registrace callbacku
esp_now_register_send_cb(OnDataSent);
// Registrace příjemce
esp_now_peer_info_t peerInfo = {};
memcpy(peerInfo.peer_addr, adresaPrijimace, 6);
peerInfo.channel = 0;
peerInfo.encrypt = false;
if (esp_now_add_peer(&peerInfo) != ESP_OK) {
Serial.println("Nepodařilo se přidat peera");
return;
}
Serial.println("=== ESP-NOW TEPLOMĚR ===");
}
void loop() {
// Ukázková data
mojeData.id = 1;
mojeData.teplota = 25.5;
// Odeslání struktury
esp_err_t vysledek = esp_now_send(
adresaPrijimace,
(const uint8_t *)&mojeData,
sizeof(mojeData));
if (vysledek == ESP_OK) {
Serial.print("Odesláno - ID: ");
Serial.print(mojeData.id);
Serial.print(" Teplota: ");
Serial.println(mojeData.teplota);
} else {
Serial.print("Chyba odeslání: ");
Serial.println(vysledek);
}
delay(5000);
}
Obrázek č. 4 ukazuje odeslání dat na centrální jednotku:
Přijímač (Centrální jednotka)
Přijímač čeká na data, která uloží do proměnné prijataData, a po jejich příchodu je vypíše do sériového monitoru. Program opět obsahuje datovou strukturu obsahující ID senzoru a hodnotu teploty. Za pozornost stojí část, ve které je délka přijeté zprávy porovnána s deklarovanou délkou. Případný rozdíl znamená
chybu přenosu.
if (len != sizeof(prijataData)) {
Serial.print("Chybná velikost dat: ");
Serial.println(len);
return;
}
Program si zjistí i MAC odesílatele, kterou zde jen vypíše:
Serial.print("Přijato od: ");
Serial.printf("%02X:%02X:%02X:%02X:%02X:%02X\n",
info->src_addr[0], info->src_addr[1],
info->src_addr[2], info->src_addr[3],
info->src_addr[4], info->src_addr[5]);
Celé přijetí zprávy od modulu teploměru znázorňuje obrázek č. 5.
Zde je uveden celý kód programu centrální jednotky:
#include <esp_now.h>
#include <WiFi.h>
// --------------------------------------------------
// Struktura přenášených dat
// Musí být STEJNÁ jako v odesílači
// --------------------------------------------------
typedef struct struct_teplomer {
int id;
float teplota;
} struct_teplomer;
// Proměnná pro přijatá data
struct_teplomer prijataData;
// Callback pro příjem dat (Arduino Core 3+)
void OnDataRecv(const esp_now_recv_info_t *info, const uint8_t *incomingData, int len) {
// Kontrola správné velikosti přijaté zprávy
if (len != sizeof(prijataData)) {
Serial.print("Chybná velikost dat: ");
Serial.println(len);
return;
}
// Kopírování dat do struktury
memcpy(&prijataData, incomingData, sizeof(prijataData));
// Výpis MAC adresy odesílatele
Serial.print("Přijato od: ");
Serial.printf("%02X:%02X:%02X:%02X:%02X:%02X\n",
info->src_addr[0], info->src_addr[1], info->src_addr[2],
info->src_addr[3], info->src_addr[4], info->src_addr[5]);
// Výpis přijatých dat
Serial.print("Data ze senzoru č.: ");
Serial.println(prijataData.id);
Serial.print("Naměřená teplota: ");
Serial.print(prijataData.teplota);
Serial.println(" °C");
Serial.println("-----------------------");
}
void setup() {
Serial.begin(115200);
// WiFi do režimu stanice
WiFi.mode(WIFI_STA);
WiFi.disconnect();
delay(100); // aby se stihla usadit MAC, jinak je 00:00:00:00:00:00
// Inicializace ESP-NOW
if (esp_now_init() != ESP_OK) {
Serial.println("Chyba inicializace ESP-NOW");
return;
}
// Registrace callbacku
esp_now_register_recv_cb(OnDataRecv);
Serial.println("Centrální jednotka připravena pro příjem ESP-NOW");
Serial.print("Moje MAC adresa: ");
Serial.println(WiFi.macAddress());
}
void loop() {
// Není potřeba žádný kód
// Příjem probíhá v callbacku OnDataRecv()
}
Šifrování
- POZNÁMKA k nastavení šifrování:
- Většina návodů k šifrovanému ESP-NOW řeší pouze lokální klíč (LMK) a o hlavním klíči (PMK) se prakticky nezmiňuje – pravděpodobně proto, že ho ignoroval už ten prvotní návod z roku 2018, ze kterého všichni dodnes „čerpáme“. 😉
- Abychom udrželi začátek co nejpřehlednější, ukážeme si nejprve klasický postup jen s LMK. Na závěr si ale vysvětlíme i roli PMK a ukážeme si, jak zabezpečení komunikace dotáhnout na maximum.
Pokud používáme ESP-NOW pouze pro přenos několika hodnot mezi vlastními moduly, může se zdát, že není potřeba nic řešit. Data ale létají vzduchem stejně jako u běžné Wi-Fi komunikace, takže kdokoliv v dosahu s vhodným zařízením by je teoreticky mohl zachytit.
ESP-NOW proto umožňuje u přímého spojení mezi dvěma zařízeními (unicast) zapnout šifrování pomocí AES. Přenos mezi senzorem a centrální jednotkou pak není posílán „volně“, ale je chráněn šifrovacím klíčem. V praxi to znamená, že data ze senzoru (například teplota) nejsou odeslána přímo, ale nejdříve se zašifrují a přijímač je pomocí stejného klíče opět rozšifruje.
- UPOZORNĚNÍ:
- Šifrování funguje pouze u adresné komunikace mezi konkrétními zařízeními.
- Vysílání typu broadcast (jedna zpráva pro všechny) šifrovat nelze.
Nastavení šifrování se provádí pro každého partnera zvlášť. Každý senzor a centrála musí znát:
- MAC adresu druhého zařízení,
- společný šifrovací klíč.
V programu se zabezpečení zapíná pomocí struktury esp_now_peer_info_t:
peerInfo.encrypt = true;
Tím se aktivuje šifrovaná komunikace pro daného peera.
Šifrovací klíč LMK
Nejdůležitější částí nastavení je takzvaný LMK (Local Master Key). Je to vlastní klíč, který používají dvě zařízení pro vzájemné šifrování komunikace. Tento klíč není automaticky vytvořen ani vyměněn mezi moduly – musíme ho zadat ručně do programu.
Například:
uint8_t lmkKlic[16] = {
0x01,0x02,0x03,0x04,
0x05,0x06,0x07,0x08,
0x09,0x0A,0x0B,0x0C,
0x0D,0x0E,0x0F,0x10
};
Tento klíč musí být naprosto stejný v senzoru i v centrální jednotce. Pokud se liší jediný bajt klíče, vzájemná komunikace nebude fungovat.
Pro vlastní projekt je vhodné nepoužívat stejný testovací klíč jako je v následující ukázce, ale vytvořit si vlastní náhodnou šestnáctibajtovou hodnotu.
Omezení počtu šifrovaných zařízení
Je třeba si ale přiznat, že šifrování není „zadarmo“, neboť ESP32 má omezený počet uložených šifrovaných partnerů. Při běžném režimu Station (STA) lze mít maximálně přibližně 10 šifrovaných peerů, celkově lze mít kolem 20 peerů (šifrovaných i nešifrovaných). V režimu SoftAP nebo kombinovaném režimu je počet šifrovaných zařízení ještě nižší.
Ale pro běžnou domácí automatizaci, kde máme jednu centrálu a pět až osm teplotních senzorů, není toto omezení problém.
Nastavení šifrování ve vysílači (teploměru)
Ve vysílači se nejdříve uloží MAC adresa centrály:
uint8_t adresaPrijimace[] = {0x24, 0x62, 0xAB, 0x00, 0x00, 0x00};
Potom se nastaví společný klíč:
uint8_t lmkKlic[16] = {
0x01,0x02,0x03,0x04,
0x05,0x06,0x07,0x08,
0x09,0x0A,0x0B,0x0C,
0x0D,0x0E,0x0F,0x10
};
Při přidání centrály jako peera se zapne šifrování následujícím způsobem:
esp_now_peer_info_t peerInfo = {};
memcpy(peerInfo.peer_addr, adresaPrijimace, 6);
peerInfo.channel = 0;
peerInfo.encrypt = true;
memcpy(peerInfo.lmk, lmkKlic, 16);
Od této chvíle už senzor neposílá data otevřeně, ale šifrovaně.
Zde je celý kód šifrovaného teploměru:
#include <esp_now.h>
#include <WiFi.h>
// --------------------------------------------------
// MAC adresa centrální jednotky
// --------------------------------------------------
uint8_t adresaPrijimace[] = {0x24, 0x62, 0xAB, 0x00, 0x00, 0x00};
// --------------------------------------------------
// Šifrovací klíč ESP-NOW (LMK)
// MUSÍ být stejný jako v centrále
// Délka vždy 16 bajtů
// --------------------------------------------------
uint8_t lmkKlic[16] = {
0x01, 0x02, 0x03, 0x04,
0x05, 0x06, 0x07, 0x08,
0x09, 0x0A, 0x0B, 0x0C,
0x0D, 0x0E, 0x0F, 0x10
};
// --------------------------------------------------
// Struktura přenášených dat
// Musí být STEJNÁ jako v centrále
// --------------------------------------------------
typedef struct struct_teplomer {
int id;
float teplota;
} struct_teplomer;
// Instance dat
struct_teplomer mojeData;
// Callback po odeslání (Arduino Core 3+)
void OnDataSent(const esp_now_send_info_t *info,
esp_now_send_status_t status) {
Serial.print("Stav doručení: ");
Serial.println(
status == ESP_NOW_SEND_SUCCESS ?
"Úspěch" :
"Chyba"
);
}
void setup() {
Serial.begin(115200);
WiFi.mode(WIFI_STA);
WiFi.disconnect();
delay(100);
Serial.println();
Serial.println("=== ESP-NOW TEPLOMĚR ===");
Serial.print("MAC senzoru: ");
Serial.println(WiFi.macAddress());
// Inicializace ESP-NOW
if (esp_now_init() != ESP_OK) {
Serial.println("Chyba inicializace ESP-NOW");
return;
}
// Callback odesílání
esp_now_register_send_cb(OnDataSent);
// --------------------------------------------------
// Přidání šifrované centrály
// --------------------------------------------------
esp_now_peer_info_t peerInfo = {};
memcpy(peerInfo.peer_addr, adresaPrijimace, 6);
peerInfo.channel = 0;
peerInfo.encrypt = true;
memcpy(peerInfo.lmk, lmkKlic, 16); // Nastavení šifrovacího klíče
if (esp_now_add_peer(&peerInfo) != ESP_OK) {
Serial.println("Chyba při přidávání šifrovaného peera");
return;
}
Serial.println("ESP-NOW šifrovaný vysílač připraven");
}
void loop() {
// Testovací hodnoty
mojeData.id = 1;
mojeData.teplota = 25.5;
esp_err_t vysledek = esp_now_send(
adresaPrijimace,
(const uint8_t *)&mojeData,
sizeof(mojeData)
);
if (vysledek == ESP_OK) {
Serial.print("Odesláno - ID: ");
Serial.print(mojeData.id);
Serial.print(" Teplota: ");
Serial.print(mojeData.teplota);
Serial.println(" °C");
} else {
Serial.print("Chyba odeslání: ");
Serial.println(vysledek);
}
delay(5000);
}
Obrázek č. 6 opět ukazuje výstup v sériovém monitoru prostředí Arduino IDE při úspěšném odeslání (tentokráte šifrovaných) dat.
Nastavení centrální jednotky
Abychom mohli šifrovaná data úspěšně odeslat, musí být na jejich příjem připraven modul ESP32 na straně příjmu. Naše centrála musí v tuto chvíli mít stejné informace jako senzor – musí znát:
- MAC adresu senzoru,
- stejný LMK klíč.
Je libo více senzorů?
Pokud chceme použít více senzorů, každý senzor přidáme jako další šifrovaný peer. Například máme 3 teploměry:
// --------------------------------------------------
// MAC adresy všech senzorů
// --------------------------------------------------
uint8_t senzory[][6] = {
// Teploměr 1
{0x24, 0x6F, 0x28, 0x11, 0x22, 0x33},
// Teploměr 2
{0x30, 0xAE, 0xA4, 0x44, 0x55, 0x66},
// Teploměr 3
{0x48, 0x27, 0xE2, 0x77, 0x88, 0x99}
};
const int pocetSenzoru = sizeof(senzory) / sizeof(senzory[0]);
// --------------------------------------------------
// Společný šifrovací klíč LMK
// Musí být stejný v centrále i všech senzorech
// --------------------------------------------------
uint8_t lmkKlic[16] = {
0x01, 0x02, 0x03, 0x04,
0x05, 0x06, 0x07, 0x08,
0x09, 0x0A, 0x0B, 0x0C,
0x0D, 0x0E, 0x0F, 0x10
};
- Poznámka:
- Výraz
sizeof(senzory) / sizeof(je takovou malou „vychytávkou“, která slouží k automatickému zjištění počtu uložených senzorů v poli MAC adres. Celková velikost celého pole se vydělí velikostí jednoho prvku (jedné MAC adresy), čímž program zjistí počet senzorů. Díky tomu není nutné při přidání nebo odebrání senzoru ručně upravovat počet zařízení – stačí pouze změnit seznam MAC adres.senzory[0])
Funkce pro přidání šifrovaného senzoru
Když se nám podařilo trochu zautomatizovat nastavení počtu senzorů, můžeme použít i podobný přístup pro registraci senzorů. Místo několika kopií kódu uděláme jednu funkci, kterou opakovaně použijeme:
void pridejSifrovanyPeer(uint8_t *mac) {
esp_now_peer_info_t peerInfo = {};
memcpy(peerInfo.peer_addr, mac, 6);
peerInfo.channel = 0;
peerInfo.encrypt = true;
// vložení LMK klíče
memcpy(peerInfo.lmk, lmkKlic, 16);
if (esp_now_add_peer(&peerInfo) == ESP_OK) {
Serial.print("Přidán senzor: ");
Serial.printf(
"%02X:%02X:%02X:%02X:%02X:%02X\n",
mac[0], mac[1], mac[2],
mac[3], mac[4], mac[5]
);
}
else {
Serial.println("Chyba při přidávání senzoru");
}
}
V sekci setup() potom stačí:
for (int i = 0; i < pocetSenzoru; i++) {
pridejSifrovanyPeer(senzory[i]);
}
Lepší varianta pro větší síť
U více senzorů bychom ale doporučili dát každému senzoru vlastní LMK:
Například:
Senzor 1
MAC: 24:6F:28:11:22:33
LMK: 01 02 03 04 ...
Senzor 2
MAC: 30:AE:A4:44:55:66
LMK: AA BB CC DD ...
Senzor 3
MAC: 48:27:E2:77:88:99
LMK: 11 22 33 44 ...
Výhoda:
- Když někdo získá klíč jednoho senzoru, neohrozí ostatní.
- Lze jeden senzor vyřadit bez změny celé sítě.
Pak by struktura mohla být:
struct Senzor {
uint8_t mac[6];
uint8_t lmk[16];
};
Senzor seznamSenzoru[] = {
{
{0x24,0x6F,0x28,0x11,0x22,0x33}, // MAC prvního senzoru
{0x01,0x02,0x03,0x04,0x05,0x06, // LMK prvního senzoru
0x07,0x08,0x09,0x0A,0x0B,0x0C,
0x0D,0x0E,0x0F,0x10}
},
// zde by byla položka druhého senzoru
atd...
};
Ale je pravda, že na běžnou domácí meteostanici nebo síť několika teploměrů nám bohatě postačí použít jeden společný LMK, protože je to zkrátka jednodušší na správu. Pokud bychom stavěli větší systém (desítky čidel), asi by byla možná cesta unikátní MAC + unikátní klíč pro každý senzor.
Vraťme se ale k centrální jednotce!
V následující ukázce šifrované centrály používáme jeden společný klíč a dokonce zde máme jen jedno teplotní čidlo (vysílač):
#include <esp_now.h>
#include <WiFi.h>
// --------------------------------------------------
// Struktura přenášených dat
// Musí být STEJNÁ jako ve vysílači
// --------------------------------------------------
typedef struct struct_teplomer {
int id;
float teplota;
} struct_teplomer;
// Přijatá data
struct_teplomer prijataData;
// --------------------------------------------------
// MAC adresa senzoru
// Přidej zde MAC adresy všech senzorů
// --------------------------------------------------
uint8_t adresaSenzoru1[] = {0xAC, 0x67, 0xB2, 0x00, 0x00, 0x00};
// --------------------------------------------------
// Šifrovací klíč ESP-NOW (LMK)
// Musí být IDENTICKÝ na senzoru i centrále
// Délka vždy 16 bajtů
// --------------------------------------------------
uint8_t lmkKlic[16] = {
0x01, 0x02, 0x03, 0x04,
0x05, 0x06, 0x07, 0x08,
0x09, 0x0A, 0x0B, 0x0C,
0x0D, 0x0E, 0x0F, 0x10
};
// --------------------------------------------------
// Přidání šifrovaného peera
// --------------------------------------------------
void pridejPeer(uint8_t *mac) {
esp_now_peer_info_t peerInfo = {};
memcpy(peerInfo.peer_addr, mac, 6);
peerInfo.channel = 0;
peerInfo.encrypt = true;
memcpy(peerInfo.lmk, lmkKlic, 16);
if (esp_now_add_peer(&peerInfo) != ESP_OK) {
Serial.println("Chyba při přidávání šifrovaného partnera");
}
else {
Serial.println("Šifrovaný peer přidán");
}
}
// Callback příjmu dat Arduino Core 3+
void OnDataRecv(const esp_now_recv_info_t *info,
const uint8_t *incomingData,
int len) {
if (len != sizeof(prijataData)) {
Serial.print("Chybná velikost dat: ");
Serial.println(len);
return;
}
memcpy(&prijataData, incomingData, sizeof(prijataData));
Serial.print("\nPřijato od: ");
Serial.printf("%02X:%02X:%02X:%02X:%02X:%02X\n",
info->src_addr[0],
info->src_addr[1],
info->src_addr[2],
info->src_addr[3],
info->src_addr[4],
info->src_addr[5]);
Serial.print("Senzor ID: ");
Serial.println(prijataData.id);
Serial.print("Teplota: ");
Serial.print(prijataData.teplota);
Serial.println(" °C");
Serial.println("-----------------------");
}
void setup() {
Serial.begin(115200);
WiFi.mode(WIFI_STA);
WiFi.disconnect();
delay(100);
Serial.println();
Serial.println("=== ESP-NOW CENTRÁLA ===");
Serial.print("Moje MAC adresa: ");
Serial.println(WiFi.macAddress());
if (esp_now_init() != ESP_OK) {
Serial.println("Chyba inicializace ESP-NOW");
return;
}
// Registrace příjmu
esp_now_register_recv_cb(OnDataRecv);
// Přidání šifrovaného senzoru
pridejPeer(adresaSenzoru1);
Serial.println("Centrála připravena");
}
void loop() {
}
Funkčnost kódu nám opět ukazuje výstup na sériovém monitoru – viz obrázek č. 7.
Pokročilé zabezpečení: Doplnění klíče PMK
Ačkoliv výše uvedený kód funguje spolehlivě, z pohledu architektury ESP-NOW v něm chybí nastavení tzv. PMK (Primary Master Key).
Abychom si udělali jasno v tom, jaký je mezi klíči rozdíl:
- LMK (Local Master Key): Šifruje samotná uživatelská data (teplotu, stav čidla) odesílaná mezi dvěma konkrétními zařízeními.
- PMK (Primary Master Key): Slouží k zašifrování samotného klíče LMK.
Jak je možné, že předchozí příklady fungovaly i bez PMK? Knihovna ESP-NOW v sobě má integrovaný výchozí tovární PMK klíč. Pokud v kódu vlastní PMK nenastavíte, ESP32 automaticky použije tento předdefinovaný klíč od výrobce. Pro běžné domácí projekty to stačí, ale pokud chcete mít jistotu, že nepoužíváte univerzální klíč známý z dokumentace, je správné si definovat i vlastní PMK.
Jak přidat PMK do programu?
Klíč PMK je rovněž 16bajtové pole (stejně jako LMK) a nastavuje se globálně pro celé zařízení – tedy ještě předtím, než začnete přidávat jednotlivé peery. Používá se k tomu funkce esp_now_set_pmk().
Definice PMK klíče (musí být stejný na vysílači i přijímači):
uint8_t pmkKlic[16] = {
0x11, 0x22, 0x33, 0x44,
0x55, 0x66, 0x77, 0x88,
0x99, 0xAA, 0xBB, 0xCC,
0xDD, 0xEE, 0xFF, 0x00
};
Za úspěšnou inicializaci esp_ přidáme volání funkce pro nastavení PMK:
esp_now_set_pmk(pmkKlic);
Pokud tento krok provedeme na straně vysílače i centrály, data budou chráněna jak na úrovni jednotlivých relací (LMK), tak na úrovni nadřazeného master klíče (PMK).
Kompletní kódy s plným zabezpečením (PMK + LMK)
Pokud chceme mít jistotu, že naše komunikace nepoužívá žádné výchozí tovární nastavení, použijeme následující kompletní ukázky. V kódu přibyla definice klíče pmkKlic a jeho aktivace v sekci setup() pomocí esp_. Ostatní logika zůstává totožná s předchozí ukázkou.
Vysílač (Teploměr) s PMK a LMK
#include <esp_now.h>
#include <WiFi.h>
// --------------------------------------------------
// MAC adresa centrální jednotky
// --------------------------------------------------
uint8_t adresaPrijimace[] = {0x24, 0x62, 0xAB, 0x00, 0x00, 0x00};
// --------------------------------------------------
// Hlavní šifrovací klíč (PMK)
// Musí být stejný na vysílači i přijímači!
// Délka vždy přesně 16 bajtů
// --------------------------------------------------
uint8_t pmkKlic[16] = {
0x11, 0x22, 0x33, 0x44,
0x55, 0x66, 0x77, 0x88,
0x99, 0xAA, 0xBB, 0xCC,
0xDD, 0xEE, 0xFF, 0x00
};
// --------------------------------------------------
// Lokální šifrovací klíč peera (LMK)
// Musí být také stejný na vysílači i přijímači
// Délka vždy přesně 16 bajtů
// --------------------------------------------------
uint8_t lmkKlic[16] = {
0x01, 0x02, 0x03, 0x04,
0x05, 0x06, 0x07, 0x08,
0x09, 0x0A, 0x0B, 0x0C,
0x0D, 0x0E, 0x0F, 0x10
};
// --------------------------------------------------
// Struktura přenášených dat
// Musí být STEJNÁ jako v centrále
// --------------------------------------------------
typedef struct struct_teplomer {
int id;
float teplota;
} struct_teplomer;
// Instance dat
struct_teplomer mojeData;
// Callback po odeslání (Arduino Core 3+)
void OnDataSent(const esp_now_send_info_t *info,
esp_now_send_status_t status) {
Serial.print("Stav doručení: ");
Serial.println(
status == ESP_NOW_SEND_SUCCESS ?
"Úspěch" :
"Chyba"
);
}
void setup() {
Serial.begin(115200);
WiFi.mode(WIFI_STA);
WiFi.disconnect();
delay(100);
Serial.println();
Serial.println("=== ESP-NOW TEPLOMĚR ===");
Serial.print("MAC senzoru: ");
Serial.println(WiFi.macAddress());
// Inicializace ESP-NOW
if (esp_now_init() != ESP_OK) {
Serial.println("Chyba inicializace ESP-NOW");
return;
}
// --------------------------------------------------
// Nastavení globálního PMK klíče
// --------------------------------------------------
if (esp_now_set_pmk(pmkKlic) != ESP_OK) {
Serial.println("Chyba při nastavování PMK klíče");
return;
}
// Callback odesílání
esp_now_register_send_cb(OnDataSent);
// --------------------------------------------------
// Přidání šifrované centrály
// --------------------------------------------------
esp_now_peer_info_t peerInfo = {};
memcpy(peerInfo.peer_addr, adresaPrijimace, 6);
peerInfo.channel = 0;
peerInfo.encrypt = true;
memcpy(peerInfo.lmk, lmkKlic, 16); // Nastavení LMK klíče pro tohoto peera
if (esp_now_add_peer(&peerInfo) != ESP_OK) {
Serial.println("Chyba při přidávání šifrovaného peera");
return;
}
Serial.println("ESP-NOW plně šifrovaný vysílač připraven");
}
void loop() {
// Testovací hodnoty
mojeData.id = 1;
mojeData.teplota = 25.5;
esp_err_t vysledek = esp_now_send(
adresaPrijimace,
(const uint8_t *)&mojeData,
sizeof(mojeData)
);
if (vysledek == ESP_OK) {
Serial.print("Odesláno - ID: ");
Serial.print(mojeData.id);
Serial.print(" Teplota: ");
Serial.print(mojeData.teplota);
Serial.println(" °C");
}
else {
Serial.print("Chyba odeslání: ");
Serial.println(vysledek);
}
delay(5000);
}
Přijímač (Centrála) s PMK a LMK
Obdobně jako u vysílače (teploměru), tak i u naší šifrované centrály se v původním kódu změnilo jen následující:
- Definice klíče
pmkKlic: Přibylo 16bajtové pole s klíčem PMK. - Volání
esp_: Vkládá se ihned ponow_ set_ pmk( pmkKlic) esp_. Tím se nastavení uplatní pro celé zařízení ještě před registrací callbacků a přidáváním jednotlivých šifrovaných peerů.now_ init()
#include <esp_now.h>
#include <WiFi.h>
// --------------------------------------------------
// Struktura přenášených dat
// Musí být STEJNÁ jako ve vysílači
// --------------------------------------------------
typedef struct struct_teplomer {
int id;
float teplota;
} struct_teplomer;
// Přijatá data
struct_teplomer prijataData;
// --------------------------------------------------
// MAC adresa senzoru
// Přidej zde MAC adresy všech senzorů
// --------------------------------------------------
uint8_t adresaSenzoru1[] = {0xAC, 0x67, 0xB2, 0x00, 0x00, 0x00};
// --------------------------------------------------
// Hlavní šifrovací klíč ESP-NOW (PMK)
// Musí být IDENTICKÝ na senzoru i centrále
// Délka vždy 16 bajtů
// --------------------------------------------------
uint8_t pmkKlic[16] = {
0x11, 0x22, 0x33, 0x44,
0x55, 0x66, 0x77, 0x88,
0x99, 0xAA, 0xBB, 0xCC,
0xDD, 0xEE, 0xFF, 0x00
};
// --------------------------------------------------
// Šifrovací klíč ESP-NOW (LMK)
// Musí být IDENTICKÝ na senzoru i centrále
// Délka vždy 16 bajtů
// --------------------------------------------------
uint8_t lmkKlic[16] = {
0x01, 0x02, 0x03, 0x04,
0x05, 0x06, 0x07, 0x08,
0x09, 0x0A, 0x0B, 0x0C,
0x0D, 0x0E, 0x0F, 0x10
};
// --------------------------------------------------
// Přidání šifrovaného peera
// --------------------------------------------------
void pridejPeer(uint8_t *mac) {
esp_now_peer_info_t peerInfo = {};
memcpy(peerInfo.peer_addr, mac, 6);
peerInfo.channel = 0;
peerInfo.encrypt = true;
memcpy(peerInfo.lmk, lmkKlic, 16);
if (esp_now_add_peer(&peerInfo) != ESP_OK) {
Serial.println("Chyba při přidávání šifrovaného partnera");
}
else {
Serial.println("Šifrovaný peer přidán");
}
}
// Callback příjmu dat Arduino Core 3+
void OnDataRecv(const esp_now_recv_info_t *info,
const uint8_t *incomingData,
int len) {
if (len != sizeof(prijataData)) {
Serial.print("Chybná velikost dat: ");
Serial.println(len);
return;
}
memcpy(&prijataData, incomingData, sizeof(prijataData));
Serial.print("\nPřijato od: ");
Serial.printf("%02X:%02X:%02X:%02X:%02X:%02X\n",
info->src_addr[0],
info->src_addr[1],
info->src_addr[2],
info->src_addr[3],
info->src_addr[4],
info->src_addr[5]);
Serial.print("Senzor ID: ");
Serial.println(prijataData.id);
Serial.print("Teplota: ");
Serial.print(prijataData.teplota);
Serial.println(" °C");
Serial.println("-----------------------");
}
void setup() {
Serial.begin(115200);
WiFi.mode(WIFI_STA);
WiFi.disconnect();
delay(100);
Serial.println();
Serial.println("=== ESP-NOW CENTRÁLA (PMK + LMK) ===");
Serial.print("Moje MAC adresa: ");
Serial.println(WiFi.macAddress());
// Inicializace ESP-NOW
if (esp_now_init() != ESP_OK) {
Serial.println("Chyba inicializace ESP-NOW");
return;
}
// --------------------------------------------------
// Nastavení hlavního šifrovacího klíče (PMK)
// Volá se IHNED po inicializaci ESP-NOW před přidáváním peerů
// --------------------------------------------------
if (esp_now_set_pmk(pmkKlic) != ESP_OK) {
Serial.println("Chyba při nastavování PMK klíče");
return;
}
// Registrace příjmu
esp_now_register_recv_cb(OnDataRecv);
// Přidání šifrovaného senzoru
pridejPeer(adresaSenzoru1);
Serial.println("Centrála připravena");
}
void loop() {
}
Obousměrná komunikace (Full Duplex).
Poslední možnou komunikací, na kterou se zde podíváme, je obousměrná komunikace (Full Duplex). V tomto režimu každé zařízení vystupuje současně jako odesílatel i jako příjemce. To umožňuje vytvářet interaktivní systémy, kde centrála nejen sbírá data, ale také aktivně řídí chování koncových uzlů – například jim určuje, jak dlouho mají spát před dalším měřením.
Aby tato komunikace fungovala, musí si oba moduly vzájemně vyměnit své MAC adresy a zaregistrovat se jako partneři (peeři). Obě MAC adresy by mohly být zadané „napevno“, ale v našem následujícím příkladu si ukážeme případ, kdy centrála přijme zprávu od teploměru a tím si teprve tuto MAC zaregistruje a odpoví na ni.
Celý princip komunikace tedy bude následující:
- Senzor změří hodnotu (teplotu).
- Senzor odešle data centrále přes ESP-NOW.
- Centrála přijme data a zjistí MAC adresu senzoru.
- Centrála si uloží MAC senzoru jako ESP-NOW peer.
- Centrála vytvoří odpověď (nastavení intervalu spánku).
- Odpověď odešle zpět konkrétnímu senzoru (jehož MAC přijetím zprávy právě získala).
- Callback potvrdí, zda byla komunikace úspěšná.
Výsledkem bude základ jednoduché bezdrátové senzorové sítě, kde může k jedné centrále přistupovat více senzorů bez nutnosti předem do kódu centrály zapisovat jejich MAC adresy. Centrála se tak automaticky „učí“ jednotlivá zařízení při prvním kontaktu a následně s nimi může vést obousměrnou komunikaci. Počet senzorů se tak může „libovolně“ měnit.
Kód pro Senzor (Teploměr)
V ukázkovém příkladu senzor odesílá napevno nastavenou teplotu 25.5 °C a následně v callback funkci čeká na instrukci z centrály, ve které bude doba předepsaného spánku do dalšího měření. Přijatá hodnota doby spánku se ale v tomto (pouze ukázkovém) kódu nijak dále neaplikuje – doba „spánku“ (čekání) je v kódu napevno.
#include <esp_now.h>
#include <WiFi.h>
//================================================
// MAC adresa centrály
//================================================
uint8_t macCentraly[] = {0x24, 0x62, 0xAB, 0x00, 0x00, 0x00};
//================================================
// Data posílaná do centrály
//================================================
typedef struct struct_sensor {
float teplota;
} struct_sensor;
//================================================
// Odpověď z centrály
//================================================
typedef struct struct_odpoved {
int intervalSpanku;
} struct_odpoved;
struct_sensor dataSenzor;
struct_odpoved dataOdpoved;
// Callback potvrzení odeslání
void OnDataSent(const wifi_tx_info_t *info,
esp_now_send_status_t status) {
Serial.print("Odeslání: ");
if (status == ESP_NOW_SEND_SUCCESS) {
Serial.println("OK");
} else {
Serial.println("CHYBA");
}
}
// Callback příjmu odpovědi z centrály
void OnDataRecv(const esp_now_recv_info_t *info,
const uint8_t *incomingData,
int len) {
if (len != sizeof(dataOdpoved)) {
Serial.println("Chyba velikosti odpovedi!");
return;
}
memcpy(&dataOdpoved, incomingData, sizeof(dataOdpoved));
Serial.print("Centrala nastavila spanek: ");
Serial.print(dataOdpoved.intervalSpanku);
Serial.println(" sekund");
}
void setup() {
Serial.begin(115200);
WiFi.mode(WIFI_STA);
if (esp_now_init() != ESP_OK) {
Serial.println("ESP-NOW chyba!");
return;
}
// Registrace callbacků
esp_now_register_send_cb(OnDataSent);
esp_now_register_recv_cb(OnDataRecv);
// Přidání centrály jako peer
esp_now_peer_info_t peerInfo = {};
memcpy(peerInfo.peer_addr, macCentraly, 6);
peerInfo.channel = 0;
peerInfo.encrypt = false;
if (esp_now_add_peer(&peerInfo) != ESP_OK) {
Serial.println("Nelze pridat centralu!");
return;
}
Serial.println("---------------------");
Serial.println("Teplomer pripraven");
Serial.println("---------------------");
}
void loop(){
//----------------------------------------------
// Simulace měření teploty
//----------------------------------------------
dataSenzor.teplota = 25.5;
Serial.print("Odesilam teplotu: ");
Serial.print(dataSenzor.teplota);
Serial.println(" °C");
// Odeslání do centrály
esp_err_t vysledek;
vysledek = esp_now_send(
macCentraly,
(uint8_t *)&dataSenzor,
sizeof(dataSenzor)
);
if (vysledek != ESP_OK) {
Serial.println("Chyba pri odeslani");
}
// zatím pevný interval
delay(10000);
}
Pojďme se na tento program podívat trochu podrobněji. Na začátku programu je uložena MAC adresa centrály:
uint8_t macCentraly[] = {0x24, 0x62, 0xAB, 0x00, 0x00, 0x00};
Na rozdíl od centrály musí mít senzor MAC adresu protistrany známou předem. Důvodem je, že senzor jako první aktivně zahajuje komunikaci – musí tedy vědět, kam má naměřená data odeslat. Centrála naopak může zjišťovat adresy senzorů automaticky z příchozích zpráv.
Program používá dvě datové struktury. První struktura struct_ obsahuje data, která senzor odesílá:
typedef struct struct_sensor {
float teplota;
} struct_sensor;
To je zatím obdobné předešlým ukázkám. V reálném zařízení by zde mohly být další hodnoty, například: vlhkost, tlak, napětí baterie, stav vstupů, identifikace senzoru.
Druhá struktura struct_odpoved slouží pro data přijatá z centrály. Opět to odpovídá tomu, co jsme si zde již o komunikaci ESP-NOW říkali:
typedef struct struct_odpoved {
int intervalSpanku;
} struct_odpoved;
Centrála díky této struktuře může senzoru předávat řídicí informace. V tomto ukázkovém příkladu nastavuje interval spánku v sekundách – to umožňuje, aby centrála dynamicky měnila chování senzoru bez nutnosti přeprogramování tohoto zařízení.
Díky oboustranné komunikaci musí program nastavit dvě callback funkce:
esp_now_register_send_cb(OnDataSent);
esp_now_register_recv_cb(OnDataRecv);
Jedna je určena pro odeslání teploty, druhá pro přijetí doby spánku. Callback OnDataSent() se automaticky zavolá po každém pokusu o odeslání zprávy. Slouží k ověření, zda byla zpráva úspěšně doručena. Callback OnDataRecv() zajišťuje příjem odpovědi z centrály. Po přijetí zprávy nejprve kontroluje, zda velikost dat odpovídá očekávané struktuře:
if (len != sizeof(dataOdpoved))
Jak jsme si už dříve říkali, tím se alespoň částečně zabrání zpracování poškozených nebo nesprávných paketů.
Následně se data překopírují do proměnné incomingData a vypíší se na sériový port. Tím se zobrazí v sériovém monitoru hodnota zaslaná centrálou (doba spánku).
Následující obrázek č. 8 ukazuje úspěšné odeslání teploty a následné přijetí času spánku senzorem:
Kód pro Centrálu (Hlavní modul)
Práce centrály je poměrně obdobná. Centrála přijme ze senzoru hodnotu teploty a okamžitě odešle senzoru zpět informaci o tom, kdy má provést další měření (zde zatím nastavená pevná hodnota 600).
Nejdříve celý kód, pak se zaměříme na jeho důležité části.
#include <esp_now.h>
#include <WiFi.h>
//------------------------------------------------
// Data přijímaná ze senzoru
//------------------------------------------------
typedef struct {
float teplota;
} DataSensor;
//------------------------------------------------
// Data posílaná zpět senzoru
//------------------------------------------------
typedef struct {
int intervalSpanku;
} DataResponse;
DataSensor dataSensor;
DataResponse dataResponse;
//------------------------------------------------
// Pomocná funkce - výpis MAC adresy
//------------------------------------------------
void printMac(const uint8_t *mac) {
for (int i = 0; i < 6; i++) {
if (mac[i] < 16)
Serial.print("0");
Serial.print(mac[i], HEX);
if (i < 5)
Serial.print(":");
}
Serial.println();
}
//------------------------------------------------
// Přidání senzoru jako peer
//------------------------------------------------
bool addPeer(const uint8_t *mac) {
if (esp_now_is_peer_exist(mac))
return true;
esp_now_peer_info_t peer = {};
memcpy(peer.peer_addr, mac, 6);
peer.channel = 0;
peer.encrypt = false;
if (esp_now_add_peer(&peer) == ESP_OK) {
Serial.println("Senzor pridan jako peer");
return true;
}
Serial.println("Chyba pridani peeru");
return false;
}
// Přijetí dat ze senzoru
void OnDataRecv(const esp_now_recv_info_t *info,
const uint8_t *incomingData,
int len) {
if (len != sizeof(dataSensor)) {
Serial.println("Chybna velikost dat");
return;
}
memcpy(&dataSensor, incomingData, sizeof(dataSensor));
Serial.println("---------------------");
Serial.print("MAC senzoru: ");
printMac(info->src_addr);
Serial.print("Prijata teplota: ");
Serial.print(dataSensor.teplota);
Serial.println(" °C");
// přidání senzoru do peer seznamu
if (!addPeer(info->src_addr)) {
return;
}
// Vytvoreni odpovedi
dataResponse.intervalSpanku = 600;
Serial.print("Nastaven interval spanku: ");
Serial.print(dataResponse.intervalSpanku);
Serial.println(" sekund");
// Odeslani odpovedi
esp_err_t vysledek;
vysledek = esp_now_send(
info->src_addr,
(uint8_t *)&dataResponse,
sizeof(dataResponse)
);
if (vysledek == ESP_OK) {
Serial.println("Odpoved odeslana");
} else {
Serial.print("Chyba odeslani: ");
Serial.println(vysledek);
}
}
// Potvrzeni odeslani
void OnDataSent(const wifi_tx_info_t *info,
esp_now_send_status_t status) {
Serial.print("Stav odpovedi: ");
if (status == ESP_NOW_SEND_SUCCESS)
Serial.println("OK");
else
Serial.println("CHYBA");
}
void setup() {
Serial.begin(115200);
WiFi.mode(WIFI_STA);
Serial.print("MAC centraly: ");
Serial.println(WiFi.macAddress());
if (esp_now_init() != ESP_OK) {
Serial.println("ESP-NOW inicializace selhala");
return;
}
esp_now_register_recv_cb(OnDataRecv);
esp_now_register_send_cb(OnDataSent);
Serial.println("---------------------");
Serial.println("Centralni jednotka pripravena");
Serial.println("---------------------");
}
void loop() {
// Centrála pouze poslouchá
delay(1000);
}
Po spuštění programu se v části setup()nejdříve modul Wi-Fi přepne do režimu Station (WIFI_STA), který je pro ESP-NOW vyžadován. Následně se inicializuje samotný protokol ESP-NOW pomocí funkce esp_. Pokud inicializace proběhne úspěšně, centrála zaregistruje dva callbacky – jeden pro příjem zpráv (OnDataRecv) a druhý pro potvrzení úspěšnosti odeslání (OnDataSent) – to je obdobné situaci v kódu teploměru.
Asi není třeba připomínat, že pro výměnu dat musí program používat dvě stejné struktury, jako má program odesílatele (teploměru). Jsou to struktura DataSensor, která obsahuje údaje přicházející ze senzoru (teplota typu float), a struktura DataResponse obsahující data, která centrála posílá zpět senzoru (interval spánku v sekundách). Díky použití stejných struktur je komunikace přes ESP-NOW jednoduchá a přehledná, protože obě strany přesně vědí, jaký formát dat očekávat.
Hlavní část komunikace probíhá v callbacku OnDataRecv(), který se automaticky zavolá při přijetí zprávy. Nejprve se opět kontroluje velikost přijatého paketu, aby nedošlo ke zpracování neplatných dat. Pokud velikost odpovídá očekávané struktuře, data se pomocí funkce memcpy() překopírují do
proměnné dataSensor.
Součástí přijaté zprávy je také informace o MAC adrese zařízení, které zprávu poslalo. Tato adresa je dostupná v parametru info->src_addr. Centrála ji vypíše do sériového monitoru a následně ji ale hlavně použije pro zpětnou komunikaci. Díky tomu není nutné mít MAC adresy všech senzorů uložené přímo v programu centrály.
Před odesláním odpovědi centrála zkontroluje, zda je daný senzor již uložen jako ESP-NOW peer.
if (esp_now_is_peer_exist(mac)) …
Pokud ne, vytvoří nový záznam pomocí struktury esp_now_peer_info_t a přidá senzor funkcí esp_now_add_peer(). Tento krok je nutný, protože ESP-NOW vyžaduje, aby zařízení, kterému chceme aktivně posílat data, bylo nejprve registrováno jako známý protějšek.
Po úspěšném přidání senzoru centrála vytvoří odpověď. V ukázkovém programu nastaví hodnotu:
dataResponse.intervalSpanku = 600;
To představuje příkaz senzoru, aby po odeslání měření přešel na 600 sekund do režimu spánku. Následně se tato struktura odešle zpět na MAC adresu senzoru pomocí:
esp_now_send(
info->src_addr,
(uint8_t *)&dataResponse,
sizeof(dataResponse)
);
Druhý callback OnDataSent() slouží pouze k potvrzení výsledku odesílání. ESP32 zde získá informaci, zda byla odpověď úspěšně doručena (ESP_) nebo zda došlo k chybě.
Funkce loop() neobsahuje žádnou aktivní činnost. Centrála nemusí pravidelně nic vykonávat, protože veškerá komunikace probíhá událostně pomocí callbacků. Procesor pouze čeká na příchozí zprávy a reaguje na ně.
Výsledné chování centrály – tedy příjem teploty a odeslání času spánku vidíme na následujícím obrázku:
Souběžný provoz: ESP-NOW a Wi-Fi
Protokol ESP-NOW je navržen tak, aby mohl běžet paralelně s klasickým Wi-Fi připojením. V praxi to znamená, že čip ESP32 dokáže v jeden okamžik naslouchat zprávám z okolních „hloupých“ senzorů a zároveň být připojen k našemu domácímu routeru a data hned odesílat do internetu (do cloudu, databáze nebo přes MQTT).
Výhody tohoto řešení
- Koncové senzory nemusí znát heslo k Wi-Fi ani podstupovat zdlouhavý proces připojování k routeru. Pouze se na zlomek sekundy probudí, „vyštěknou“ data přes ESP-NOW a okamžitě usnou.
- Pouze jedno zařízení (brána) je vystaveno internetu a spravuje síťovou komunikaci, což zvyšuje bezpečnost a přehlednost systému.
- Komunikace v lokální části (mezi senzory a bránou) probíhá s minimálním zpožděním, zatímco brána se postará o „pomalejší“ doručení dat do cloudu.
Toto řešení je základním stavebním kamenem pokročilé domácí automatizace, kdy lokální síť rychlých senzorů tvoří jeden funkční celek propojený s globálním světem internetu věcí (IoT).
Při kombinování obou technologií narazíte na jedno zásadní technické omezení:
Všechna zařízení musí pracovat na stejném Wi-Fi kanálu.
Jakmile se centrální modul (Gateway) připojí k vašemu Wi-Fi routeru, automaticky převezme kanál, který tento router používá (např. kanál 6). Všechny ostatní senzory, které chtějí s touto bránou komunikovat přes ESP-NOW, musí být nastaveny na tento identický kanál. Pokud by senzory vysílaly na kanálu 1 a brána byla kvůli routeru na kanálu 6, data nebudou doručena!
Pro souběžný běh je nutné nastavit modul brány do režimu WIFI_STA (případně WIFI_), aby se mohl připojit k internetu. Nejprve se modul připojí k routeru pomocí standardního WiFi.begin(. Po úspěšném připojení je dobré zjistit, na jakém kanálu router vysílá, a tento kanál nastavit i pro ESP-NOW část. Následně se spustí protokol ESP-NOW a zaregistruje se callback pro příjem dat. Jakmile přijde zpráva ze senzoru (např. teplota), kód uvnitř této funkce data zpracuje a okamžitě spustí proces odeslání do internetu (např. HTTP POST požadavek na webový server).
- Poznámka ke zpracování dat:
- Doporučuje se přijatá data z callback funkce pouze uložit do globální proměnné a samotný HTTP požadavek provést v hlavní smyčce
loop(), aby se neblokovalo zpracování dalších paketů.
Ukázkový kód brány (Receiver + HTTP Client)
Následující program je jakousi Wi-Fi centrálou, která přijme strukturu s teplotou a hodnotu teploty odešle jako parametr GET požadavku na zvolenou adresu (URL http://).
#include <esp_now.h>
#include <WiFi.h>
#include <HTTPClient.h>
// Nastavení Wi-Fi pro připojení k internetu
const char* ssid = "Vase_WiFi_SSID";
const char* password = "Vase_Heslo";
// Struktura musí odpovídat té na straně vysílače
typedef struct struct_message {
float teplota;
} struct_message;
struct_message prijataData;
bool novaData = false;
// Callback funkce při přijetí dat přes ESP-NOW
void OnDataRecv(const esp_now_recv_info_t *info, const uint8_t *incomingData, int len) {
memcpy(&prijataData, incomingData, sizeof(prijataData));
novaData = true; // Nastavení příznaku pro loop()
}
void setup() {
Serial.begin(115200);
// 1. Připojení k Wi-Fi routeru
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("\nWiFi připojeno k internetu.");
// 2. Inicializace ESP-NOW (po inicializaci WiFi)
if (esp_now_init() != ESP_OK) {
Serial.println("Chyba inicializace ESP-NOW");
return;
}
// 3. Registrace callbacku pro příjem
esp_now_register_recv_cb(OnDataRecv);
}
void loop() {
if (novaData && WiFi.status() == WL_CONNECTED) {
HTTPClient http;
// Sestavení URL s daty (GET parametry)
String serverPath = "http://example.com/data-storage.php?temp=" + String(prijataData.teplota);
Serial.print("Odesílám HTTP GET: ");
Serial.println(serverPath);
http.begin(serverPath.c_str());
int httpResponseCode = http.GET(); // Provedení requestu
if (httpResponseCode > 0) {
Serial.printf("Odpověď serveru: %d\n", httpResponseCode);
} else {
Serial.printf("Chyba při HTTP požadavku: %d\n", httpResponseCode);
}
http.end();
novaData = false; // Reset příznaku
}
}
Je MAC skutečně neměnná?
Na závěr našeho článku je ještě důležité zmínit, že ačkoliv má každý čip ESP32 od výroby přidělenou unikátní hardwarovou MAC adresu, v případě potřeby ji lze softwarově změnit. Je však zároveň nutné zdůraznit, že se jedná o čistě softwarovou záležitost. Tato změna je platná pouze po dobu běhu programu a není trvale zapsána do hardwaru čipu. Jakmile dojde k resetu modulu nebo jeho odpojení od napájení, čip se při novém startu automaticky vrátí ke své původní tovární MAC adrese.
Možnost změny MAC je v bastlířské praxi vcelku neocenitelná například ve chvíli, kdy v rozsáhlejší síti senzorů potřebujete vyměnit jeden vadný kus za druhý, aniž byste museli složitě přepisovat MAC adresu příjemce v kódech všech ostatních modulů.
Pro změnu MAC adresy v prostředí Arduino IDE se používá funkce esp_wifi_set_mac z knihovny esp_. Změnu MAC
je nutné provést po nastavení WiFi režimu, ale ještě před samotnou inicializací protokolu ESP-NOW. Tímto způsobem lze vytvořit vlastní systém adresování, který bude nezávislý na konkrétních kusech hardwaru, což výrazně usnadňuje údržbu a rozšiřování vašich projektů postavených na ESP-NOW.
Ukázka kódu pro změnu MAC adresy:
#include <esp_now.h>
#include <WiFi.h>
uint8_t novaMac[6] = {0x02, 0x11, 0x22, 0x33, 0x44, 0x55};
String aktualniMac() {
uint8_t mac[6];
esp_wifi_get_mac(WIFI_IF_STA, mac);
char buf[18];
sprintf(buf, "%02X:%02X:%02X:%02X:%02X:%02X",
mac[0], mac[1], mac[2],
mac[3], mac[4], mac[5]);
return String(buf);
}
void setup() {
Serial.begin(115200);
delay(1000);
WiFi.mode(WIFI_STA);
delay(100);
Serial.print("Původní MAC: ");
Serial.println(aktualniMac());
esp_err_t err = esp_wifi_set_mac(WIFI_IF_STA, novaMac);
if (err == ESP_OK) {
Serial.println("Nová MAC byla úspěšně nastavena.");
} else {
Serial.printf("Chyba: %d\n", err);
return;
}
Serial.print("Aktuální MAC: ");
Serial.println(aktualniMac());
Serial.print("WiFi.macAddress(): ");
Serial.println(WiFi.macAddress());
}
void loop() {
}
- POZNÁMKA:
- Při přechodu na Arduino Core 3.x pro ESP32 došlo k několika klíčovým změnám, které vyžadují úpravu kódu. Jednou z nejvýznamnějších úprav, na kterou je nutné si dát pozor, je způsob změny MAC adresy zařízení. Zatímco ve starších verzích (Core 2.x) bylo možné předat ukazatel na celou
strukturu pole pomocí operátoru
&, novější verze postavená na aktualizovaném ESP-IDF vyžaduje přímé předání pole jako ukazatele (tedy bez počátečního&). Ignorování této změny vede k chybám při překladu, a proto je nutné starší skripty důkladně zkontrolovat a případně upravit.
Na obrázku vidíme výstup v sériovém monitoru prostředí Arduino IDE:
esp_wifi_get_mac() a WiFi.macAddress().Výše uvedený obrázek ukazuje jednu zásadní věc. Ve výpisu vidíme zprávu o úspěšné změně MAC a pak její načtení dvěma různými metodami – nízkoúrovňovou funkcí esp_wifi_get_mac() a „arduiňáckou“ funkcí WiFi.macAddress(). A ejhle, obě hodnoty se liší!
Původní MAC: AC:67:B2:26:XX:XX:XX
Nová MAC (driver): 02:11:22:33:44:55
WiFi.macAddress(): AC:67:B2:XX:XX:XX
- Důležitá poznámka pro Arduino ESP32 Core 3.x
- V Arduino ESP32 Core 3.x funkce
WiFi.macAddress(), jak vidíme, vrací původní tovární MAC adresu, i když byla aktivní MAC adresa ve Wi-Fi driveru úspěšně změněna. V tomto případě bude pro komunikaci ESP-NOW používána MAC adresa02:. Hodnota z11: 22: 33: 44: 55 WiFi.je tedy (asi) neaktualizovaná informace z Arduino vrstvy a je otázkou, kdy tato funkce začne akceptovat novou změněnou hodnotu. V projektech s ESP32, ve kterých je třeba používat změnu MAC adresy, pro zjištění MAC proto vždy doporučujeme používat funkcimacAddress() esp_místowifi_ get_ mac() WiFi..macAddress()
Závěr
Protokol ESP-NOW představuje jeden z nejvýkonnějších a nejflexibilnějších nástrojů v ekosystému čipů ESP32. Nabízí unikátní kombinaci extrémně nízké latence, minimální spotřeby energie a jednoduchosti, díky čemuž se skvěle hodí pro širokou škálu aplikací od bateriových senzorů až po rychlá dálková ovládání. Schopnost přímé komunikace mezi zařízeními bez nutnosti WiFi routeru odstraňuje složitou režii síťových protokolů a umožňuje vašim projektům reagovat v řádu milisekund.
I přes svá drobná omezení, jako je limit 250 bajtů na zprávu nebo dosah závislý na prostředí, se jedná o neuvěřitelně spolehlivé řešení, které nevyžaduje žádný další hardware. Jak jsme si ukázali, možnosti topologií jsou téměř neomezené – od jednoduchého broadcastu přes adresné spojení až po komplexní brány propojující lokální senzory s internetem.
Teorie už ale bylo dost! Nezapomeňte, že číst tutoriály je sice fajn pro začátek, ale to pravé dobrodružství začíná až ve chvíli, kdy se vám pod rukama začnou motat dráty a v kódu se objeví první chyby, které sice celý projekt zdrží, ale tvůrce posunou zase o kus dál. Skutečné mistrovství v bastlení totiž nezískáte z knih a internetových tutoriálů, ale tím, že své vize proměníte v realitu – i kdyby to měl být jen bezdrátový teploměr, co vám pošle data z lednice.
Tak tedy hurá do toho... 3, 2, 1, …NOW!