Simulátor Bluetooth Low Energy – nová naděje ve vývoji internetu věcí
Žijeme v jednadvacátém století, kdy s námi zvířata možná ještě nemluví, ale věci s tím rozhodně začínají. Chodíme po světě se svými řídicími centry misí (alias chytrými telefony), které se starají o naše peníze, komunikaci, zprávy, chytré domy a všechna další chytrá řešení, která nás obklopují, zkrátka o náš život.
Trénink na závod? Můžete tak učinit s chytrým monitorem svalového kyslíku Humon. Chcete zlepšit kvalitu svého spánku? Pořiďte si chytrý náramek, chytré hodinky nebo zařízení pro sledování spánku (všechny využívají technologii Bluetooth Low Energy a komunikují s vaším telefonem). Snažíte se někam dojet na kole? Použijte navigaci s podporou BLE, abyste minimalizovali rozptylování, jako jsou oznámení, ale přesto věděli, jak se dostat tam, kam chcete. Možná byste rádi věděli, kdy je v kanceláři hotová káva? Máme pro tebe řešení!“
S příchodem internetu věcí a rostoucí propojeností světa však přišly nové výzvy pro nás, vývojáře.
- Vývoj aplikace s podporou BLE
- Jak to funguje
- Jak to používat
- Kontrakt na periferii
- Nejjednodušší periferie
- Ať připojení trvá nějakou dobu
- Odmítnutí připojení
- Přerušení spojení inicializované periferií
- Změna RSSI v informacích o skenování
- Vyjednávání MTU
- Vynucení hodnot v podporovaném rozsahu
- Zapnutí čidla
- Vysílání oznámení
- Pokročilá periferie v akci
- Použití v automatizovaném testování
- Podívejte se sami
Vývoj aplikace s podporou BLE
Pokud jste někdy pracovali na mobilní aplikaci, která se připojuje k zařízení Bluetooth Low Energy, víte, že to není nejjednodušší úkol. V aplikaci je mnoho obrazovek a vnitřních stavů, které souvisejí s periferií a jejím chováním. Aplikace se často vytváří souběžně s firmwarem (někdy dokonce hardwarem (!!!)) zařízení, spojení může být nestálé, vnější rozhraní nestabilní, vnitřní logika chybná. Výsledkem je, že tým při testování svých funkcí ztrácí spoustu času řešením známých problémů (Known Issues™).
Obecně k tomu lze přistupovat dvěma způsoby:
- První způsob je jednoduchý a časově náročný: použijeme fyzický smartphone a zařízení BLE, projdeme všemi problémy s připojením a nastavením zařízení do stavu, v jakém ho potřebujeme mít, a znovu vytvoříme podmínky pro testování.
- Pak je tu složitější způsob: abstrahovat zařízení, vytvořit jednoduchou maketu, která dokáže skrýt skutečnou manipulaci s BLE. To vám umožní pracovat na emulátoru Androidu/simulátoru iOS, ušetří vám to později nějaký čas a umožní vám to spustit automatizované testy na CI. Zároveň to zvyšuje náklady na údržbu a přináší nové riziko, protože netestujete skutečnou komunikaci při každém spuštění kódu. Koneckonců, ta periferie Bluetooth je pravděpodobně srdcem naší aplikace a nesmí se neočekávaně odpojit nebo se chovat podivně.
Naši přátelé z Frontside – austinské konzultační společnosti v oblasti frontendového softwarového inženýrství a architektury – si uvědomili potřebu efektivnějšího řešení. Požádali nás, abychom vyvinuli open source řešení pro vývoj spolehlivých aplikací s podporou BLE, které by mohl využít každý.
A tak přichází…
BLEmulator /pronun.: bleh-mulator/, simulátor Bluetooth Low Energy.
BLEmulator je tu, aby vám usnadnil život! Zvládne veškerý váš produkční kód související s BLE a simuluje chování skutečné periferie a zásobníku Bluetooth systému. Je jednoduchý a flexibilní a umožňuje vám vytvořit jak základní maketu, tak plnohodnotnou simulaci zařízení s podporou BLE. A co je nejlepší – má otevřený zdrojový kód!
Umožňuje vám otestovat koncept vašeho zařízení bez nákladů na vytvoření hardwarového prototypu. Umožňuje vašemu mobilnímu týmu postupovat vpřed bez čekání na firmware nebo prototypy, pouze se specifikací. Umožňuje pracovat pouze s emulátorem Androidu nebo simulátorem iOS, čímž umožňuje větší mobilitu, snadnější práci na dálku a vyhnutí se omezené dostupnosti fyzických smartphonů. Umožňuje testovat aplikace v automatizovaných testech spouštěných CI.
V současné době je BLEmulator k dispozici pro Flutter a funguje pouze s naší knihovnou FlutterBleLib.
Jak to funguje
Flutter jako multiplatformní framework potřebuje nativní závislosti pro obě platformy. Za normálních okolností by byly součástí samotné knihovny, ale zde jsme použili jiný přístup. Polidea má knihovnu React Native BLE s názvem react-native-ble-plx, což je úžasné dílo našich kolegů. Rozhodli jsme se z ní vyjmout veškerou nativní logiku do samostatné knihovny, známé jako Multiplatform BLE Adapter. Tímto způsobem jsme vytvořili společné jádro, které používají jak react-native-ble-plx, tak náš plugin Flutter, FlutterBleLib. Jako vedlejší efekt jsme vytvořili společnou abstrakci používanou v nativním můstku, BleAdapter
, což je ideální vstupní bod pro simulaci!
Takto vypadá datový tok FlutterBleLib:
- Volání metody na jednom z objektů FlutterBleLib (BleManager, Peripheral, Service, Charakteristika)
- Dart kód odešle název metody a její parametry nativnímu můstku
- Nativní můstek přijme data a v případě potřeby je deserializuje
- Nativní můstek zavolá příslušnou metodu na instanci BleAdapter z multiplatformního BLE adaptéru
- BleAdapter zavolá metodu na RxAndroidBle nebo RxBluetoothKit, v závislosti na platformě
- RxAndroidBle/RxBluetoothKit volá systémovou metodu
- Systém vrací odpověď zprostředkovateli
- Zprostředkovatel vrací odpověď BleAdapteru
- BleAdapter odpovídá nativnímu mostu
- Nativní bridge namapuje odpověď a pošle ji Dart
- Dart analyzuje odpověď a vrátí ji původnímu volajícímu
Měli jsme práci rozdělanou za nás – body 4 až 9 jsou ideální vstupní body s nastavenou externí smlouvou. Injektováním jiné implementace BleAdapteru můžeme simulaci zapnout, kdykoli budeme chtít.
Dále jsme se museli rozhodnout, kde má simulace probíhat. Rozhodli jsme se pro zachování co největší části simulace v jazyce Dart, a to ze dvou hlavních důvodů:
- Jedna definice pro obě platformy
Je důležité minimalizovat jak počet míst, kde je možné udělat chybu, tak množství práce potřebné k vytvoření simulované periferie a její údržbě.
- Jazyk nativní pro platformu
To má několik výhod. Za prvé, vývojáři budou efektivněji pracovat se známým nástrojem, takže bychom se měli vyhnout zavádění dalších jazyků. Za druhé jsme nechtěli omezovat věci, které je možné dělat na simulované periferii. Pokud byste si chtěli vyžádat odpověď z nějakého HTTP serveru (třeba pomocí pokročilejší simulace, na které běží samotný firmware?), můžete tak bez problémů učinit pomocí stejného kódu, jaký byste napsali pro jakoukoli jinou HTTP komunikaci ve své aplikaci.
Simulovaná trasa volání vypadá takto:
- Volání metody na jednom z objektů FlutterBleLib (BleManager, Peripheral, Service, Characteristic)
- Dart kód odešle název metody a její parametry nativnímu můstku
- Nativní můstek přijme data a v případě potřeby je deserializuje
-
Nativní můstek zavolá příslušnou metodu na SimulatedAdapter
Changed nyní začíná část
- BleAdapter – v tomto případě ten ze simulátoru – předá volání nativnímu můstku BLEmulátoru
- BLEmulátor buď provede logiku sám (pokud nezahrnuje periferii), nebo zavolá příslušnou metodu na dodané periferii uživatelem
- Odpověď je předána nativnímu můstku BLEmulátoru
- Nativní můstek BLEmulátoru předá odpověď simulovanému adaptéru
-
Simulovaný adaptér odpoví nativnímu můstku
Zpět k původnímu toku
- Nativní můstek namapuje odpověď a pošle ji Dartu
- Dart odpověď analyzuje a vrátí ji původnímu volajícímu
Takto používáte veškerý svůj kód pro manipulaci s BLE a pracujete s typy poskytovanými FlutterBleLib bez ohledu na to, jaký backend používáte, ať už je to skutečný systém BT stack nebo simulace. To také znamená, že můžete testovat interakci s periferií v automatizovaných testech na vašem CI!
Jak to používat
Probrali jsme, jak to funguje a jaké možnosti to poskytuje, takže se nyní vrhneme na to, jak to používat.
- Přidejte závislost na
blemulator
ve svém pubspecu.yml - Vytvořte si vlastní simulovanou periferii pomocí tříd poskytovaných zásuvným modulem
SimulatedPeripheral
,SimulatedService
aSimulatedCharacteristic
(podrobně se tím budu zabývat v další části) - Přidejte periferii do BLEmulátoru pomocí
Blemulator.addPeripheral(SimulatedPeripheral)
- Zavolejte
Blemulator.simulate()
před volánímBleManager.createClient()
z FlutterBleLib
To je vše, jen čtyři kroky a můžete začít pracovat! No, přiznávám, že ten nejsložitější krok jsme tak trochu přeskočili, takže se pojďme věnovat druhému bodu – definování periferie.
Kontrakt na periferii
Následující příklady budou vycházet z CC2541 SensorTag od Texas Instruments se zaměřením na IR senzor teploty.
Potřebujeme vědět, jak vypadají UUID služby a její vlastnosti. Zajímají nás dvě místa v dokumentaci:
UUID nás zajímají:
- Služba IR teploty:
F000AA00-0451-4000-B000-000000000000
Tato služba obsahuje všechny charakteristiky týkající se teplotního čidla. -
Údaje o teplotě IR:
F000AA01-0451-4000-B000-000000000000
Teplotní údaje, které lze číst nebo sledovat. Formát dat je ObjectLSB:ObjectMSB:AmbientLSB:AmbientMSB. Po dobu sledování bude každou konfigurovatelnou dobu vysílat oznámení.
-
Konfigurace teploty IR:
F000AA02-0451-4000-B000-000000000000
Přepínač zapnutí/vypnutí čidla. Pro tuto charakteristiku existují dvě platné hodnoty::
- 00-senzor uspán (IR teplotní data budou čtyři bajty nul)
- 01-senzor zapnut (IR teplotní data budou vysílat správné hodnoty)
-
IR teplota period:
F000AA03-0451-4000-B000-000000000000
Interval mezi oznámeními. dolní mez je 300 ms, horní mez je 1000 ms. Hodnota charakteristiky se násobí 10, takže podporované hodnoty jsou mezi 30 a 100.
To je vše, co jsme hledali, takže přejděme k implementaci!!!
Nejjednodušší periferie
Nejjednodušší simulace přijme libovolnou hodnotu a uspěje ve všech operacích.
V Dartu to vypadá takto:
Krátké a stručné, vypadá to skoro jako JSON.
Děje se toto: Vytvořili jsme periferii s názvem SensorTag, která má runtime zadané ID (stačí jakýkoli řetězec, ale musí být unikátní mezi periferiemi známými BLEmulátoru). Dokud je zapnuto skenování periferie, bude se každých 800 milisekund ohlašovat pomocí výchozích informací o skenování. Obsahuje jednu službu, jejíž UUID je obsaženo v reklamních datech. Služba obsahuje 3 charakteristiky, stejně jako u skutečného zařízení, a všechny jsou čitelné. Do první z charakteristik nelze zapisovat, ale podporuje oznámení; další dvě nelze sledovat, ale lze do nich zapisovat. Pro charakteristiky neexistují žádné neplatné hodnoty. Argument convenienceName
není BLEmulátorem nijak využíván, ale usnadňuje čtení definice.
Konfigurace teploty IR a Perioda teploty IR přijímají a nastavují jakoukoli hodnotu, která jim byla předána. Charakteristika IR Temperature Data podporuje notifikace, ale ve skutečnosti nikdy žádné neposílá, protože jsme je nijak nedefinovali.
BLEmulator poskytuje základní chování z krabice, stará se o šťastnou cestu pro všechny vaše konstrukce, čímž minimalizuje množství potřebné práce. I když může stačit pro některé testy a základní kontroly dodržování specifikace, nesnaží se chovat jako skutečné zařízení.
Musíme implementovat vlastní chování!
Při vytváření BLEmulatoru jsme zachovali jednoduchost i flexibilitu. Chtěli jsme vývojářům poskytnout potřebnou kontrolu nad každým aspektem vytvářené periferie a zároveň od nich vyžadovat co nejméně práce při vytváření funkční definice. Abychom tohoto cíle dosáhli, rozhodli jsme se vytvořit výchozí implementaci všech metod, které by mohly být vstupním bodem pro vlastní chování, a nechat vás rozhodnout, co přepsat.
Nyní přidáme nějakou logiku.
Ať připojení trvá nějakou dobu
Skutečné periferii bude pravděpodobně nějakou dobu trvat, než se připojí. Abyste toho dosáhli, stačí přepsat jednu metodu:
To je vše. Nyní připojení trvá 200 milisekund!
Odmítnutí připojení
Podobný případ. Chceme zachovat zpoždění, ale vrátit chybu, že se spojení nepodařilo navázat. Mohli byste metodu přepsat a vyhodit SimulatedBleError
sami, ale můžete také udělat:
@override Future<bool> onConnectRequest() async { await Future.delayed(Duration(milliseconds: 200)); return false; }
Přerušení spojení inicializované periferií
Řekněme, že chcete zkontrolovat proces opětovného připojení nebo simulovat vyřazení z dosahu. Můžete požádat kolegu, aby běžel na druhou stranu kanceláře se skutečnou periferií, nebo přidat nějaké ladicí tlačítko a v jeho onPress
udělat:
yourPeripheralInstance.onDisconnect();
(I když první možnost se zdá být uspokojivější.)
Změna RSSI v informacích o skenování
Řekněme, že chceme seřadit periferie podle jejich vnímané síly signálu a potřebujeme to otestovat. Vytvoříme několik simulovaných periferií, jednu necháme se statickým RSSI a v druhé uděláme:
@overrideScanResult scanResult() { scanInfo.rssi = -20 - Random.nextInt(50); return super.scanResult();}
Takto můžeme mít několik zařízení s proměnlivým RSSI a otestovat funkci.
BLEmulátor provádí většinu logiky sám, čímž omezí MTU na podporovaný rozsah 23 až 512, ale pokud ji potřebujete omezit ještě více, měli byste přepsat metodu requestMtu()
:
BLEmulátor automaticky vyjedná nejvyšší podporovanou MTU v systému iOS.
Vynucení hodnot v podporovaném rozsahu
Chcete-li omezit hodnoty přijímané charakteristikou, musíte vytvořit novou třídu rozšiřující třídu SimulatedCharacteristic
.
Vlastnost nyní omezuje vstup na 0 nebo 1 na prvním bajtu, ignoruje všechny další bajty a vrací chybu, pokud hodnota přesahuje podporovaný rozsah. Aby charakteristika podporovala vracení chyby, musí vracet odpověď na operaci zápisu, proto nastavujeme writableWithoutResponse
na false.
Zapnutí čidla
Rádi bychom věděli, kdy je čidlo teploty zapnuté nebo vypnuté.
Abychom toho dosáhli, vytvoříme novou službu s natvrdo zadanými UUID:
Námi definovanou charakteristiku nelze sledovat prostřednictvím FlutterBleLib, protože jí chybí isNotifiable: true
, ale lze ji, pro vaše pohodlí, sledovat na úrovni BLEmulátoru. To usnadňuje kontrolu celkového toku, který simulujeme, zjednodušuje strukturu a umožňuje nám to vyhnout se zbytečnému rozšiřování základních tříd.
Vysílání oznámení
Stále nám chybí vysílání oznámení z charakteristiky IR Temperature Data. Postaráme se o to.
_emitTemperature()
je volán v konstruktoru služby Temperature Service a běží v nekonečné smyčce. V každém intervalu, který je určen hodnotou charakteristiky IR Temperature Period, se kontroluje, zda existuje posluchač (isNotifying
). Pokud existuje, zapíše data (nuly nebo náhodnou hodnotu podle toho, zda je čidlo zapnuté nebo vypnuté) do charakteristiky IR Temperature Data. SimulatedCharacteristic.write()
O nové hodnotě informuje všechny aktivní posluchače.
Pokročilá periferie v akci
Kompletní příklad pokročilejší periferie najdete na úložišti BLEmulátoru. Pokud byste si ho chtěli vyzkoušet, stačí naklonovat repozitář a spustit jeho příklad.
Použití v automatizovaném testování
Velký dík na tomto místě patří mému kolegovi z Polideanu Pawlu Byszewskému za jeho výzkum použití BLEmulatoru v automatizovaných testech.
Flutter má odlišný kontext provádění pro testovanou aplikaci a pro samotný test, což znamená, že nelze jen tak sdílet simulovanou periferii mezi nimi a měnit chování z testu. Co můžete udělat, je přidat obsluhu dat do testovacího ovladače pomocí enableFlutterDriverExtension(handler: DataHandler)
, předat simulované periferie do main()
vaší aplikace a předávat řetězcové zprávy obsluze uvnitř kontextu provádění aplikace.
Shrnuje se to do:Wrapper pro aplikaci
Vaše periferie
Vaše testy
Díky tomuto mechanismu můžete inicializovat periferie, jak chcete, a volat libovolné chování, které máte předdefinované uvnitř simulovaného zařízení.
Podívejte se sami
Nejlepší na tom všem je, že mi nemusíte věřit! Vyzkoušejte si to sami na GitHubu. Používejte to, bavte se s tím, zkuste to rozbít a dejte nám o tom vědět!
Chcete to vidět i na jiných platformách? Ozvěte se nám a my to zařídíme! Určitě se podívejte na naše další knihovny a neváhejte nás kontaktovat, pokud byste chtěli, abychom pracovali na vašem projektu.