ESP
DIYer
32
LAB

Moderní mikrokontrolér v rukou „bastlíře“

Nízkoúrovňové BLE v MicroPythonu

Když jsme začali psát původní seriál MicroPythonu na modulu ESP32, problematice Bluetooth jsme se záměrně vyhnuli. Důvody nás k tomu vedly hned dva:

  1. Programování Bluetooth není pro začátek úplně jednoduché – na nízké úrovni vyžaduje poměrně dost nastavování a obsluhu přerušení a na vyšší úrovni pak paralelní zpracování úloh. A to zrovna není téma ideální pro začátečnický kurz.
  2. Samotnou technologii a komunikaci Bluetooth (nejen na modulu ESP32) nemáme zrovna moc v lásce.

I když oba výše zmíněné důvody lze považovat za „závažné“, neměly by nám ale bránit v tom, abychom si o Bluetooth v MicroPythonu nakonec něco řekli.

Čeká nás tedy čtyřdílný miniseriál, který by nám používání Bluetooth v MicroPythonu na modulu ESP32 měl alespoň trochu představit.

Díl první: Začínáme s BT…

V tomto prvním díle se ponoříme do samotných základů. Vysvětlíme si, jak funguje hierarchická databáze GATT, k čemu slouží služby a charakteristiky a jakou roli hrají unikátní identifikátory UUID. Naučíme se používat vestavěný modul bluetooth, který představuje nízkoúrovňovou cestu k ovládání Bluetooth stacku přímo v MicroPythonu.


Když dnes vezmete do ruky chytrý telefon, bezdrátová sluchátka, fitness náramek nebo chytrý teploměr, téměř jistě v nich najdete technologii Bluetooth. Původně Bluetooth vzniklo jako náhrada kabelů. Místo propojení telefonu se sluchátky nebo počítače s myší pomocí vodiče měla zařízení komunikovat bezdrátově. Postupem času se však ukázalo, že stejná technologie je ideální i pro malé senzory, měřicí přístroje a různá IoT zařízení. Dnes tedy Bluetooth neslouží jen k přenosu hudby, ale i k odesílání teploty, vlhkosti, stavu baterie nebo jednoduchých ovládacích příkazů.

Právě zde začíná být zajímavý modul ESP32. Tento čip má Bluetooth integrované přímo v sobě, takže není potřeba žádný externí modul. V MicroPythonu tak můžeme během několika minut vytvořit zařízení, které telefon najde, připojí se k němu a začne si s ním vyměňovat data. To pro bastlíře představuje další možnou cestu, jak propojit vlastní elektroniku s mobilem.

Bluetooth Classic a Bluetooth Low Energy

Mnoho lidí si představuje Bluetooth jako jednu jedinou technologii. Ve skutečnosti dnes existují dvě hlavní varianty, které jsou určeny pro odlišné úlohy.

První z nich je Bluetooth Classic. To najdeme například v bezdrátových sluchátkách, reproduktorech nebo autorádiích. Je navrženo pro trvalý přenos většího množství dat, což umožňuje plynulé streamování zvuku. Aby toho dosáhlo, musí být rádiová část zařízení aktivní téměř neustále. Výsledkem je vyšší datová propustnost, ale tím pádem také vyšší spotřeba energie.

Druhou variantou je Bluetooth Low Energy, zkráceně BLE. Jak už název napovídá, hlavním cílem zde není maximální rychlost, ale minimální spotřeba. Představme si bezdrátový teploměr, který jednou za pět sekund odešle hodnotu teploty. Samotný přenos trvá jen několik milisekund a po zbytek času může zařízení setrvávat v režimu spánku. Díky tomu vydrží fungovat na jednu baterii celé měsíce nebo dokonce roky.

Je důležité pochopit, že BLE používá úplně jiný způsob organizace dat, protože je navrženo pro krátké, úsporné a často nepravidelné přenosy. To z něj dělá ideální technologii pro senzory, chytré hodinky, domácí automatizaci nebo různá bateriově napájená zařízení.

Proč budeme na ESP32 používat právě BLE

V prostředí MicroPythonu se vždy pracuje s BLE. Důvod je jednoduchý: standardní firmware MicroPythonu pro modul ESP32 podporuje pouze BLE. Existují sice určité soukromé pokusy o úpravu firmwaru tak, aby bylo možné využívat i Bluetooth Classic, ale těmi se opravdu zabývat nebudeme. Koneckonců nám absence BT Classic ani moc vadit nebude, neboť většina projektů s ESP32 nepotřebuje přenášet hudbu ani velké soubory. Typicky spíše chceme odeslat několik bajtů s naměřenou hodnotou, rozsvítit LED, zjistit stav tlačítka nebo přijmout jednoduchý příkaz z telefonu.

BLE je pro tyto úlohy tedy naprosto ideální – má nízkou spotřebu, je podporováno prakticky všemi moderními telefony a MicroPython pro něj nabízí přímou vestavěnou podporu. To znamená, že se můžeme soustředit na logiku projektu místo řešení složité bezdrátové komunikace.

Poznámka:
U rodiny ESP32 je potřeba si dát pozor na konkrétní variantu čipu. Klasické ESP32, stejně jako ESP32-S3, ESP32-C3 a ESP32-C6, Bluetooth podporují. Výjimkou je ale ESP32-S2, které Bluetooth vůbec neobsahuje. Pokud tedy budete zkoušet příklady z tohoto článku, ujistěte se, že nepoužíváte právě model S2!

Jak si komunikaci představit

