Simulator Bluetooth Low Energy – o nouă speranță în dezvoltarea IoT

Am trăit în secolul XXI, unde poate că animalele încă nu vorbesc cu noi, dar lucrurile încep cu siguranță să o facă. Ne plimbăm cu centrele noastre de control al misiunii (aka smartphone-uri) care se pot ocupa de banii noștri, de comunicarea noastră, de știrile noastre, de casele noastre inteligente și de orice altă soluție inteligentă care ne înconjoară, pe scurt, de viețile noastre.

Se antrenează pentru o cursă? Puteți face acest lucru cu un monitor inteligent de oxigen pentru mușchi Humon. Vreți să vă îmbunătățiți calitatea somnului? Ia-ți o brățară inteligentă, un ceas inteligent sau un dispozitiv de urmărire a somnului (toate acestea folosesc Bluetooth Low Energy și comunică cu telefonul tău). Încercați să ajungeți undeva pe bicicletă? Folosiți sistemul de navigație cu BLE pentru a minimiza distragerile, cum ar fi notificările, dar pentru a ști cum să ajungeți unde doriți. Poate că ați dori să știți când este gata cafeaua de la birou? Vă acoperim noi!

Dar odată cu apariția Internetului Lucrurilor și interconectarea din ce în ce mai mare a lumii au apărut noi provocări pentru noi, dezvoltatorii.

Dezvoltarea unei aplicații compatibile BLE

Dacă ați lucrat vreodată la o aplicație mobilă care se conectează la un dispozitiv Bluetooth Low Energy, știți că nu este cea mai ușoară sarcină. Există multe ecrane și stări interne în aplicație care sunt legate de periferic și de comportamentul acestuia. Adesea, aplicația este creată în paralel cu firmware-ul (uneori chiar hardware (!!!!)) dispozitivului, conexiunea poate fi nestatornică, interfața externă instabilă, logica internă eronată. Ca urmare, echipa pierde mult timp cu Known Issues™ atunci când își testează funcțiile.

În general, există două modalități de abordare a acestui aspect:

  1. Primul, există o modalitate ușoară și consumatoare de timp: să folosim un smartphone fizic și un dispozitiv BLE, să trecem prin toate bătăile de cap pentru a conecta și configura dispozitivul în starea în care avem nevoie să fie și să recreăm condițiile de testare.
  2. Apoi există calea dificilă: abstractizăm dispozitivul, creăm o machetă simplă care poate ascunde manipularea BLE reală. Acest lucru vă va permite să lucrați pe un emulator Android/simulator iOS, economisind timp mai târziu și permițându-vă să rulați teste automate pe CI-urile dvs. În același timp, crește costul de întreținere și introduce un nou risc prin faptul că nu testați comunicarea reală de fiecare dată când vă executați codul. La urma urmei, acel periferic Bluetooth se află probabil în centrul aplicației noastre și nu trebuie să se deconecteze în mod neașteptat sau să se comporte în mod ciudat.

Amicii noștri de la Frontside – o firmă de consultanță în domeniul arhitecturii și ingineriei software front-end cu sediul în Austin – au recunoscut nevoia unei soluții mai eficiente. Ei ne-au cerut să dezvoltăm o soluție open source pentru o dezvoltare fiabilă a aplicațiilor cu BLE de care toată lumea ar putea beneficia.

Și astfel apare…

BLEmulator /pronun.: bleh-mulator/, un simulator Bluetooth Low Energy.

BLEmulator este aici pentru a vă face viața mai ușoară! Acesta se ocupă de tot codul dvs. de producție legat de BLE și simulează comportamentul unui periferic real și al stivei Bluetooth a sistemului. Este simplu și flexibil, permițându-vă să creați atât simularea de bază, cât și o simulare completă a unui dispozitiv compatibil BLE. Și cel mai bun lucru – este cu sursă deschisă!

Vă permite să testați conceptul din spatele dispozitivului dvs. fără costul prototipării hardware. Permite echipei dvs. mobile să avanseze fără a aștepta firmware sau prototipuri, doar cu o specificație. Vă permite să lucrați folosind doar emulatorul Android sau simulatorul iOS, permițând astfel o mai mare mobilitate, o muncă la distanță mai ușoară și evitarea disponibilității limitate a smartphone-urilor fizice. Vă permite să vă testați aplicațiile în teste automatizate rulate de CI-ul dumneavoastră.

În prezent, BLEmulator este disponibil pentru Flutter și funcționează numai cu FlutterBleLib-ul nostru.

Cum funcționează

