W 2019 roku inżynierowie firmy Espressif podjęli trud pracowania nowego interfejsu komunikacji radiowej, który miał być prostszy niż Wi-Fi i Bluetooth, oferować większy zasięg i zapewniać wystarczający poziom bezpieczeństwa, aby nie dało się łatwo podsłuchać wiadomości, ani zastosować techniki ataku typu reply attack.
ESP-NOW wykorzystuje częstotliwość 2,4 GHz – podobnie jak Wi-Fi i Bluetooth, nie potrzeba zatem żadnych dodatkowych zasobów sprzętowych poza tymi, które moduły ESP32 (a także stare ESP8266) mają już wbudowane. Wynika z tego bardzo istotny wniosek: komunikację przez ESP-NOW można zastosować nawet w urządzeniach, które zostały wyprodukowane przed rokiem 2019. Wystarczy zaktualizować oprogramowanie i już można korzystać z możliwości ESP-NOW.
Interfejs ten umożliwia dwukierunkową komunikację pomiędzy dwoma urządzeniami, a także komunikację typu „jeden do wszystkich”. Urządzenia można ze sobą sparować (ale nie zawsze jest taka potrzeba), dzięki czemu można bardzo łatwo tworzyć proste sieci. Parowanie urządzeń ma zastosowanie w celu wprowadzenia szyfrowania – połączone w ten sposób urządzenia zapisują sobie klucz szyfru AES-128.
Zasięg komunikacji jest całkiem dobry. Testy przeprowadzone przez wielu radioamatorów dowodzą, że przy pomocy ESP-NOW można przesyłać dane na odległość ok. 300 metrów, a w trybie long range nawet 500...800 metrów w otwartej przestrzeni.
ESP-NOW ma jednak pewną wadę – działa tylko i wyłącznie na modułach produkowanych przez firmę Espressif. Zatem wybierając ten interfejs skreślamy jednocześnie możliwość komunikacji z modułami firm Nordic, Microchip czy jakiejkolwiek innej. Póki co nie jest pewne, czy Espressif kiedykolwiek umożliwi wykorzystywanie swojego interfejsu na sprzęcie innych producentów.
Interfejs ESP-NOW wciąż dynamicznie się rozwija i ciągle dodawane są nowe funkcjonalności. Dlatego przed przystąpieniem do testów koniecznie zainstaluj najnowszego MicroPythona. W chwili pisania tego odcinka dostępna jest wersja 1.27.0 i to z nią zgodne są wszystkie przykłady. Wiadomo już teraz, że MicroPython 1.27.0 obsługuje ESP-NOW w wersji 1.0, ale dostępny jest także ESP-NOW 2.0, który obsługiwać można póki co tylko w ESP-IDF, a w MicroPythonie jeszcze nie. Zasadnicza różnica jest taka, że stara wersja ESP-NOW umożliwia wysyłanie wiadomości o długości maksymalnie 250 bajtów, a nowa wersja – 1470 bajtów. Być może obsługa ESP-NOW 2.0 zostanie dodana w MicroPythonie 1.28.0.
Trochę teorii
Jak już wspomniano, ESP-NOW wykorzystuje transmiter Wi-Fi, zatem oprócz zaimportowania modułu odpowiedzialnego za ESP-NOW, musimy także dodać moduł obsługujący sieć Wi-Fi.
import espnow
Wszystkie moduły z rodziny ESP32 mają po dwie karty sieciowe – jedna może pracować jako klient (station), a druga jako access point. Musimy utworzyć instancję jednej z nich, a następnie ją aktywować. Jeżeli chcemy wykorzystać kartę sieciową klienta, należy użyć poniższego polecenia:
Jeżeli chcemy wykorzystać access point do ESP-NOW, wystarczy tylko zmienić argument przekazywany do konstruktora klasy WLAN:
Obie karty sieciowe mogą być użyte na potrzeby ESP-NOW i mają takie same możliwości (ale inny adres MAC!). W praktycznych zastosowaniach może się zdarzyć, że oprócz ESP-NOW będziemy chcieli mieć dostęp do sieci Wi-Fi i Internetu. W takiej sytuacji możemy zastosować kartę sieciową station zupełnie normalnie na potrzeby dostępu do Internetu, a kartę access point – do obsługi ESP-NOW.
Niezależnie od tego, którą kartę sieciową wybierzemy, musimy ją aktywować poleceniem:
Identyfikatorem wszystkich urządzeń w sieci ESP-NOW jest adres MAC karty sieciowej, która obsługuje transmisję bezprzewodową. Możemy ten adres pozyskać w następujący sposób:
print(f"MAC Address: {mac}")
Tak odczytany adres MAC zapisany zostaje w zmiennej typu bytes, składającej się z 6 bajtów. Wyświetlając je funkcją print uzyskamy mniej więcej taki rezultat:
Nie wygląda to jak adres MAC, jaki widzimy na komputerach, gdzie poszczególne bajty adresu oddzielone są znakiem dwukropka. Aby wyświetlić adres MAC w sposób tradycyjny, możemy posłużyć się funkcją hexlify z modułu binascii.
Funkcja hexlify przekształca ciąg bajtów na ich reprezentację ASCII, wykorzystując cyfry w zakresie 0–9 i litery a–f. Drugim argumentem tej funkcji jest separator, jaki ma oddzielać bajty i musi to być jeden znak. Funkcja hexlify zwraca obiekt typu bytes, czyli poprzedzony jest znakiem b'. Aby się go pozbyć, musimy bytes przekonwertować na string, korzystając z metody decode. Ostatnim etapem jest zamiana małych liter na wielkie przy pomocy metody upper. Po takim zabiegu zobaczymy w konsoli adres MAC w tradycyjnej postaci, do której wszyscy są przyzwyczajeni:
'DC:DA:0C:1E:4E:E0'
Możemy teraz przejść do obsługi klasy ESP-NOW. Najpierw musimy utworzyć i aktywować jej instancję:
e.active(True)
Metoda active przyjmuje argument True lub False, aby aktywować lub dezaktywować klasę, zaś jeżeli wywołamy ją bez argumentu, wówczas metoda zwróci informację, czy klasa jest obecnie aktywna.
Aby wysłać wiadomość musimy wywołać metodę send, która przyjmuje trzy argumenty:
Pierwszy z nich to adres MAC odbiorcy. Możemy wykorzystać także adres specjalny b'\xFF\xFF\xFF\xFF\xFF\xFF', aby wysłać wiadomość do wszystkich urządzeń. Możemy także podać None, jeżeli chcemy wysłać wiadomość do wszystkich urządzeń, jakie zostały wcześniej sparowane.
Drugi argument to dane do wysłania, podane jako string, bytes lub bytearray.
Trzeci argument jest opcjonalny. Jeżeli podamy True, to klasa sprawdzi, czy wiadomość została odczytana przez wybranego odbiorcę lub wszystkich sparowanych odbiorców. W razie niepowodzenia, metoda zgłosi wyjątek ETIMEDOUT. Jest to domyślne zachowanie tej metody. Jeżeli podamy False, wówczas metoda nie będzie sprawdzać faktu odebrania wiadomości i zakończy działanie natychmiast po wysłaniu wiadomości.
Oto przykład wysłania krótkiej wiadomości tekstowej do wszystkich odbiorców, jacy są w zasięgu:
Odebrane wiadomości są najpierw zapisywane do bufora klasy ESPNow, gdzie czekają na odczytanie przez nasz program. Odebrać je możemy na trzy sposoby. Pierwszym z nich jest wykorzystanie metody recv, która zwraca dane w postaci krotki (mac, msg), gdzie mac to adres nadawcy podany jako bytes, a msg to dane jako bytearray. Jeżeli w buforze nie ma żadnych danych, funkcja zwraca krotkę (None, None).
Metoda recv przyjmuje tylko jeden argument – timeout, czyli czas oczekiwania w milisekundach. Możliwe są następujące opcje:
- 0 – brak oczekiwania. Funkcja natychmiast kończy pracę, jeżeli w buforze nie ma żadnych danych,
- liczba dodatnia – czas w milisekundach, na który program jest zawieszany w oczekiwaniu na wiadomość,
- liczba ujemna – funkcja czeka w nieskończoność,
- None lub brak argumentu – funkcja zachowuje się zgodnie z konfiguracją domyślną lub konfiguracją użytkownika, ustaloną przy pomocy metody config.
Inny sposób to użycie metody irecv. Działa dokładnie tak samo, jak recv, ale zwracając dane nie alokuje nowych zmiennych w pamięci, lecz zwraca wskaźniki do wewnętrznego bufora klasy. Dlatego też używanie tej funkcji jest wskazane w przerwaniach. Metoda irecv również przyjmuje argument timeout, który działa tak samo, jak w recv.
Trzecia metoda to recvinto. Jej drugim argumentem jest wartość timeout, tak samo jak w poprzednio omawianych metodach. Pierwszym argumentem musi być lista, do której zostaną zapisane dane zwracane przez metodę. Pierwszym elementem tej listy musi być zmienna typu bytearray o długości 6 bajtów, służąca do zapisywania adresu MAC nadawcy. Drugim elementem listy jest również bytearray, w którym zostaje zapisana wiadomość o długości maksymalnie 250 bajtów.
Wspomniano, że metoda irecv może być z powodzeniem wykorzystywana w przerwaniach. Najpierw jednak musimy skonfigurować klasę, wywołując w tym celu metodę irq, która przyjmuje tylko jeden argument – nazwę funkcji, która ma zostać uruchomiona, kiedy zostanie odebrana jakaś wiadomość. Ta funkcja również musi przyjmować jeden argument – jest on instancją klasy ESPNow, która odebrała wiadomość.
Należy pamiętać, że jeżeli ESP32 odbierze jakąś wiadomość zanim skonfigurujemy obsługę przerwań, wówczas klasa nie wywoła funkcji obsługującej przerwania, a wiadomość będzie czekała w buforze tak długo, aż przyjdzie jakaś następna wiadomość. Dopiero wtedy uruchomi się obsługa przerwań. Dobrym zwyczajem jest, aby po konfiguracji przerwań ręcznie sprawdzić, czy są jakieś wiadomości oczekujące w buforze, a jeżeli tak, to samodzielnie wywołać funkcję obsługującą przerwanie.
Kolejną metodą klasy ESPNow jest any. Sprawdza ona, czy w buforze są jakieś wiadomości do odczytania i w takiej sytuacji zwraca True. Metoda nie przyjmuje żadnych argumentów.
Aby przesyłać wiadomość do wybranego przez nas urządzenia, najpierw musimy je sparować przy pomocy metody add_peer. Jako argument podajemy adres MAC urządzenia, które ma odebrać wiadomość. Adres musi być zapisany w postaci obiektu bytes o długości 6 bajtów. Urządzenie odbierające nie musi być sparowane z urządzeniem nadającym – pod warunkiem, że transmisja nie jest szyfrowana. Oto przykład takiej operacji:
e.add_peer(peer_mac)
Pamiętaj, że jeżeli chcesz wysyłać wiadomości do wszystkich urządzeń przy pomocy adresu b'\xFF\xFF\xFF\xFF\xFF\xFF', to również musisz go wcześniej dodać do listy sparowanych adresów.
Jeżeli chcemy wprowadzić szyfrowanie wiadomości, najpierw na wszystkich urządzeniach musimy określić 16-bajtowy klucz PMK (Primary Master Key) przy pomocy metody set_pmk. Na każdym urządzeniu w naszej sieci klucz PMK musi być identyczny:
e.set_pmk(pmk)
Kolejnym krokiem jest sparowanie nadajnika i odbiornika, przy czym w tym przypadku to odbiorca musi sparować się z nadawcą. Potrzebujemy do tego klucz LMK (Local Master Key), który również ma długość 16 bajtów. Następnie wywołujemy metodę add_peer z dodatkowymi argumentami, jak na przykładzie poniżej:
lmk = b'\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F'
e.add_peer(peer_mac, lmk, encrypt=True)
Aby usunąć powiązane urządzenie, należy posłużyć się funkcją del_peer(mac), a żeby dostać różne informacje na jego temat, należy wywołać metodę get_peer(mac). Możemy także wyświetlić wszystkie dodane urządzenia przy pomocy get_peers().
Ćwiczenia praktyczne
Aby zrealizować ćwiczenia, zaprezentowane w tym odcinku kursu, potrzebować będziemy co najmniej dwie płytki z modułami ESP32 lub ESP32-S3. Jedna będzie pełniła rolę nadajnika, a druga będzie odbiornikiem, który wyświetli w konsoli odebrane wiadomości.
Na listingu 1 przedstawiono kod najprostszego możliwego odbiornika ESP-NOW – składa się on zaledwie z kilkunastu linijek. Linia 1 służy do tego, aby adres MAC odbiornika wyświetlić w konsoli. Należy go skopiować i wkleić do kodów nadajnika w odpowiednim miejscu.
import espnow
import network
sta = network.WLAN(network.STA_IF)
sta.active(True)
mac = sta.config('mac')
print(f"MAC Address: {mac}") # 1
e = espnow.ESPNow()
e.active(True)
while True: # 2
sender, data = e.recv(-1) # 3
data = data.decode() # 4
print(f"{sender} -> {data}") # 5
Listing 1. Kod pliku receiver.py
Po zainicjalizowaniu sieci WLAN oraz klasy ESPNow, program ogranicza się do jednej pętli nieskończonej while True (linia 2).
W linii 3 widzimy wywołanie metody recv z argumentem -1, co oznacza, że program zatrzymuje się w tym miejscu i czeka w nieskończoność, aż zostanie odebrana jakaś wiadomość. Kiedy to nastąpi, adres MAC nadawcy oraz dane wiadomości zostają zapisane do zmiennych sender oraz data.
Obie zmienne są typu bytes. Moglibyśmy je wyświetlić już teraz, jednak w konsoli dostalibyśmy napis poprzedzony znakiem b', a wszystkie polskie znaki diakrytyczne byłyby wyświetlone jako kody znaków UTF-8. Aby przekształcić obiekt bytes na ładnie wyglądający string, musimy wywołać metodę decode (linia 4). Pozostaje już tylko wyświetlić nadawcę oraz wiadomość, co robimy w linii 5.
Uruchamiamy kod w edytorze Thonny. Na konsoli wyświetli się adres MAC. Należy go skopiować i przejść do listingu 2, gdzie znajduje się kod najprostszego możliwego nadajnika. W linii 1 znajduje się zmienna peer_mac, gdzie musimy wkleić adres MAC odbiornika. W linii 2 tworzymy także zmienną everyone, do której zapisujemy uniwersalny adres MAC. Spowoduje to wysłanie wiadomości do wszystkich odbiorników, jakie są w zasięgu nadajnika.
import espnow
import network
sta = network.WLAN(network.STA_IF)
sta.active(True)
mac = sta.config('mac')
print(f"MAC Address: {mac}")
e = espnow.ESPNow()
e.active(True)
peer_mac = b'\xd8\xa0\x1di\x9fD' # 1
everyone = b'\xFF\xFF\xFF\xFF\xFF\xFF' # 2
e.add_peer(peer_mac) # 3
e.add_peer(everyone)
e.send(everyone, "Wiadomość do wszystkich") # 4
e.send(peer_mac, "Wiadomość do wybranego odbiorcy") # 5
e.send(peer_mac, "Wiadomość do wybranego odbiorcy # 6
bez potwierdzenia odbioru", False)
Listing 2. Kod pliku sender.py
Zanim zaczniemy wysłać wiadomości, musimy te adresy MAC dodać przy pomocy metody add_peer, co jest realizowane w linii 3 i kolejnej.
W linii 4 wysyłamy wiadomość do wszystkich. W kolejnej linii 5 znajduje się polecenie wysłania wiadomości tylko do jednego odbiorcy. Jeżeli odbiorca nie odbierze tej wiadomości, to dostaniemy wyjątek ETIMEDOUT. Jeżeli nie interesuje nas sprawdzanie, czy wiadomość została doręczona, możemy dodać argument False (linia 6).
Przejdźmy teraz do listingu 3, gdzie znajduje się kod odbiornika korzystającego z przerwań. Pojawiła się w nim funkcja receive_cb (linia 1), która jest wywoływana po odebraniu wiadomości. Ponieważ mogłoby się zdarzyć, że zostanie odebranych kilka wiadomości zanim funkcja zostanie wywołana, to przy uruchomieniu jej musimy przeprowadzić obsługę wszystkich wiadomości znajdujących się już w buforze odbiorczym. W tym celu w programie pojawiła się pętla while, która wykonuje się tak długo, aż metoda any zwróci False (linia 2), czyli do momentu całkowitego opróżnienia bufora odbiorczego. Obsługa wiadomości wygląda bardzo podobnie do tej, którą przedstawiono na listingu 1, a jedyna różnica jest taka, że zamiast metody recv wykorzystujemy irecv (linia 3).
import espnow
import network
sta = network.WLAN(network.STA_IF)
sta.active(True)
mac = sta.config('mac')
print(f"MAC Address: {mac}")
e = espnow.ESPNow()
e.active(True)
def receive_cb(e): # 1
while e.any(): # 2
sender, data = e.irecv() # 3
data = data.decode()
print(f"{sender} -> {data}")
e.irq(receive_cb) # 4
if e.any(): # 5
receive_cb(e)
Listing 3. Kod pliku receiver_irq.py
Aby obsługa przerwań działała, musimy ją skonfigurować i w linii 4 wskazujemy, jaka funkcja ma się wywoływać po odebraniu wiadomości. Istnieje prawdopodobieństwo, że zostanie odebrana jakaś wiadomość, zanim jeszcze skonfigurujemy przerwania. W takiej sytuacji przerwanie nie zostanie zgłoszone, a wiadomość będzie czekać w buforze, aż zostanie odebrana jakaś kolejna wiadomość i dopiero wtedy zostanie wywołana funkcja przerwania. Aby uniknąć takiej sytuacji, musimy sprawdzić przy pomocy metody any, czy w buforze odbiorczym coś się znajduje i jeżeli tak, to ręcznie wywołać funkcję receive_cb (linia 5).
Listing 4 demonstruje, w jaki sposób można wprowadzić szyfrowanie do odbiornika wiadomości. W linii 1 definiujemy klucz PMK. Pamiętajmy, że transmisja szyfrowana wymaga, aby odbiornik był sparowany z nadajnikiem. Zatem w linii 2 dodajemy go metodą add_peer, gdzie podajemy jego adres MAC, a także klucz LMK.
import espnow
import network
sta = network.WLAN(network.STA_IF)
sta.active(True)
mac = sta.config('mac')
print(f"MAC Address: {mac}")
e = espnow.ESPNow()
e.active(True)
e.set_pmk(b'\x00\x11\x22\x33\x44\x55\x66\x77 # 1
\x88\x99\xAA\xBB\xCC\xDD\xEE\xFF')
sender_mac = b'\xdc\xda\x0c\x1eN\xe0'
e.add_peer(sender_mac, b'\x00\x01\x02\x03\x04\x05\x06\x07\x08 # 2
\x09\x0A\x0B\x0C\x0D\x0E\x0F', encrypt=True)
while True:
sender, data = e.recv(-1)
data = data.decode()
print(f"{sender} -> {data}")
Listing 4. Kod pliku receiver_encrypted.py
Analogicznie postępujemy tworząc kod nadajnika, który jest przedstawiony na listingu 5. W liniach 1 podajemy taki sam klucz PMK, a w linii 2 dodajemy odbiornik z identycznym kluczem LMK.
import espnow
import network
sta = network.WLAN(network.STA_IF)
sta.active(True)
mac = sta.config('mac')
print(f"MAC Address: {mac}")
e = espnow.ESPNow()
e.active(True)
e.set_pmk(b'\x00\x11\x22\x33\x44\x55\x66\x77 # 1
\x88\x99\xAA\xBB\xCC\xDD\xEE\xFF')
receiver_mac = b'\xd8\xa0\x1di\x9fD'
e.add_peer(receiver_mac, b'\x00\x01\x02\x03\x04\x05\x06\x07\x08 # 2
\x09\x0A\x0B\x0C\x0D\x0E\x0F', encrypt=True)
e.send(receiver_mac, "Szyfrowana wiadomość")
Listing 5. Kod pliku sender_encrypted.py
Kolejny przykład pokazuje, w jaki sposób można uzyskać większy zasięg połączenia, ale kosztem 4-krotnego zmniejszenia prędkości transmisji. Zobaczmy kod z listingu 6. W linii 1 zmieniamy konfigurację sieci WLAN. Przestawiamy jej protokół z normalnego Wi-Fi na PROTOCOL_LR, który jest autorskim rozwiązaniem inżynierów firmy Espressif. Tak skonfigurowany nadajnik może komunikować się tylko z odbiornikami, które zostały skonfigurowane w ten sam sposób. Ponadto musimy określić, który kanał pasma chcemy wykorzystywać. W kodach demonstracyjnych autorzy podają kanał 6, choć nie tłumaczą dlaczego – nie wchodząc w szczegóły również zastosujemy ten sam kanał.
import espnow
import network
sta = network.WLAN(network.STA_IF)
sta.active(True)
sta.config(channel=6, protocol=network.WLAN.PROTOCOL_LR) # 1
mac = sta.config('mac')
print(f"MAC Address: {mac}")
e = espnow.ESPNow()
e.active(True)
e.config(rate=espnow.RATE_LORA_250K) # 2
while True:
sender, data = e.recv(-1)
data = data.decode()
print(f"{sender} -> {data}")
Listing 6. Kod pliku receiver_long_range.py
Kolejnym krokiem jest zmodyfikowanie konfiguracji klasy ESPNow, co robimy w linii 2. Tak skonfigurowana klasa będzie pracować w trybie długiego zasięgu z prędkością transmisji 250 kbit/s. Kod nadajnika znajdziemy na listingu 7. Należy go skonfigurować tak samo i w taki sam sposób jak odbiornik.
import espnow
import network
sta = network.WLAN(network.STA_IF)
sta.active(True)
sta.config(channel=6, protocol=network.WLAN.PROTOCOL_LR) # 1
mac = sta.config('mac')
print(f"MAC Address: {mac}")
e = espnow.ESPNow()
e.active(True)
e.config(rate=espnow.RATE_LORA_250K) # 2
everyone = b'\xFF\xFF\xFF\xFF\xFF\xFF'
e.add_peer(everyone)
e.send(everyone, "Test długiego zasięgu")
Listing 7. Kod pliku sender_long_range.py
To wszystko na temat komunikacji przez ESP-NOW. W następnym odcinku, który będzie już ostatnią częścią kursu MicroPythona, zobaczymy w jaki sposób możemy połączyć moduł ESP32 z telefonem poprzez Bluetooth.
Dominik Bieczyński
leonow32@gmail.com
• Repozytorium kursu na GitHubie https://github.com/leonow32/micropython
• Dokumentacja klasy ESP-NOW https://docs.micropython.org/en/latest/library/espnow.html