Pro úplný začátek si vystačíme s jednoduchou představou, ve které modulu ESP32 bude fungovat jako malé bezdrátové zařízení, které pravidelně oznamuje svou přítomnost do okolí. Telefon tato oznámení zachytí, zobrazí zařízení v seznamu a po připojení si s ním začne vyměňovat data. Typická komunikace pak vypadá kupříkladu takto:

  1. ESP32 změří teplotu a pošle ji telefonu.
  2. Telefon ji zobrazí na displeji.
  3. Uživatel stiskne tlačítko v aplikaci a telefon odešle zpět příkaz, například ke spuštění topení
  4. ESP32 přijme příkaz a sepne silové relé topení.

Central vs. Peripheral?

Ještě než se pustíme do dalšího textu, musíme si ujasnit ještě několik pojmů a faktů. Zaprvé si musíme uvědomit, že v BLE existují dvě základní role. Telefon obvykle aktivně hledá zařízení v okolí, zatímco ESP32 čeká, až ho někdo najde. Telefon tedy funguje jako tzv. Central a ESP32 jako tzv.  Peripheral.

Ale pozor! Tato role říká pouze, kdo spojení zahajuje, nikoli kdo posílá data.

Server vs. klient (druhý pohled na totéž)

Kromě rolí Central a Peripheral existují ještě role GATT Server a GATT Client. Vynecháme-li slovo „GATT“, připomíná nám to klasickou síťovou komunikaci, kde:

  • Server uchovává data,
  • Klient tato data ze serveru získává a s daty pracuje.

Nejinak tomu bude i při komunikaci BLE. V našich projektech je modul ESP32 téměř vždy server a telefon klientem.

Takže pokud spojíme předešlé dva pohledy, můžeme říci: Telefon zahájí spojení (je Central), ale zároveň čte data z modulu ESP32, takže je Klient. Modul ESP32 čeká na připojení (je Peripheral) a zároveň poskytuje data, tak je Server.

Proč BLE používá GATT

Jak už jsme si řekli, Bluetooth Low Energy (BLE) nepřenáší data jako souvislý datový proud, jak je běžné například u sériové komunikace. Komunikace v BLE je založena na atributech – jednotlivých hodnotách uložených v zařízení.

Pravidla pro organizaci a zpřístupnění těchto atributů definuje tzv. GATT (Generic Attribute Profile).

V praxi to znamená, že zařízení BLE (například ESP32) vystupuje jako GATT server, který obsahuje databázi atributů. Druhé zařízení (telefon, tablet nebo počítač) vystupuje jako GATT klient a může:

  • zjišťovat, jaké údaje server nabízí,
  • číst jejich hodnoty,
  • zapisovat nové hodnoty,
  • přijímat oznámení o změnách hodnot.

Struktura GATT

Databáze GATT má pevně danou hierarchii. Trochu nám to může připomínat systém uspořádaných souborů na našem počítači. Tam také máme nějaké složky a v nich soubory s určitými daty a vlastnostmi (např. read-only apod.).

V případě GATT však budeme pracovat s pojmem Služba (Service), což nám může připomínat tu „složku“, a s pojmem Charakteristika (Characteristic), což si lze představit jako „soubor“. Pro každou charakteristiku budeme muset nastavit Vlastnosti (Properties), které budou určovat možné operace s charakteristikou.

Následující obrázek č. 1 nám ukazuje strukturu GATT obsahující jednu službu se dvěma charakteristikami.

struktura GATT
Obrázek č. 1 – Schéma Struktury GATT obsahující jednu službu se dvěma charakteristikami.
Service (služba)

Služba seskupuje logicky související údaje. Sama obvykle neobsahuje užitečnou hodnotu, ale slouží jako kontejner pro charakteristiky.

Příklady standardních služeb:

  • Environmental Sensing
  • Battery Service
  • Heart Rate

Každá služba je identifikována pomocí UUID, o kterém se zmíníme dále.

Characteristic (charakteristika)

Charakteristika představuje konkrétní údaj nebo funkci zařízení. Obsahuje tři důležité části:

  • UUID – jednoznačný identifikátor charakteristiky.
  • Properties – určuje povolené operace (čtení, zápis, notifikace…).
  • Value – vlastní přenášená data.

Například charakteristika teploty může obsahovat hodnotu 24.6 °C.

Descriptor (deskriptor)

Deskriptor poskytuje doplňující informace o charakteristice. Nejčastěji se používá deskriptor Client Characteristic Configuration Descriptor (CCCD), pomocí kterého klient povoluje notifikace nebo indikace.

Pro základní práci v MicroPythonu není nutné deskriptory vytvářet ručně; systém je u notifikací, kde jsou nejvíce potřeba, obvykle spravuje automaticky.

Vlastnosti charakteristik

Vlastnosti určují způsob práce s charakteristikou.

Vlastnost Význam
Read Klient může hodnotu přečíst.
Write Klient může hodnotu zapsat.
Notify Server odešle novou hodnotu bez předchozího požadavku klienta.
Indicate Podobné jako Notify, ale s potvrzením přijetí.

Čtení teploty (Read) – Telefon odešle požadavek na čtení a ESP32 vrátí aktuální hodnotu teploty.

Ovládání LED (Write) – Telefon zapíše hodnotu 1 nebo 0 do charakteristiky a ESP32 podle ní LED zapne nebo vypne.

Změna stavu tlačítka (Notify) – Při změně stavu tlačítka ESP32 automaticky odešle novou hodnotu telefonu. Telefon se nemusí opakovaně dotazovat na aktuální stav. Notifikace jsou v BLE velmi důležité, protože snižují počet přenosů a tím i spotřebu energie.

Příklad struktury pro ESP32

Představme si zařízení, které:

  • měří teplotu,
  • umožňuje ovládat digitální výstup (rozsvěcet LED),
  • hlásí změnu stavu digitálního vstupu (tlačítka).

Možná struktura GATT je ilustrována na obrázku č. 2 a může vypadat takto:

ukazka-GATT
Obrázek č. 2 – Ukázková struktura GATT obsahující tři služby vždy s jednou charakteristikou.

