Moderní BLE s modulem aioble
Díl druhý: Přechod na vyšší úroveň – knihovna aioble
Dosud jsme pro komunikaci v MicroPythonu používali základní modul bluetooth. Ten je sice funkční, ale práce s ním je kvůli nutnosti ručního zpracování událostí a binárních paketů poměrně „bolestivá“. V tomto článku si představíme knihovnu aioble, která staví na moderním asynchronním programování asyncio. Uvidíme, jak tato vyšší vrstva umožňuje psát kód, který je mnohem čitelnější, blíže standardnímu Pythonu a dokáže efektivně odbavovat více úloh najednou – například číst senzory a zároveň obsluhovat Bluetooth spojení.
Minulý článek Nízkoúrovňové BLE v MicroPythonu jsme uzavřeli dvěma praktickými ukázkami pro modul ESP32. Ty přehledně ukázaly dvě hlavní oblasti práce s Bluetooth (BLE) – jak modul řídit a jak z něj načítat stavové informace. Tyto předchozí ukázky využívaly nízkoúrovňovou knihovnu bluetooth, ve které bylo nutné ručně řešit aktivaci BLE, registraci služeb, obsluhu IRQ událostí, správu připojení i znovuspuštění advertisingu po odpojení. Pro jednoduché demonstrace je to užitečné, protože je dobře vidět, jak BLE funguje „odspodu“. V praktických projektech ale bývá takový kód poměrně dlouhý a hůře čitelný.
Nízkoúrovňový přístup je výborný pro studium, ale při psaní větších programů začne být kód poměrně složitý a nepřehledný. Proto se dnes posuneme o úroveň výš. Knihovna aioble přináší elegantní asynchronní přístup, díky kterému nemusíte řešit nízkoúrovňové detaily typu „na handle 14 dorazila událost 3“. Místo toho píšete kód přirozeně podle jeho významu – například „uživatel právě zapsal do charakteristiky LED“.
Srovnejme si oba přístupy jen v načtení hodnoty:
Jelikož knihovna aioble staví na modulu asyncio (známe z článku Multitasking v MicroPythonu na ESP32), dokáže na události čekat elegantně a bez blokování zbylého kódu. To je ideální pro projekty, kde současně:
- komunikujeme přes BLE,
- čteme senzory,
- blikáme LED,
- obsluhujeme tlačítka,
- nebo zobrazujeme data na displeji.
Jak aioble nainstalovat do ESP32
Tady je důležité rozlišit dvě věci: Python v počítači (Windows/Linux/macOS) versus MicroPython uvnitř ESP32.
V Thonny existuje nabídka Nástroje → Spravovat pluginy, ale v tomto případě nainstaluje balíčky do Pythonu
běžícího na počítači, nikoli do MicroPythonu v ESP32. Pokud se po instalaci objeví balíček v počítači, ale import aioble na ESP32 nefunguje, pak knihovna nebyla nahrána do zařízení. Navíc tato verze si s tímto MicroPythonem nerozumí ani po přenesení do modulu ESP32.
- Naše zkušenost je tedy následující:
- Stáhnout knihovnu
aioblepro MicroPython přímo z GitHubu a rovnou ji nakopírovat do modulu ESP32. - Oficiální zdroj knihovny je: micropython/micropython-lib.
- Stáhneme kompletně celý adresář
aioblea nahrajeme do složky/libv modulu ESP32.
V Thonny to provedete takto:
- Otevřete správce souborů v pravém panelu.
- V části Zařízení otevřete (nebo vytvoříme) složku
lib. - Přetáhněte do ní celý adresář
aioble.
Výsledná struktura má vypadat asi takto:
/lib
/aioble
__init__.py
core.py
device.py
server.py
client.py
peripheral.py
central.py
...
Ověření instalace
Po nahrání restartujeme modul ESP32 ikonou STOP a v REPLu zkusíme:
import aioble
Pokud se nevypíše chyba, je knihovna správně nainstalována.
Máme-li úspěšně nainstalováno, můžeme se pustit do další práce.
Praktické příklady
Než zkusíme předělat naše dva ukázkové příklady do podoby s využitím modulu aioble, zkusíme si trochu ukázat, jak vyřešit některé standardní BLE situace.
Vytvoření služby s aioble
# UUID služby
SERVICE_UUID = bluetooth.UUID('12345678-1234-5678-1234-56789abcdef0')
# UUID
charakteristiky CHAR_UUID = bluetooth.UUID('12345678-1234-5678-1234-56789abcdef1')
# vytvoření služby
service = aioble.Service(SERVICE_UUID)
# charakteristika pro čtení i zápis
char = aioble.Characteristic(
service,
CHAR_UUID,
read=True,
write=True,
notify=True
)
# registrace do BLE stacku
aioble.register_services(service)
Kde vlastnosti charakteristiky nastavujeme pomocí konstant: read, write a notify. Výchozí hodnoty jsou False, takže se do kódu vpisují pouze pokud je nastavujeme na hodnotu True.
Zapsání a načtení hodnoty na straně ESP32
Při výše definovaném objektu char, který nám umožňuje přístup k charakteristice UUID (1234), můžeme hodnotu charakteristiky číst:
char.write(b'Ahoj')nebo zapisovat:
data = char.read()
print(data)Čekání na zápis z telefonu
Modul aioble se využívá ve spolupráci s modulem asyncio, který umožňuje paralelní běh zadaných částí programu. Hlavní části programu se tedy řeší v paralelních samostatně běžících funkcích.
Následující funkce tedy čeká v nekonečné smyčce, až aplikace (např. nRF Connect) něco do charakteristiky zapíše:
async def wait_for_write():
while True:
connection, data = await char.written()
print('Přišlo:', data)
asyncio.create_task(wait_for_write())
Poslání notifikace do telefonu
Důležité pro nás bude i odeslání notifikace do telefonu:
char.write(b'25.3')
char.notify(connection)
Například zde každou vteřinu (dokud je telefon připojen) odešleme text + postupně se zvyšující hodnotu:
async def send_values(connection):
i = 0
while connection.is_connected():
text = f'Hodnota {i}'
char.write(text.encode())
char.notify(connection)
i += 1
await asyncio.sleep(1)
V předešlé ukázce využíváme zjištění stavu připojení:
connection.is_connected()
Pokud bychom chtěli počkat na odpojení, použili bychom:
await connection.disconnected()
Advertising
Při advertisingu se opět využívá spolupráce modulu aioble s modulem asyncio, což umožní paralelní běh zadaných částí programu. Advertising se tak spouští v samostatně běžící funkci:
async def advertise():
while True:
async with await aioble.advertise(
250_000, # interval v mikrosekundách
name='ESP32-BLE', # jmeno zařízení
services=[SERVICE_UUID], # nabízená služba
) as connection:
print('Připojeno:', connection.device)
await connection.disconnected()
print('Odpojeno')
asyncio.run(advertise())
Následující tabulka nám ukazuje přehled základních funkcí, které můžeme při řešení BLE serveru potřebovat
| Funkce | K čemu je |
|---|---|
aioble.Service() |
vytvoření služby |
aioble.Characteristic() |
vytvoření charakteristiky |
aioble.register_services() |
registrace služeb |
aioble.advertise() |
spuštění advertisingu |
char.write() |
nastavení hodnoty |
char.read() |
čtení lokální hodnoty |
await char.written() |
čekání na zápis z klienta |
char.notify(conn) |
odeslání notifikace |
Pokud nám předešlé fragmenty kódu alespoň trochu naznačily, jak lze řešit BLE komunikaci pomocí modulu aioble, zkusíme se pustit do ukázek celých kódů.
V následujících dvou praktických příkladech si vyzkoušíme vytvořit stejné úlohy jako v minulém článku, tedy
- rozsvícení vestavěné LED na modulu ESP32 pomocí připojeného telefonu
- načtení stisku tlačítka
BOOTpřipojeným telefonem.
Porovnání stejných příkladů jasně ukáže, jak s modulem aioble řešit známé věci modernějším způsobem.
V obou případech si uvedeme nejdříve hotový program, pak si jej postupně rozebereme.
PŘÍKLAD č. 1: Program s aioble – ovládání LED
import asyncio
import aioble
import bluetooth
from machine import Pin
# Definice UUID a hardware (LED na GPIO 2)
SVC_UUID = bluetooth.UUID("6e400001-b5a3-f393-e0a9-e50e24dcca9e")
LED_UUID = bluetooth.UUID("6e400002-b5a3-f393-e0a9-e50e24dcca9e")
led = Pin(2, Pin.OUT)
# Registrace služby a charakteristiky s právem pro zápis
service = aioble.Service(SVC_UUID)
led_char = aioble.Characteristic(service, LED_UUID, write=True, capture=True)
aioble.register_services(service)
async def led_task():
while True:
# Čekání na zápis od klienta (např. z mobilu)
conn, value = await led_char.written()
led.value(1 if value == b"1" else 0)
print("Stav LED změněn na:", value)
async def peripheral_task():
while True:
# Asynchronní vysílání (Advertising)
async with await aioble.advertise(250000, name="ESP32-LED", services=[SVC_UUID]) as connection:
print("Připojeno:", connection.device)
await connection.disconnected()
async def main():
print("Čekám na připojení a vysílám ESP32-LED...")
await asyncio.gather(led_task(), peripheral_task())
asyncio.run(main())
Program začíná importem potřebných modulů:
import asyncio
import aioble
import bluetooth
from machine import Pin
Modul asyncio slouží pro asynchronní běh programu a aioble jako vyšší vrstva nad nízkoúrovňovou knihovnou bluetooth.
Stejně jako v předchozím nízkoúrovňovém kódu jsou v kódu definovány UUID služby a charakteristiky:
SVC_UUID = bluetooth.UUID("6e400001-b5a3-f393-e0a9-e50e24dcca9e")
LED_UUID = bluetooth.UUID("6e400002-b5a3-f393-e0a9-e50e24dcca9e")
Tyto hodnoty musí odpovídat UUID použitým v klientské aplikaci. Služba reprezentuje funkci „ovládání LED“ a charakteristika slouží k přenosu příkazů.
Obdobně jako v minulém programu i zde musíme nastavit GPIO, na kterém je připojena vestavěná LED, na režim digitálního výstupu.
led = Pin(2, Pin.OUT)
V nízkoúrovňové knihovně bluetooth by bylo nutné ručně nastavovat příznaky charakteristiky a zpracovávat události zápisu. V aioble je vytvoření zapisovatelné charakteristiky jednodušší. Nejdříve nastavíme UUID služby a vzniklou proměnnou service, hned využijeme při zaregistrování její charakteristiky s LED_UUID:
service = aioble.Service(SVC_UUID)
led_char = aioble.Characteristic(service, LED_UUID, write=True, capture=True)
Parametr write= povolí zápis z klienta a capture= zajistí, že program může na jednotlivé zápisy čekat pomocí asynchronní metody written().
Proměnná led_char je pro nás nyní objektem, pomocí jehož metod a atributů můžeme pracovat s právě vytvořenou GATT strukturou.
Předchozí sdělení bylo mírně předčasné. Abychom mohli se službou a její charakteristikou dále pracovat, je třeba ji nejprve zaregistrovat. Registrace služby se provede jediným poměrně jednoduchým příkazem:
aioble.register_services(service)
A to je z hlediska přípravy komunikace celé!
Hlavní logika příjmu dat je umístěna ve funkci led_.
async def led_task():
while True:
# Čekání na zápis od klienta (např. z mobilu)
conn, value = await led_char.written()
led.value(1 if value == b"1" else 0)
print("Stav LED změněn na:", value)
Uvnitř nekonečné smyčky program čeká na zápis od klienta:
conn, value = await led_char.written()
To je jeden z největších přínosů aioble a vůbec celého asynchronního pojetí programu. Není potřeba obsluha IRQ ani nějaké ruční čtení bufferů. Program se zde jednoduše v této části „zasekne“ čekáním na zápis od klienta a pokračuje až ve chvíli, kdy klient skutečně zapíše nová data. Nejde však o uvíznutí programu, protože díky příznaku async tato funkce uvolní procesor pro další běh zbylé části programu.
Metoda written() vrací dvojici:
conn– objekt reprezentující připojeného klienta,value– přijatá data typubytes.
Předpokládáme, že klient – aplikace nRF Connect nebo, jak příště uvidíme, i webová stránka – odesílá data jako bajty b"1" nebo b"0", proto se přijatá hodnota porovnává také s bajty, nikoli s nějakým textovým řetězcem. V následujícím zápisu jsme si neodpustili trochu typicky pythonovského zápisu. Koho toto podmíněné přiřazení děsí, jistě to zvládne zapsat klasickou podmínkou.
led.value(1 if value == b"1" else 0)
b"1"→ LED se zapne- cokoliv jiného → LED se vypne
Následně se přijatá hodnota vypíše do sériové konzole:
print("Stav LED změněn na:", value)
Toto pojetí práce je přesně to, co jsme slibovali na samém začátku – zbavené příznaků připojení, rutin přerušení… apod. Někdo by řekl, že to už není taková „nízkoúrovňová“ legrace, ale z pohledu projektu, kde je BT komunikace jen určitou součástí, to pojetí s modulem aioble asi také není špatné, ne?
ALE pozor, ještě nemáme hotovo!
Máme tu ještě druhou asynchronní úlohu. Tato úloha nazvaná peripheral_ zajišťuje advertising a správu připojení. To jsme totiž vůbec neřešili.
Klíčovým je zde konstruktor:
async with await aioble.advertise(250000, name="ESP32-LED", services=[SVC_UUID]) as connection:
Konstrukce async with ... as connection: je asynchronní kontextový manažer. Stará se o životní cyklus vysílání i samotného spojení. Jakmile blok kódu skončí (nebo nastane chyba), automaticky zajistí bezpečné úklidové práce – zastaví reklamní vysílání a ukončí spojení. Při úspěšném připojení dostanete v proměnné connection objekt reprezentující navázanou relaci, přes který můžeme sledovat stav nebo komunikovat s připojeným telefonem.
Díky await se pozastaví asynchronní úloha a počká, dokud proces vysílání nezačne a neproběhne úspěšný pokus o připojení ze strany klienta.
Metoda aioble. knihovny aioble, udělá z modulu ESP32 BLE periferii a začne do okolí vysílat identifikační pakety. S následujícími parametry:
250000: Interval vysílání v mikrosekundách – ESP32 pošle signalizační paket každou čtvrt sekundu. Kratší interval znamená rychlejší vyhledání zařízením, ale vyšší spotřebu energie.name=: Název zařízení obsažený v reklamním paketu. Tento text uvidíte při skenování okolí (např. v aplikaci nRF Connect)."ESP32-LED" services=: Seznam UUID služeb, které zařízení poskytuje. Mobil díky tomu už před samotným připojením ví, jaké funkce toto BT nabízí.[SVC_UUID]
V nízkoúrovňové implementaci by bylo nutné ručně sestavovat advertising paket. Dokonce jsme v minulém článku v rámci advertisingu propagovali jen název zařízení. Zde vidíme, že můžeme propagovat i poskytovanou službu services=, což je z hlediska BLE komunikace „slušnější“.
Na nízké úrovni jsme museli i zpracovávat události připojení a odpojení a znovu spouštět advertising. To se zde řeší automaticky v rámci nekonečné smyčky v asynchronní funkci peripheral_.
K dokončení popisu funkce peripheral_ je třeba ještě dodat, že po úspěšném připojení se vypíše informace o právě připojeném klientovi:
print("Připojeno:", connection.device)
a program opět v této části „zamrzne“ čekáním na odpojení:
await connection.disconnected()
Zajímavé je, že funkce pro příjem dat i ta pro advertising běží současně. Dokonce to tak musí být! To umožňuje funkce asyncio.:
await asyncio.gather(led_task(), peripheral_task())
Tento řádek kódu v MicroPythonu slouží k paralelnímu (souběžnému) spuštění více úloh a k počkání, až všechny dojedou do konce. Funkce asyncio. vezme všechny předané úlohy, zabalí je dohromady a předá je řídicí smyčce (event loop). Vlastně řekne mikrokontroleru: „Začni řešit všechny tyto úlohy naráz.“. Díky tomu funkce led_task() neustále čeká na zápisy do charakteristiky, zatímco peripheral_ se stará o BLE připojení. Obě úlohy běží paralelně v rámci jediné asynchronní smyčky – to je hodně velká výhoda celého řešení.
Celý program se ale ve skutečnosti spouští až na konci kódu příkazem:
asyncio.run(main())
Knihovna aioble společně s knihovnou asyncio výrazně nejen zjednodušuje kód, ale i základní filozofii a celkový běh. Právě díky tomu je aioble velmi vhodná pro řešení BLE komunikace a proto se s ní tak často ve výukových tutoriálech projektů s ESP32 a MicroPythonem setkáváme. I když nás první setkání s ní na samém začátku může docela dobře vyděsit.
Nyní můžeme program spustit na modulu ESP32 a na mobilu spustit aplikaci nRF Connect. Po vyhledání BT zařízení v blízkém okolí by se mělo objevit zařízení ESP32-LED. Po připojení bychom měli vidět stejnou GATT strukturu, jako v příkladu předešlého článku. Z hlediska služby a její charakteristiky se totiž použitím modulu aioble nic nezměnilo.
Z pohledu připojeného klienta bychom neměli poznat, jakým způsobem je ovládací program na straně modulu ESP32 naprogramován.
PŘÍKLAD č. 2: Program s aioble – načítání tlačítka
Ještě se podíváme, jak modul aioble řeší charakteristiky s vlastností Notify. Poslouží nám to opět jako ukázka pro porovnání nízkoúrovňové obsluhy BLE komunikace a přínosy modulu aioble.
Zde je uveden celý program, který si následně rozebereme:
import asyncio
import aioble
import bluetooth
from machine import Pin
# BOOT tlačítko (GPIO0)
button = Pin(0, Pin.IN, Pin.PULL_UP)
# UUID služby a charakteristiky
UUID_SVC = bluetooth.UUID("a1b2c300-1234-5678-9abc-def012345678")
UUID_BTN = bluetooth.UUID("a1b2c301-1234-5678-9abc-def012345678")
# BLE služba a notifikační charakteristika
service = aioble.Service(UUID_SVC)
btn_char = aioble.Characteristic(service, UUID_BTN, notify=True)
aioble.register_services(service)
async def button_task(connection):
last = button.value()
while connection.is_connected():
state = button.value()
if state != last:
last = state
# 1 = stisknuto, 0 = uvolněno
value = b"\x01" if state == 0 else b"\x00"
btn_char.notify(connection, value)
print("Tlačítko:", "STISK" if state == 0 else "UVOLNĚNO")
await asyncio.sleep_ms(50)
async def main():
print("Čekám na připojení a vysílám jméno ESP32-BUTTON...")
while True:
async with await aioble.advertise(
250_000,
name="ESP32-BUTTON",
services=[UUID_SVC],
) as connection:
print("Zařízení připojeno:", connection.device)
await button_task(connection)
print("Zařízení odpojeno")
asyncio.run(main())
I zde knihovna aioble výrazně zjednodušuje implementaci BLE, a to především implementaci notifikací. Oproti nízkoúrovňové knihovně bluetooth není opět potřeba řešit IRQ události, správu conn_handle ani ruční sestavování advertising paketu. Program se tak může soustředit pouze na logiku aplikace.
Po vytvoření služby a charakteristiky:
service = aioble.Service(UUID_SVC)
btn_char = aioble.Characteristic(service, UUID_BTN, notify=True)
je charakteristika automaticky nastavena pro BLE notifikace. To zde zaručuje parametr notify=, který nyní nahrazuje ruční práci s příznakem _FLAG_.
Hlavní logika je ve funkci button_. Všimněme si, že nyní zde není nekonečná smyčka, ale, že vnitřní smyčka běží pouze po dobu, kdy je klient připojen:
while connection.is_connected():
Tlačítko je zde testováno naprosto klasickým způsobem, kdy testujeme, zda nedošlo ke změně jeho stavu. Při změně stavu tlačítka se odešle jediný bajt:
value = b"\x01" if state == 0 else b"\x00"
0x01– tlačítko stisknuto0x00– tlačítko uvolněno
Samotná notifikace se odešle až příkazem:
btn_char.notify(connection, value)
U příkladu s ovládáním LED zapisoval data klient do ESP32 (write), zatímco zde ESP32 aktivně odesíláme změny klientovi (notify).
Odeslání notifikace vypíšeme pro kontrolu na straně modulu ESP32 na sériový port (viz obrázek č. 2).
print("Tlačítko:", "STISK" if state == 0 else "UVOLNĚNO")
Krátké zpoždění v této pracovní smyčce:
await asyncio.sleep_ms(50)
omezuje frekvenci kontroly tlačítka a zároveň ponechává prostor pro běh dalších asynchronních úloh.
Připojení a advertising zajišťuje konstrukce:
async with await aioble.advertise(
250_000,
name="ESP32-BUTTON",
services=[UUID_SVC],
) as connection:
kterou jsme ale již poznali v minulém příkladu.
Všimněme si, jak je v tomto druhém příkladu strukturovaná hlavní smyčka main(). Na rozdíl od prvního příkladu, kde se úlohy spouštěly paralelně pomocí asyncio., je zde volání aioble. umístěno přímo uvnitř cyklu while True. Důvodem je způsob, jakým fungují notifikace:
- Předání objektu
connection:Aby mohlo ESP32 poslat notifikaci (btn_char), musí přesně vědět, komu ji posílá. Vyžaduje k tomu živý objekt connection..notify - Vazba na životní cyklus: Tento objekt vzniká až v momentě, kdy se klient (např. mobil) připojí skrze blok:
async with await aioble.advertise(...) as connection:
Funkcibutton_task(proto musíme zavolat uvnitř tohoto bloku a vzniklé spojení jí předat.connection)
Jakmile se mobil odpojí, button_task doběhne, blok async with zanikne a vnější smyčka while True v main() automaticky spustí nové vysílání pro dalšího zájemce.
(V prvním příkladu s LED jsme toto řešit nemuseli – metoda written() přijímá data globálně na úrovni rozhraní a spojení si sama odchytí až v momentě zápisu, díky čemuž mohla běžet jako zcela nezávislá úloha na pozadí.)
Z pohledu BLE tento program představuje typický scénář ESP32 ↔ nRF Connect (obrázek č. 3). Modul ESP32 posílá informaci o každém stisku nebo uvolnění tlačítka a díky nastavené charakteristice typu Notify telefon získává okamžitou informaci o změně stavu tlačítka. Právě tento princip se používá u většiny senzorů, tlačítek a dalších zařízení, která mají klienta informovat o změně stavu bez nutnosti neustálého dotazování.
Závěr
Přechodem od nízkoúrovňového modulu bluetooth k vyšší vrstvě aioble získáváme v MicroPythonu nesrovnatelně čistší, čitelnější a udržovatelnější kód. Díky stavbě na asynchronním frameworku asyncio odpadá ruční obsluha rutin přerušení (IRQ) i složitá správa stavů. Kód reaguje na události přirozeně a neblokuje přitom ostatní paralelní úlohy – ať už jde o měření senzorů, obsluhu displeje, nebo řízení motorů.
Dosud jsme pro testování a interakci s ESP32 používali univerzální mobilní aplikaci nRF Connect. To je skvělé pro vývoj, ale pro běžného uživatele asi málo přívětivé.
V příštím díle se proto podíváme na to, jak nRF Connect úplně vynechat a jak vytvořit vlastní grafické rozhraní přístupné přímo z prohlížeče. Ukážeme si, jak lze naše BLE zařízení řídit a monitorovat přímo z běžné webové stránky pomocí technologií Web Bluetooth.