Flutter, ca un cadru multiplatformă, are nevoie de dependențe native pentru ambele platforme. În mod normal, acestea ar face parte din biblioteca însăși, dar am folosit o abordare diferită aici. Polidea are o bibliotecă React Native BLE, numită react-native-ble-plx, o lucrare minunată a colegilor noștri. Am decis să extragem toată logica nativă din ea într-o bibliotecă separată, cunoscută sub numele de Multiplatform BLE Adapter. În acest fel, am creat un nucleu comun care este utilizat atât de react-native-ble-plx, cât și de pluginul nostru Flutter, FlutterBleLib. Ca efect secundar, am creat o abstracțiune comună utilizată în puntea nativă, BleAdapter, care este un punct de intrare perfect pentru simulare!

Acesta este modul în care arată fluxul de date al FlutterBleLib:

  1. Apelează o metodă pe unul dintre obiectele FlutterBleLib (BleManager, Peripheral, Service, Characteristic)
  2. Codul Dart trimite un nume de metodă și parametrii săi către o punte nativă
  3. Puntea nativă primește datele și le deserializează dacă este necesar
  4. Puntea nativă apelează o metodă corespunzătoare pe instanța BleAdapter din adaptorul BLE multiplatformă
  5. BleAdapter apelează metoda fie pe RxAndroidBle, fie pe RxBluetoothKit, în funcție de platformă
  6. RxAndroidBle/RxBluetoothKit apelează o metodă de sistem
  7. Sistemul returnează un răspuns către intermediar
  8. Intermediarul returnează răspunsul către BleAdapter
  9. BleAdapter răspunde către puntea nativă
  10. Nativă bridge mapează răspunsul și îl trimite către Dart
  11. Dart analizează răspunsul și îl returnează către apelantul inițial

Am avut munca pregătită pentru noi – punctele 4-9 sunt punctele de intrare ideale cu un contract extern stabilit. Prin injectarea unei implementări diferite a BleAdapter se poate porni simularea oricând dorim.

În continuare, a trebuit să decidem unde ar trebui să aibă loc simularea. Am optat pentru a păstra cât mai mult din simulare în Dart, din două motive principale:

  1. O singură definiție pentru ambele platforme

Este important să minimizăm atât numărul de locuri în care este posibil să facem o greșeală, cât și cantitatea de muncă necesară pentru a crea un periferic simulat și pentru a-l menține.

  1. Limbajul nativ al platformei

Acest lucru are câteva avantaje. În primul rând, dezvoltatorii vor lucra mai eficient cu un instrument cunoscut, astfel încât ar trebui să evităm introducerea unor limbaje suplimentare. În al doilea rând, nu am vrut să limităm lucrurile care sunt posibile de făcut pe perifericul simulat. Dacă doriți să solicitați un răspuns de la unele servere HTTP (poate cu o simulare mai avansată, care să ruleze chiar firmware-ul?), o puteți face fără probleme cu același cod pe care l-ați scrie pentru orice altă comunicare HTTP în aplicația dumneavoastră.

Ruta de apelare simulată arată astfel:

  1. Apelează o metodă pe unul dintre obiectele FlutterBleLib (BleManager, Peripheral, Service, Characteristic)
  2. Codul Dart trimite numele metodei și parametrii acesteia către puntea nativă
  3. Pontea nativă primește datele și le deserializează dacă este necesar
  4. Pontea nativă apelează metoda corespunzătoare de pe SimulatedAdapter

    Changed porțiunea începe acum

  5. BleAdapter – în acest caz, cel din simulator – transmite apelul către puntea nativă a BLEmulatorului
  6. BleAdapter fie face el însuși logica (dacă nu implică perifericul), fie apelează metoda corespunzătoare pe perifericul furnizat de către utilizator
  7. Răspunsul este transmis către puntea nativă a BLEmulatorului
  8. Puntea nativă a BLEmulatorului transmite răspunsul către SimulatedAdapter
  9. SimulatedAdapter răspunde către puntea nativă

    Înapoi la fluxul inițial

  10. Puntea nativă mapează răspunsul și îl trimite către Dart
  11. Dart analizează răspunsul și îl returnează către apelantul original

În acest fel, utilizați tot codul de manipulare BLE și lucrați cu tipurile furnizate de FlutterBleLib, indiferent de backend-ul pe care îl utilizați, fie că este vorba de stiva BT a sistemului real sau de simulare. Acest lucru înseamnă, de asemenea, că puteți testa interacțiunea cu un periferic în testele automate de pe CI-ul dvs.!

Cum se utilizează

Am acoperit cum funcționează și ce posibilități oferă, așa că acum să trecem la modul de utilizare.

  1. Adaugați dependența la blemulator în pubspec-ul dvs.yml
  2. Creați-vă propriul periferic simulat folosind clasele furnizate de plugin SimulatedPeripheral, SimulatedService și SimulatedCharacteristic (le voi aborda în detaliu în secțiunea următoare)
  3. Adaugați perifericul la BLEmulator folosind Blemulator.addPeripheral(SimulatedPeripheral)
  4. Chemați Blemulator.simulate() înainte de a apela BleManager.createClient() din FlutterBleLib