V tomto případě bychom tedy měli tři služby, každá s jednou charakteristikou. S ohledem na charakter přenášených dat je asi toto rozdělení logické. Zde jde opravdu jen o určitou přehlednost, protože pro funkčnost by nijak neovlivnilo, kdybychom měli službu jedinou se třemi charakteristikami.

U jednotlivých charakteristik vidíme jejich vlastnosti, které opět plynou z charakteru dat – teplota je určena pro čtení, neboť modul ESP32 ji bude patrně měřit a telefon jen číst. Stav LED musí být naopak nastaven pro zápis, neboť chceme telefonem do této charakteristiky zapisovat. A poslední charakteristika připadající na monitoring stavu tlačítka modulu ESP32 má nastavené vlastnosti hned dvě – read a notify. Vlastnost Read odpovídá tomu, že telefon může tuto hodnotu kdykoliv načíst. Zajímavější je však vlastnost Notify, která říká, že při změně této charakteristiky (tj. při změně stavu tlačítka) je okamžitě odeslána zpráva telefonu. Telefon má tedy neustále aktuální hodnotu, aniž by se musel neustále na tento stav dotazovat.

Co je z GATT pro MicroPython nejdůležitější?

Při programování ESP32 v MicroPythonu budete nejčastěji pracovat právě s charakteristikami:

  • vytvoříte službu,
  • v ní vytvoříte jednu nebo více charakteristik,
  • nastavíte vlastnosti charakteristik,
  • měníte hodnoty charakteristik,
  • případně se při změnách odesílají notifikace připojenému klientovi.

Klíčová myšlenka GATT je jednoduchá: GATT je hierarchická databáze atributů: služby organizují charakteristiky a charakteristiky nesou skutečná data, která klient čte, zapisuje nebo přijímá prostřednictvím notifikací.

Jak probíhá skutečná komunikace?

Teď jsme trochu pochopili, jak jsou data uspořádána v GATT, a můžeme se podívat, jak taková BLE komunikace probíhá.

Celý proces se skládá z těchto kroků:

  1. Advertising: ESP32 vysílá svou přítomnost.
  2. Scanning: telefon tato vysílání hledá.
  3. Connect: telefon naváže spojení.
  4. Discovery: telefon si prohlédne dostupné služby a charakteristiky GATT.
  5. Exchange: probíhá čtení, zápis a notifikace.
ukazka BLE-connection
Obrázek č. 3 – Schéma vzájemné komunikace při BLE.
Zdroj obrázku: https://www.rfwireless-world.com/terminology/ble-connection-establishment

Až do bodu 4 telefon vlastně ještě neví, co modul ESP32 umí. Teprve při Discovery zjistí: „Aha, tady je teplota, tady stav dveří a tady zapínání topení.

Kde se v tom objeví UUID?

V BLE (Bluetooth Low Energy) má každá služba i každá charakteristika své unikátní UUID, tedy jedinečný identifikátor. Konsorcium Bluetooth SIG už předem definovalo tisíce standardních UUID pro běžné funkce, jako jsou teplota, baterie, srdeční tep nebo tlačítka. Tyto standardní funkce mají krátká 16bitová UUID. Například služba baterie používá 0x180F, nebo pokud použijeme 0x181A, říkáme tím ostatním zařízením: „Toto je standardní služba Environmentálního měření“. Podobně hodnota 0x2A56 znamená „Digital Input“ a tak dále. Tyto hodnoty jsou už obsazené a mají přesně určený význam.

V následující tabulce vidíme přehled některých standardních UUID. Kompletní seznam standardních UUID je na stránkách Bluetooth SIG: Assigned Numbers.

Typ UUID Význam
Služba 0x1800 Generic Access
Služba 0x1801 Generic Access
Služba 0x180F Battery Service
Služba 0x181A Environmental Sensing
Charakteristika 0x2A19 Battery Level
Charakteristika 0x2A6E Temperature
Charakteristika 0x2A56 Digital Input

Pro výukový pokus s LED by šly tyto standardní (krátké) UUID využít a asi by i fungovaly, ale z pohledu správného návrhu BLE to není ideální. Je to jako zatloukat hřebíky otvírákem na konzervy. Předem určené UUID může klientovi vnutit očekávání úplně jiných dat, než ve skutečnosti posíláme. A je jen otázkou, zda nám to projde. Takže se tomu budeme pokud možno vyhýbat.

Budeme si pamatovat:
Když si vytváříme vlastní BLE službu, například pro ovládání LED, přenos textu nebo komunikaci s domácí automatizací, standardní UUID použít prostě nesmíme! Krátká 16bit UUID používejme pouze tehdy, když skutečně implementujete odpovídající standardní Bluetooth služby nebo charakteristiky.

Aby si lidé mohli dělat své vlastní služby a charakteristiky a vzájemně si „nelezli do zelí“ díky stejným UUID, používají se 128bit dlouhé UUID, které nám nabízejí téměř neomezenou škálu UUID, například: 6E400001-B5A3-F393-E0A9-E50E24DCCA9E

Toto číslo je prakticky globálně jedinečné. Můžeme si vytvořit libovolný počet vlastních UUID, aniž bychom se báli kolize s oficiálními Bluetooth službami nebo s jiným výrobcem zařízení.

Často se vytvoří jedna vlastní služba a několik charakteristik, které se liší jen několika číslicemi:

UUID_SVC = bluetooth.UUID("6E400001-B5A3-F393-E0A9-E50E24DCCA9E")
UUID_RX = bluetooth.UUID("6E400002-B5A3-F393-E0A9-E50E24DCCA9E")
UUID_TX = bluetooth.UUID("6E400003-B5A3-F393-E0A9-E50E24DCCA9E")

kde:

  • ...0001... = služba
  • ...0002... = přijímací charakteristika (RX)
  • ...0003... = vysílací charakteristika (TX)

Ale je to jen konvence pro lepší přehlednost. Jinak si jednotlivá UUID můžeme vytvořit skoro libovolně.

Důležitý poznatek je, že UUID samo o sobě neurčuje, zda lze číst, zapisovat nebo posílat notifikace. Tyto vlastnosti se nastavují zvlášť pomocí příznaků, například když v Pythonu napíšeme:

TEMP_CHAR = (UUID_TEMP, _FLAG_READ | _FLAG_NOTIFY)

nebo pak ještě později

temp_char = aioble.Characteristic(service, UUID_TEMP, read=True, notify=True)

ve skutečnosti v obou případech tím říkáme: „Vytvoř v této službě charakteristiku (položku) jménem UUID_TEMP. Tuto položku smí telefon číst a já mu mohu automaticky oznamovat změny.

A když zavoláme:

temp_char.write(b'25.1', send_update=True)

znamená to: „Ulož novou hodnotu 25.1 a okamžitě o ní informuj připojený telefon.

Na úrovni MicroPythonu už nepracujeme s nějakým radiovým přenosem ani s pakety, pracujeme pouze s hodnotami v bezdrátové databázi, jejíž přenos za nás bude řešit nějaký programový modul, který budeme využívat.

A právě na tomto jednoduchém, ale velmi důležitém principu stojí všechny další příklady s ESP32 a MicroPythonem.

Praktická část

Asi bude nejlepší si to celé rovnou ukázat na konkrétních příkladech. Nejprve vytvoříme BT dálkové ovládání vestavěné LED na modulu ESP32. Telefon bude posílat hodnoty do ESP32 a my uvidíme okamžitou reakci hardwaru.

Jako druhý příklad si vytvoříme BT bezdrátové tlačítko. ESP32 bude sledovat stisk tlačítka BOOT a při změně pošle notifikaci do telefonu.

Tyto dva příklady nejsou vybrány náhodou. Jakmile pochopíte LED (Write) a tlačítko (Notify), budete umět vytvořit většinu základních BLE projektů: Chytré vypínače, senzory, alarmy, dálkové ovladače i jiné prvky jednoduché domácí automatizace.

nRF Connect

Ještě než se ale pustíme do programování v MicroPythonu, musíme si pro naše experimenty také připravit náš telefon, se kterým bude naše ESP32 komunikovat. Většina moderních smartphonů dnes již BLE funkce podporuje, takže náš BLE server na modulu ESP32 by se měl dát chytrým telefonem naskenovat a prohlédnout si jeho služby a vlastnosti. K tomu využijeme bezplatnou aplikaci s názvem nRF Connect od společnosti Nordic® Semiconductor. Tato aplikace je dostupná pro telefony jak s operačním systémem Android (odkaz na Google Play Store), tak systémem iOS (odkaz na App Store).

nRF Connect

Přejdeme tedy do obchodu Google Play nebo App Store a vyhledáme a nainstalujeme aplikaci nRF Connect.

Začínáme s BLE v MicroPythonu

Na začátku si ukážeme jak v MicroPythonu zapnout komunikaci BT BLE a jak spustit tzv. advertising, při kterém naše zařízení bude svému okolí oznamovat svou přítomnost.

To jsou základní věci, které budeme muset použít v každém BT projektu.

Zapnutí Bluetooth na ESP32

Než začneme vytvářet služby a charakteristiky, musíme aktivovat samotné Bluetooth uvnitř čipu ESP32. To provedeme pomocí příkazů z modulu bluetooth:

import bluetooth

ble = bluetooth.BLE()
ble.active(True)

První řádek načte modul bluetooth. Druhý vytvoří objekt ble, který představuje přístup k Bluetooth stacku v MicroPythonu. Třetí řádek BT-rádio skutečně zapne. Je to podobné, jako když u Wi-Fi nestačí mít jen knihovnu, ale musíte bezdrátové rozhraní také aktivovat.

Můžeme si ověřit, že Bluetooth běží pomocí příkazu:

print(ble.active())

Pokud se vypíše True, je rádio aktivní.

Jak zjistit, že ESP32 skutečně vysílá

Samotné zapnutí BLE rádia ještě neznamená, že telefon zařízení uvidí. ESP32 musí začít pravidelně vysílat svou přítomnost – advertising.

Nejprve nastavíme jméno zařízení. To je text, který se zobrazí v aplikaci pro skenování BLE zařízení, například v aplikaci nRF Connect.

Nejjednodušší možnost advertisingu by mohla vypadat kupříkladu jako v následujícím kódu, kde jsme si pro tento účel vytvořili vlastní funkci nazvanou start_advertising():

import bluetooth
import time

ble = bluetooth.BLE()
ble.active(True)

def start_advertising(name=b'ESP32-BLE'):
    # BLE advertising paket
    adv = (
        bytearray(b'\x02\x01\x06') +
        bytearray((len(name) + 1, 0x09)) +
        name
    )

    ble.gap_advertise(250000, adv_data=adv)

start_advertising(b'ESP32-BTN')
print("ESP32-BTN je viditelné přes BLE")

while True:
    time.sleep_ms(1000)

Funkce start_advertising() má jediný nepovinný vstupní parametr název, kterým se má modul ESP32 hlásit. Pokud tento parametr nezadáme, je použita výchozí hodnota ESP32-BLE. My jsme ale tento parametr nastavili na ESP32-BTN, takže při detekci našeho modulu ESP32 budeme hledat název.

Za pozornost stojí hodnota 250000 ve funkci ble.gap_advertise(), ta udává interval vysílání v mikrosekundách, tedy zde 250 ms. Modul ESP32 tak každých 250 ms opakuje krátkou zprávu typu: „Jsem tady, jmenuji se ESP32-BTN.