Asta este, doar patru pași și sunteți gata de funcționare! Ei bine, recunosc că cel mai complex pas a cam fost sărit, așa că haideți să vorbim despre al doilea punct-definirea perifericului.

Contractul perifericului

Voi baza următoarele exemple pe CC2541 SensorTag de la Texas Instruments, concentrându-mă pe senzorul de temperatură IR.

Trebuie să știm cum arată UUID-urile serviciului și caracteristicile sale. Suntem interesați de două locuri din documentație.

UUID-urile care ne interesează:

  • Serviciul de temperatură IR: F000AA00-0451-4000-B000-000000000000Acest serviciu conține toate caracteristicile referitoare la senzorul de temperatură.
  • Date de temperatură IR: F000AA01-0451-4000-B000-000000000000

    Datele de temperatură care pot fi citite sau monitorizate. Formatul datelor este ObjectLSB:ObjectMSB:AmbientLSB:AmbientMSB. Va emite o notificare la fiecare perioadă configurabilă în timp ce este monitorizată.

  • IR temperature config: F000AA02-0451-4000-B000-000000000000

    Interuptor pornit/oprit pentru senzor. Există două valori valide pentru această caracteristică::

    • 00-senzor pus în adormire (datele de temperatură IR vor fi patru octeți de zerouri)
    • 01-senzor activat (datele de temperatură IR vor emite citiri corecte)
  • Perioadă temperatură IR:

    Intervalul dintre notificări. limita inferioară este de 300 ms, limita superioară este de 1000 ms. Valoarea caracteristicii este înmulțită cu 10, astfel încât valorile acceptate sunt cuprinse între 30 și 100.

Asta este tot ce căutam, așa că să trecem la implementare!

Cel mai simplu periferic

Cea mai simplă simulare va accepta orice valoare și va reuși în toate operațiile.

În Dart arată astfel:

Scurtă și concisă, arată aproape ca un JSON.

Iată ce se întâmplă: am creat un periferic numit SensorTag care are un ID specificat în timp de execuție (orice șir de caractere este în regulă, dar trebuie să fie unic printre perifericele cunoscute de BLEmulator). În timp ce scanarea perifericului este activată, acesta se va anunța folosind informațiile de scanare implicite la fiecare 800 de milisecunde. Acesta conține un serviciu, al cărui UUID este conținut în datele de anunț. Serviciul conține 3 caracteristici, la fel ca pe un dispozitiv real, și toate acestea pot fi citite. Prima dintre caracteristici nu poate fi scrisă, dar acceptă notificări; celelalte două nu pot fi monitorizate, dar pot fi scrise. Nu există valori invalide pentru aceste caracteristici. Argumentul convenienceName nu este utilizat în niciun fel de BLEmulator, dar face ca definiția să fie mai ușor de citit.

IR Temperature Config și IR Temperature Period acceptă și setează orice valoare care le este transmisă. Caracteristica IR Temperature Data suportă notificări, dar nu trimite niciodată de fapt niciuna, deoarece nu le-am definit în nici un fel.

BLEmulator oferă comportamentul de bază din start, având grijă de calea fericită pentru toate construcțiile dumneavoastră, minimizând cantitatea de muncă necesară. Deși poate fi suficient pentru unele teste și verificări ale respectării specificațiilor de bază, acesta nu încearcă să se comporte ca un dispozitiv real.

Trebuie să implementăm un comportament personalizat!

Am păstrat atât simplitatea, cât și flexibilitatea atunci când am creat BLEmulator. Am vrut să le oferim dezvoltatorilor controlul necesar asupra fiecărui aspect al perifericului creat, solicitând în același timp cât mai puțină muncă din partea lor pentru a crea o definiție funcțională. Pentru a atinge acest obiectiv, am decis să creăm implementarea implicită a tuturor metodelor care ar putea fi punctul de intrare pentru un comportament personalizat și să vă lăsăm să decideți ce să suprascrieți.

Acum, să adăugăm puțină logică.

Faceți ca conectarea să dureze

Un periferic real va dura probabil ceva timp înainte de a se conecta. Pentru a realiza acest lucru, trebuie doar să suprascrieți o singură metodă:

Asta este. Acum conexiunea durează 200 de milisecunde!

Refuzarea conexiunii

Un caz similar. Vrem să păstrăm întârzierea, dar să returnăm o eroare conform căreia conexiunea nu a putut fi stabilită. Ați putea suprascrie metoda și să aruncați voi înșivă un SimulatedBleError, dar puteți face și:

 @override Future<bool> onConnectRequest() async { await Future.delayed(Duration(milliseconds: 200)); return false; }