V této chvíli je pro nás nejdůležitější jen to, aby telefon viděl zařízení s názvem ESP32-BTN. Na telefonu si otevřeme aplikaci nRF Connect a stiskneme tlačítko  Scan . V seznamu všech dostupných BT zařízení v okolí by měl být vidět i název ESP32-BTN – viz obr. č. 4.

pripojeni k ESP32-BTN
Obrázek č. 4 – Nalezení zařízení ESP32-BTN v aplikaci nRF Connect.

Je dobré si uvědomit jednu důležitou věc a to, že samotný advertising ještě nevytvoří žádnou komunikaci! ESP32 pouze oznamuje svoji přítomnost. Teprve po připojení (tlačítko  CONNECT ) vytvoří telefon spojení a může pracovat se službami a charakteristikami.

Po připojení k našemu modulu ESP32-BTN uvidíme ale zatím jen prázdnou strukturu GATT, ve které máme jen primární službu UUID 0x1800 a atribut 0x1801 – viz obr. č. 5. Zatím tedy nic pro řízení modulu ESP32 nebo načítání jeho stavů.

otevreni prazdne GATT
Obrázek č. 5 – Otevření prázdné struktury GATT v aplikaci nRF Connect.

Musíme tedy v tuto chvíli připravit svou část struktury GATT a naplnit ji službami a charakteristikami, které je třeba na straně modulu ESP32 zaregistrovat pomocí funkce gatts_register_services().

PŘÍKLAD č. 1 – BT rozsvícení LED

Dokážeme-li modul „nabídnout“ světu a máme přístup do jeho zatím skoro prázdné GATT, máme vše připravené pro vybudování našeho BLE projektu. Začneme postupně vytvářet strukturu budoucího programu. V této ukázce chceme pomocí mobilního telefonu rozsvěcet pomocí BT vestavěnou LED na modulu ESP32.

Vytvoření první charakteristiky – ovládání LED

Na většině desek ESP32 je vestavěná LED připojena na GPIO 2. To v programu pro MicroPython na modulu ESP32 zapíšeme:

from machine import Pin
led = Pin(2, Pin.OUT)

Tím jsme připravili hardware.

Nyní se pustíme do Bluetooth komunikace. Nejdříve definujeme BLE službu a charakteristiku.

UUID_SVC = bluetooth.UUID("6E400001-B5A3-F393-E0A9-E50E24DCCA9E")
UUID_LED = bluetooth.UUID("6E400002-B5A3-F393-E0A9-E50E24DCCA9E")

Zde používáme dvě UUID, které jsme si zcela vymysleli. První označuje službu, druhá charakteristiku pro LED. Dále bude třeba nastavit to, že do dané charakteristiky bude možné zapisovat. Pro to bude třeba „na správném místě“ zadat hodnotu 0x0008.

Kdo však má vědět, že zrovna tuto hodnotu?

Standardní modul bluetooth bohužel nemá pro tyto případy jasně definované nějaké pojmenované konstanty, tak si pro lepší práci těmto číselným hodnotám můžeme názvy dodefinovat – například pro zápis:

_FLAG_WRITE = 0x0008

V následující tabulce je přehled možných definicí dalších těchto hodnot – nejde o žádné oficiální pojmenování, je to přehled hodnot a případných názvů konstant, které pak můžeme pohodlně používat.

Název konstanty Hex hodnota Význam
_FLAG_BROADCAST 0x0001 Charakteristiku lze vysílat v reklamních (broadcast) datech.
_FLAG_READ 0x0002 Klient může hodnotu číst.
_FLAG_WRITE_NO_RESPONSE 0x0004 Zápis bez potvrzení od serveru.
_FLAG_WRITE 0x0008 Zápis s potvrzením od serveru.
_FLAG_NOTIFY 0x0010 Server může posílat notifikace klientovi.
_FLAG_INDICATE 0x0020 Server může posílat indikace vyžadující potvrzení klienta.
_FLAG_AUTHENTICATED 0x0040 Vyžaduje autentizované spojení.
_FLAG_AUX_WRITE 0x0100 Pomocný (auxiliary) zápis, používá se zřídka.

Naštěstí pro běžné projekty s ESP32 nejčastěji budeme využívat jen tyto čtyři:

_FLAG_READ = 0x0002
_FLAG_WRITE = 0x0008
_FLAG_NOTIFY = 0x0010
_FLAG_INDICATE = 0x0020

Pro nastavení charakteristiky použijeme pomocnou proměnnou LED_CHAR, do které vložíme tuple složený z dvojice UUID_LED a jejích vlastností:

LED_CHAR = (UUID_LED, _FLAG_WRITE)

Pokud bychom charakteristice chtěli přidat více vlastností, udělali bychom to logickým sloučením, tj. následujícím způsobem (při výše definovaných konstantách _FLAG_READ | _FLAG_WRITE | _FLAG_NOTIFY):

LED_CHAR = (UUID_LED, _FLAG_READ | _FLAG_WRITE | _FLAG_NOTIFY )

Poznámka k _FLAG_NOTIFY
Samotný příznak _FLAG_NOTIFY pouze říká, že charakteristika notifikace podporuje – ještě je automaticky neposílá. Pokud přidáme _FLAG_NOTIFY, musí klient (mobil/aplikace) notifikace povolit a ty je pak odesíláme například: ble.gatts_notify(conn_handle, led_handle, b"1")

Nyní máme vše nastaveno, takže musíme samotnou službu registrovat. Samotná registrace služby a charakteristiky vypadá následujícím způsobem. Pozor na čárky, které v kódu vypadají jako chyby – protože se vkládá typ tuple a my vkládáme jen některé jeho části, čárka vlastně označuje prázdnou část této struktury.

services = (
    (UUID_SVC, (
        LED_CHAR,
    )),
)

((led_handle,),) = ble.gatts_register_services(services)

V případě, že bychom nechtěli využít pomocné proměnné LED_CHAR, mohli bychom její vlastnosti nastavit rovnou zde:

services = (
    (UUID_SVC, (
        (UUID_LED, _FLAG_WRITE),
    )),
)

Dokud máme jedinou charakteristiku, je způsob bez pomocné proměnné ještě přijatelný. Problém nastane ve chvíli, kdy projekt povyroste a máme najednou více charakteristik – například:

LED_CHAR = (UUID_LED, _FLAG_READ | _FLAG_WRITE)
TEMP_CHAR = (UUID_TEMP, _FLAG_READ | _FLAG_NOTIFY)
BTN_CHAR = (UUID_BTN, _FLAG_NOTIFY)

services = (
    (UUID_SVC, (
        LED_CHAR,
        TEMP_CHAR,
        BTN_CHAR,
    )),
)

Tento kód je pak mnohem přehlednější, než něco následujícího:

services = (
    (UUID_SVC, (
        (UUID_LED, _FLAG_READ | _FLAG_WRITE),
        (UUID_TEMP, _FLAG_READ | _FLAG_NOTIFY),
        (UUID_BTN, _FLAG_NOTIFY),
    )),
)

Nebo dokonce v jedné řádce a bez našich dodefinovaných konstant:

services=((UUID_SVC,((UUID_LED,0x0002|0x0008),(UUID_TEMP,0x0002|0x0010),(UUID_BTN,0x0010))),)

Celý tento zápis může na první pohled vypadat děsivě, ale důležitá je jediná věc: Ve všech případech funkce ble.gatts_register_services() vrátí v proměnné led_handle handle. Handle si můžeme představit jako interní číslo, pod kterým je charakteristika uložena v paměti ESP32. A to nám stačí, rázem máme cestu k přístupu vyřešenou a nějaké vnitřní struktury uspořádání dat řešit již nemusíme.

Zkrátka, pokud to dobře nakonfigurujeme, máme (skoro) vše vyřešené.

V tuto chvíli bychom v okně nRF Connect už mohli vidět naši ovládací charakteristiku – viz obrázek č. 6. Charakteristiku poznáme pomocí jejího unikátního UUID, dále vidíme, že do této charakteristiky lze zapisovat (Properties: Write ). Výchozí hodnota zatím není nastavena (prázdné pole Value:).

pristup k charakteristice GATT
Obrázek č. 6 – Otevření struktury GATT a přístup k charakteristice 6E400002-... v aplikaci nRF Connect.

Když na tuto charakteristiku klikneme, můžeme do ní i nějakou hodnotu zapsat. Problém je, že tato změna hodnoty, na modulu ESP32 stále nic neudělá. Musíme tedy modulu ESP32 nějak říci, že má na tuto hodnotu dané charakteristiky reagovat.

Jak ESP32 pozná, že telefon něco zapsal?

BLE je v MicroPythonu řízeno událostmi. Když telefon zapíše data do charakteristiky, vyvolá se událost 3, které budeme říkat _IRQ_GATTS_WRITE. Každá událost má své číslo. Stejně jako u příznaků charakteristik (_FLAG_READ, _FLAG_WRITE, _FLAG_NOTIFY) nejsou tyto IRQ konstanty v modulu bluetooth definovány.

Následující tabulka ukazuje přehled základních hodnot a také názvy konstant, které si pro ně definujeme.

Konstanta Číslo Význam
_IRQ_CENTRAL_CONNECT 1 Telefon (centrála) se připojil.
_IRQ_CENTRAL_DISCONNECT 2 Telefon (centrála) se odpojil.
_IRQ_GATTS_WRITE 3 Klient zapsal data do charakteristiky.
_IRQ_GATTS_READ_REQUEST 4 Klient požaduje čtení charakteristiky.
_IRQ_SCAN_RESULT 5 Nalezeno zařízení při skenování.
_IRQ_SCAN_DONE 6 Skenování bylo dokončeno.
_IRQ_PERIPHERAL_CONNECT 7 ESP32 se připojilo k jinému zařízení.
_IRQ_PERIPHERAL_DISCONNECT 8 ESP32 se odpojilo od zařízení.
_IRQ_GATTC_SERVICE_RESULT 9 Nalezena služba na vzdáleném zařízení.
_IRQ_GATTC_CHARACTERISTIC_RESULT 11 Nalezena charakteristika.
_IRQ_GATTC_DESCRIPTOR_RESULT 13 Nalezen descriptor.
_IRQ_GATTC_READ_RESULT 15 Přijata data po čtení.
_IRQ_GATTC_WRITE_DONE 17 Zápis na vzdálené zařízení dokončen.
_IRQ_GATTC_NOTIFY 18 Přijata notifikace.
_IRQ_GATTC_INDICATE 19 Přijata indikace.

Vytvoříme obslužnou funkci přerušení, která bude volána při důležitých událostech, jako kupříkladu při zápisu do kterékoliv charakteristiky GATT. (událost _IRQ_GATTS_WRITE).

# Obsluha BLE událostí (IRQ)
def bt_irq(event, data):   # Funkce se zavolá při každé BLE události
    if event == _IRQ_GATTS_WRITE:
        # Událost číslo 3 = klient (telefon) zapsal data
        # do některé z našich BLE charakteristik

        conn_handle, value_handle = data
        # data obsahují:
        # conn_handle  - identifikátor spojení
        # value_handle - identifikátor charakteristiky, do které se zapisovalo

        if value_handle == led_handle:
            # Ověříme, že zápis přišel právě do LED charakteristiky
            msg = ble.gatts_read(led_handle)

            # Přečteme nově zapsaná data z charakteristiky
            if msg == b'\x01':
                # Pokud telefon poslal bajt 0x01, LED zapneme
                led.value(1)
                print("LED zapnuta")
            else:
                # Jakákoli jiná hodnota LED vypne
                led.value(0)
                print("LED vypnuta")