Deconectare inițializată de periferic

Să spunem că doriți să verificați procesul de reconectare sau să simulăm ieșirea din interval. Puteți cere unui coleg să alerge în cealaltă parte a biroului cu un periferic real sau puteți adăuga un buton de depanare și în onPress acestuia să faceți:

yourPeripheralInstance.onDisconnect();

(Deși prima opțiune pare a fi mai satisfăcătoare.)

Modificarea RSSI în informațiile de scanare

În regulă, să spunem că vrem să sortăm perifericele în funcție de puterea semnalului perceput și trebuie să testăm acest lucru. Creăm câteva periferice simulate, lăsăm unul cu RSSI static și apoi în celălalt facem:

@overrideScanResult scanResult() { scanInfo.rssi = -20 - Random.nextInt(50); return super.scanResult();}

În acest fel, puteți avea câteva dispozitive cu RSSI variabil și puteți testa funcția.

Negocierea MTU

BLEmulator face cea mai mare parte a logicii de unul singur, limitând astfel MTU la intervalul suportat de la 23 la 512, dar dacă aveți nevoie să îl limitați și mai mult, trebuie să suprascrieți metoda requestMtu():

BLEmulator va negocia automat cel mai mare MTU suportat pe iOS.

Forțarea valorilor în intervalul suportat

Pentru a limita valorile acceptate de o caracteristică, trebuie să creați o nouă clasă care să extindă SimulatedCharacteristic.

Caracteristica limitează acum intrarea la 0 sau 1 pe primul octet, ignoră orice octet suplimentar și returnează o eroare dacă valoarea depășește intervalul suportat. Pentru a suporta returnarea unei erori, o caracteristică trebuie să returneze un răspuns la operația de scriere, de unde setarea writableWithoutResponse la false.

Pornirea senzorului

Am dori să știm când este pornit sau oprit senzorul de temperatură.

Pentru a realiza acest lucru, vom crea un nou serviciu cu UUID-uri hardcodate:

Caracteristica pe care am definit-o nu poate fi monitorizată prin FlutterBleLib, deoarece îi lipsește isNotifiable: true, dar poate fi, pentru comoditate, monitorizată la nivelul BLEmulator. Acest lucru facilitează controlul fluxului general pe care îl simulăm, simplifică structura și ne permite să evităm extinderile inutile ale claselor de bază.

Emiterea de notificări

Încă ne lipsește emiterea de notificări de la caracteristica IR Temperature Data. Să ne ocupăm de aceasta.

_emitTemperature() este apelat în constructorul Serviciului de temperatură și se execută într-o buclă infinită. La fiecare interval, specificat de valoarea caracteristicii IR Temperature Period, se verifică dacă există un ascultător (isNotifying). Dacă există unul, atunci scrie date (zerouri sau o valoare aleatorie, în funcție de faptul că senzorul este pornit sau oprit) în caracteristica IR Temperature Data. SimulatedCharacteristic.write() notifică orice ascultători activi cu privire la noua valoare.

Periferic avansat în acțiune

Puteți găsi un exemplu complet al unui periferic mai avansat în repertoriul BLEmulator’s repository. Dacă doriți să-l încercați, trebuie doar să clonați depozitul și să rulați exemplul său.

Utilizare în testele automate

Un mare mulțumesc, aici, colegului meu Polidean Paweł Byszewski pentru cercetarea sa privind utilizarea BLEmulator în testele automate.

Flutter are un context de execuție diferit pentru aplicația testată și pentru testul în sine, ceea ce înseamnă că nu puteți pur și simplu să partajați un periferic simulat între cele două și să modificați comportamentul din test. Ceea ce puteți face este să adăugați un manipulator de date la driverul de testare folosind enableFlutterDriverExtension(handler: DataHandler), să treceți perifericele simulate la main() al aplicației dvs. și să treceți mesaje de tip șir de caractere către manipulator în interiorul contextului de execuție al aplicației.

Se reduce la:Wrapper pentru aplicație

Perifericul tău

Testul tău

Grație acestui mecanism, ai putea să inițializezi perifericul oricum dorești și să apelezi oricare dintre comportamentele pe care le-ai predefinit în interiorul dispozitivului tău simulat.

Vezi cu ochii tăi

Cel mai bun lucru despre toate acestea este că nu trebuie să mă crezi! Verificați singur pe GitHub. Folosiți-l, distrați-vă cu el, încercați să îl spargeți și spuneți-ne totul despre asta!

Ați dori să îl vedeți și pe alte platforme? Contactați-ne și vom face ca acest lucru să se întâmple! Nu uitați să verificați celelalte biblioteci ale noastre și nu ezitați să ne contactați dacă doriți să lucrăm la proiectul dumneavoastră.