Protože celá BLE komunikace je postavena na přerušení, je tato funkce spouštěna i při jiných událostech – např. při připojení nebo odpojení klienta. V kódu této funkce tedy musíme podle kódu nastalé události rozdělit jednotlivé případy. Výše uvedená ukázka zpracovává jen zapsání do charakteristiky. Prvním úkolem je zjištění, do které charakteristiky bylo zapsáno. Tuto informaci načteme z dat předaných přerušením funkci bt_irq a zapíšeme do proměnné value_handle. Následně tuto proměnnou porovnáme s proměnnou led_handle, ve které máme odkaz na charakteristiku pro LED – tuto hodnotu jsme získali při registraci dané služby – viz dříve. Pokud dojde ke shodě, došlo ke změně v charakteristice LED a na základě její hodnoty (načteme funkcí ble.gatts_read(led_handle)) provedeme změnu stavu LED.

Funkci přerušení máme připravenou, ale nakonec ji pochopitelně ještě musíme zaregistrovat mezi přerušení:

ble.irq(bt_irq)

Kompletní první program: LED ovládaná z telefonu

import bluetooth
import time
from machine import Pin

# Inicializace vestavěné LED (na mnoha deskách ESP32 je na GPIO2)
led = Pin(2, Pin.OUT)
led.value(0)

# Inicializace BLE
ble = bluetooth.BLE()
ble.active(True)

# UUID
UUID_SVC = bluetooth.UUID("6E400001-B5A3-F393-E0A9-E50E24DCCA9E")
UUID_LED = bluetooth.UUID("6E400002-B5A3-F393-E0A9-E50E24DCCA9E")

#definice použitého priznaku
_FLAG_WRITE = 0x0008

LED_CHAR = (UUID_LED, _FLAG_WRITE)

# Registrace BLE služby s jednou zapisovatelnou charakteristikou
services = (
    (UUID_SVC, (
       LED_CHAR,
    )),
)

((led_handle,),) = ble.gatts_register_services(services)

# Výchozí hodnota charakteristiky = LED vypnuta
ble.gatts_write(led_handle, b'\x00')

#definice IRQ událostí
_IRQ_CENTRAL_CONNECT = const(1)
_IRQ_CENTRAL_DISCONNECT = const(2)
_IRQ_GATTS_WRITE = const(3)

def bt_irq(event, data):
    if event == _IRQ_CENTRAL_CONNECT:
        conn_handle, addr_type, addr = data
        print("Zařízení připojeno")
    elif event == _IRQ_CENTRAL_DISCONNECT:
        conn_handle, addr_type, addr = data
        print("Zařízení odpojeno, spouštím advertising znovu...")
        start_advertising() # Po odpojení začne ESP32 opět vysílat
    elif event == _IRQ_GATTS_WRITE:
        conn_handle, value_handle = data
        if value_handle == led_handle:
            msg = ble.gatts_read(led_handle)
            if msg == b'1':
                led.value(1)
                print("LED zapnuta")
            else:
                led.value(0)
                print("LED vypnuta")

ble.irq(bt_irq)

def start_advertising(name=b'ESP32-BLE'):
    # BLE advertising paket
    adv = (
        bytearray(b'\x02\x01\x06') +
        bytearray((len(name) + 1, 0x09)) +
        name
    )

    ble.gap_advertise(250000, adv_data=adv)

# Prvotní spuštění vysílání
start_advertising(b'ESP32-BTN')
print("Čekám na připojení a vysílám jméno ESP32-LED...")

# Hlavní smyčka může běžet prázdná, vše obsluhují BLE přerušení (IRQ)
while True:
    time.sleep_ms(1000)

Po spuštění programu v modulu ESP32:

  1. otevřeme na telefonu aplikaci nRF Connect,
  2. připojíme se aplikací k BLE nazvanému: ESP32-LED,
  3. v aplikaci najdeme charakteristiku s UUID 6E400002-B5A3-F393-E0A9-E50E24DCCA9E (zpravidla ta poslední)
  4. Klikneme na ikonku zápisu (šipka) a zapíšeme hodnotu 1 (viz obr. č. 7), odešleme tlačítkem  SEND  → LED se rozsvítí,
zmena hodnoty charakteristiky
Obrázek č. 7 – Zadání hodnoty charakteristiky pomocí aplikace nRF Connect.
  1. Zapíšeme-li jakoukoliv jinou hodnotu, například 0 – LED zhasne.

Pozorný čtenář si jistě ve výpisu celého programu všiml, že nám zde obslužná rutina přerušení poněkud nabobtnala. V obslužné rutině přerušení totiž netestujeme jen událost zápisu (_IRQ_GATTS_WRITE), ale i událost připojení (_IRQ_CENTRAL_CONNECT) a odpojení (_IRQ_CENTRAL_DISCONNECT). Při těchto událostech dostane funkce bt_irq() v proměnné data doplňující informace. V ukázkovém programu hodnoty conn_handle, addr_type a addr přijímáme, protože je BLE událost poskytuje, ale pro samotné ovládání LED využíváme pouze value_handle. V reálných aplikacích se conn_handle používá při odesílání notifikací a addr může sloužit k identifikaci připojeného zařízení.

  • conn_handle – Číslo aktuálního BLE spojení – Používá se při komunikaci s konkrétním zařízením. BLE zařízení nemusí komunikovat pouze s jedním klientem. Každé spojení má svůj vlastní identifikátor. Pak například při posílání notifikace musíme říct, komu ji posíláme: ble.gatts_notify(conn_handle, temp_handle, b"25").
  • addr_type – Typ Bluetooth adresy – Určuje, zda jde o veřejnou nebo náhodnou adresu.
  • addr – MAC adresa připojeného zařízení – identifikace telefonu nebo jiného klienta. Adresu bychom mohli použít například pro omezení přístupu.
animace BT řízení LED
Obrázek č. 8 – BT ovládání vestavěné LED modulu ESP32 pomocí aplikace nRF Connect.

PŘÍKLAD č. 2 – Tlačítko jako bezdrátový senzor

Teď obrátíme směr komunikace. Nebudeme posílat příkaz do modulu ESP32, ale budeme chtít, aby ESP32 samo informovalo telefon o stisku tlačítka.

Na většině desek je tlačítko BOOT na GPIO 0.

button = Pin(0, Pin.IN, Pin.PULL_UP)

Použijeme službu a charakteristiku s vlastností Notify.

# Definice služby a charakteristiky
UUID_SVC = bluetooth.UUID("a1b2c300-1234-5678-9abc-def012345678")
UUID_BTN = bluetooth.UUID("a1b2c301-1234-5678-9abc-def012345678")

_FLAG_NOTIFY = 0x0010

BUTTON_STATE = (UUID_BTN, _FLAG_NOTIFY)

Službu registrujeme obdobným způsobem jako v předešlé ukázce:

# Registrace BLE služby s jednou zapisovatelnou charakteristikou
services = (
    (UUID_SVC, (
        BUTTON_STATE,
    )),
)

((btn_handle,),) = ble.gatts_register_services(services)

Odeslání notifikace

Když na modulu ESP32 zjistíme stisk tlačítka, zavoláme:

ble.gatts_notify(conn_handle, btn_handle, b'1')

Tato funkce odešle hodnotu všem připojeným klientům, kteří mají notifikace povolené. Náš telefon s notifikacemi nemá problém, takže, jak uvidíme na screenshotu z aplikace nRF Connect (obr. č. 9), notifikace se povolí automaticky (Notifications enabled).

charakteristika NOTIFY
Obrázek č. 9 – Charakteristika typu NOTIFY aktuálně zobrazující hodnotu 0x01.

Vše ostatní je obdobné předchozímu příkladu s rozsvěcením LED. Ukážeme si tedy celý program a případně si potřebné informace doplníme až pod výpisem.

Jednoduchý program pro tlačítko

import bluetooth
import time
from machine import Pin
# GPIO0 je obvykle vestavěné BOOT tlačítko

button = Pin(0, Pin.IN, Pin.PULL_UP)

ble = bluetooth.BLE()
ble.active(True)

# Definice služby a charakteristiky
UUID_SVC = bluetooth.UUID("a1b2c300-1234-5678-9abc-def012345678")
UUID_BTN = bluetooth.UUID("a1b2c301-1234-5678-9abc-def012345678")

_FLAG_NOTIFY = 0x0010

BUTTON_STATE = (UUID_BTN, _FLAG_NOTIFY)

# Registrace BLE služby s jednou zapisovatelnou charakteristikou
services = (
    (UUID_SVC, (
       BUTTON_STATE,
    )),
)

((btn_handle,),) = ble.gatts_register_services(services)

connections = set()

#definice IRQ událostí
_IRQ_CENTRAL_CONNECT = const(1)
_IRQ_CENTRAL_DISCONNECT = const(2)

def bt_irq(event, data):
    if event == _IRQ_CENTRAL_CONNECT:
        conn_handle, addr_type, addr = data
        connections.add(conn_handle)
        print("Zařízení připojeno")
    elif event == _IRQ_CENTRAL_DISCONNECT:
        conn_handle, addr_type, addr = data
        connections.remove(conn_handle)
        print("Zařízení odpojeno")
        # Po odpojení je dobré znovu spustit advertising, aby se mohlo připojit znova
        start_advertising()

ble.irq(bt_irq)

def start_advertising(name=b'ESP32-BLE'):
    # BLE advertising paket
    adv = (
        bytearray(b'\x02\x01\x06') +
        bytearray((len(name) + 1, 0x09)) +
        name
    )

    ble.gap_advertise(250000, adv_data=adv)

# Spuštění advertisingu při startu
start_advertising(b'ESP32-BUTTON')
print("Čekám na připojení a vysílám jméno ESP32-BUTTON...")

last = button.value()

while True:
    state = button.value()

    if state != last:
        last = state
        # Stisknutí tlačítka (PULL_UP znamená 0 při stisku, 1 při uvolnění)
        value = b'\x01' if state == 0 else b'\x00'

        for conn in connections:
            ble.gatts_notify(conn, btn_handle, value)

        print("Tlačítko:", state)

    time.sleep_ms(50)

Co je zde jiného než u příkladu s rozsvěcením LED?

U LED jsme nepotřebovali znát připojené klienty, protože klient posílal data nám. U notifikací naopak modul ESP32 posílá data klientovi, a proto musí vědět, komu je má odeslat. Proto si při připojení ukládáme conn_handle do množiny connections. To je velmi důležitý rozdíl mezi Write a Notify.

animace BT tlačítka
Obrázek č. 8 – BT načítání tlačítka BOOT pomocí charakteristiky typu Notify aplikací nRF Connect.

Závěr

V dnešní části jsme si prošli náročnou cestu nastavení základní komunikace. Viděli jsme, že práce na nízké úrovni vyžaduje ruční správu tzv. handle (interních čísel charakteristik), definování příznaků a obsluhu událostí pomocí přerušení IRQ. Ačkoliv je tento přístup náročnější na programování, dává nám plnou kontrolu nad tím, jak ESP32 vysílá svou přítomnost (advertising) a jak reaguje na připojené klienty.

Nyní, když naše ESP32 dokáže komunikovat se světem, si v příštím článku představíme knihovnu aioble, která programování Bluetooth komunikace na straně mikrokontroleru dramaticky zefektivní.

Pošli signál autorovi: