Bluetooth Low Energy Simulator – et nyt håb inden for IoT-udvikling

Vi lever i det XXI århundrede, hvor dyr måske stadig ikke taler til os, men ting er helt sikkert begyndt at gøre det. Vi går rundt med vores missionskontrolcentre (aka smartphones), der kan håndtere vores penge, vores kommunikation, vores nyheder, vores smarte hjem og alle andre smarte løsninger, der omgiver os, kort sagt – vores liv.

Træner vi til et løb? Det kan du gøre med en smart muskel-oxygen-monitor Humon. Vil du forbedre din søvnkvalitet? Få et smartbånd, et smartwatch eller en søvn-tracker (som alle bruger Bluetooth Low Energy og kommunikerer med din telefon). Prøver du at komme et sted hen på en cykel? Brug BLE-aktiveret navigation for at minimere distraktioner som notifikationer, men stadig vide, hvordan du kommer frem til det sted, du ønsker. Måske vil du gerne vide, hvornår kaffen på kontoret er klar? Vi har styr på det!

Men med fremkomsten af Internet of Things og den stigende sammenkobling af verden kom der nye udfordringer for os udviklere.

Udvikling af en BLE-aktiveret app

Hvis du nogensinde har arbejdet på en mobilapp, der opretter forbindelse til en Bluetooth Low Energy-enhed, ved du, at det ikke er den letteste opgave. Der er mange skærme og interne tilstande i appen, der er forbundet med periferien og dens adfærd. Appen bliver ofte oprettet sammen med enhedens firmware (nogle gange endda hardware (!!!)), forbindelsen kan være ustabil, den eksterne grænseflade ustabil, den interne logik fejlbehæftet. Som følge heraf spilder teamet en masse tid på kendte problemer™, når de tester deres funktioner.

Generelt er der to måder at gribe dette an på:

  1. Først er der en nem, tidskrævende måde: Brug en fysisk smartphone og BLE-enhed, gå igennem alt besværet med at tilslutte og opsætte enheden til den tilstand, vi har brug for, og genskabe testbetingelserne.
  2. Dernæst er der den svære måde: abstrahere enheden, oprette en simpel mock-up, der kan skjule den faktiske BLE-håndtering. Dette vil lade dig arbejde på en Android-emulator / iOS-simulator, hvilket sparer dig noget tid senere og lader dig køre automatiserede tests på dine CIs. Samtidig øger det vedligeholdelsesomkostningerne og introducerer en ny risiko ved ikke at teste den faktiske kommunikation, hver gang du kører din kode. Når alt kommer til alt, er den Bluetooth-periferi sandsynligvis kernen i vores applikation, og den må ikke afbryde forbindelsen uventet eller opføre sig mærkeligt.

Vores venner hos Frontside – et i Austin baseret konsulentfirma for frontend-softwareudvikling og -arkitektur – har erkendt behovet for en mere effektiv løsning. De bad os om at udvikle en open source-løsning til en pålidelig BLE-aktiveret app-udvikling, som alle kunne drage fordel af.

Og så kommer ind…

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

BLEmulator er her for at gøre dit liv lettere! Den håndterer al din produktions BLE-relaterede kode og simulerer opførslen af et rigtigt periferiudstyr og systemets Bluetooth-stack. Den er enkel og fleksibel, så du kan oprette både den grundlæggende mock og en fuld simulering af en BLE-aktiveret enhed. Og det bedste er, at den er open source!

Den giver dig mulighed for at teste konceptet bag din enhed uden omkostningerne ved hardwareprototypering. Det giver dit mobilteam mulighed for at komme videre uden at vente på firmware eller prototyper, blot med en specifikation. Det giver dig mulighed for at arbejde kun ved hjælp af Android-emulator eller iOS-simulator, hvilket giver mulighed for større mobilitet, lettere fjernarbejde og undgår den begrænsede tilgængelighed af fysiske smartphones. Det lader dig teste dine apps i automatiserede tests, der køres af dit CI.

I øjeblikket er BLEmulator tilgængelig for Flutter og fungerer kun med vores FlutterBleLib.

Sådan fungerer det

Flutter, som en multiplatformsramme, har brug for native afhængigheder for begge platforme. Normalt ville disse være en del af selve biblioteket, men vi har brugt en anden tilgang her. Polidea har et React Native BLE-bibliotek, kaldet react-native-ble-plx, et fantastisk arbejde af vores kolleger. Vi har besluttet at udtrække al den native logik fra det til et separat bibliotek, kendt som Multiplatform BLE Adapter. På den måde har vi skabt en fælles kerne, der bruges af både react-native-ble-plx og vores Flutter-plugin, FlutterBleLib. Som en sideeffekt har vi skabt en fælles abstraktion, der bruges i den native bro, BleAdapter, som er et perfekt indgangspunkt for simulering!

Dette er, hvordan FlutterBleLib’s datastrøm ser ud:

  1. Kald en metode på et af FlutterBleLib-objekterne (BleManager, Peripheral, Service, Characteristic)
  2. Dartkode sender et metode navn og dets parametre over til en native bridge
  3. Native bridge modtager dataene og deserialiserer dem om nødvendigt
  4. Native bridge kalder en passende metode på BleAdapter instansen fra Multiplatform BLE Adapteren
  5. BleAdapteren kalder metoden på enten RxAndroidBle eller RxBluetoothKit, afhængigt af platformen
  6. RxAndroidBle/RxBluetoothKit kalder en systemmetode
  7. Systemet returnerer et svar til mellemmanden
  8. Mellemmanden returnerer svaret til BleAdapteren
  9. BleAdapteren svarer til den native bro
  10. Native bridge kortlægger svaret og sender det over til Dart
  11. Dart analyserer svaret og returnerer det til den oprindelige opkalder

Vi havde vores arbejde lagt ud for os – punkt 4 til 9 er de ideelle indgangspunkter med en fastlagt ekstern kontrakt. Ved at injicere en anden implementering af BleAdapter kan man slå simuleringen til når som helst vi ønsker det.

Næst skulle vi beslutte, hvor simuleringen skulle finde sted. Vi valgte at holde så meget af simuleringen som muligt i Dart af to hovedårsager:

  1. En definition til begge platforme

Det er vigtigt at minimere både antallet af steder, hvor det er muligt at begå fejl, og mængden af arbejde, der er nødvendigt for at oprette en simuleret periferi og vedligeholde den.

  1. Sprog hjemmehørende i platformen

Dette har et par fordele. For det første vil udviklerne arbejde mere effektivt med et kendt værktøj, så vi bør undgå at indføre yderligere sprog. For det andet ønskede vi ikke at begrænse de ting, der er mulige at gøre på det simulerede periferiudstyr. Hvis du gerne vil anmode om et svar fra nogle HTTP-servere (måske med en mere avanceret simulering, der kører selve firmwaren?), kan du gøre det uden problemer med den samme kode, som du ville skrive til enhver anden HTTP-kommunikation i din app.

Den simulerede call-route ser således ud:

  1. Kald en metode på et af FlutterBleLib-objekterne (BleManager, Peripheral, Service, Characteristic)
  2. Dartkoden sender metodens navn og dens parametre over til den native bridge
  3. Den native bridge modtager dataene og deserialiserer dem om nødvendigt
  4. Den native bridge kalder den relevante metode på SimulatedAdapter

    Changed delen starter nu

  5. BleAdapter – i dette tilfælde den fra simulatoren – videresender opkaldet til BLEmulatorens native bridge
  6. BLEmulator udfører enten logikken selv (hvis den ikke involverer periferien) eller kalder den relevante metode på den leverede periferi af brugeren
  7. Svaret sendes til BLEmulatorens native bridge
  8. BLEmulatorens native bridge sender svaret videre til SimulatedAdapter
  9. SimulatedAdapter svarer til den native bridge

    Tilbage til det oprindelige flow

  10. Native bridge kortlægger svaret og sender det over til Dart
  11. Dart analyserer svaret og returnerer det til den oprindelige opkalder

På denne måde bruger du al din BLE-håndteringskode og arbejder på typer, der leveres af FlutterBleLib, uanset hvilken backend du bruger, uanset om det er det reelle system BT stack eller simuleringen. Det betyder også, at du kan teste interaktionen med en periferi i automatiserede tests på dit CI!

Sådan bruger du det

Vi har dækket, hvordan det fungerer, og hvilke muligheder det giver, så lad os nu springe over til, hvordan du bruger det.

  1. Føj afhængighed til blemulator i din pubspec.yml
  2. Opret din egen simulerede periferi ved hjælp af de klasser, der leveres af plugin SimulatedPeripheral, SimulatedService og SimulatedCharacteristic (jeg vil dække det i detaljer i næste afsnit)
  3. Føj periferien til BLEmulatoren ved hjælp af Blemulator.addPeripheral(SimulatedPeripheral)
  4. Kald Blemulator.simulate() før du kalder BleManager.createClient() fra FlutterBleLib

Det er det, kun fire trin, og du er i gang! Nå, jeg indrømmer, at det mest komplekse trin ligesom er blevet sprunget over, så lad os tale om det andet punkt – at definere periferien.

Periferikontrakt

Jeg vil basere de følgende eksempler på CC2541 SensorTag fra Texas Instruments, med fokus på IR-temperatursensor.

Vi skal vide, hvordan UUID’erne for tjenesten og dens egenskaber ser ud. Vi er interesserede to steder i dokumentationen.

UUUID’er, som vi er interesserede i:

  • IR-temperaturtjeneste: F000AA00-0451-4000-B000-000000000000Denne tjeneste indeholder alle de egenskaber, der vedrører temperatursensoren.
  • IR-temperaturdata:

  • F000AA01-0451-4000-B000-000000000000

    De temperaturdata, der kan aflæses eller overvåges. Formatet for dataene er ObjectLSB:ObjectMSB:AmbientLSB:AmbientMSB. Den udsender en meddelelse hver konfigurerbar periode, mens den overvåges.

  • IR-temperaturkonfiguration: F000AA02-0451-4000-B000-000000000000

    Tænd/sluk-knap for sensoren. Der er to gyldige værdier for denne egenskab::

    • 00-sensor sat i dvale (IR-temperaturdata vil være fire bytes med nuller)
    • 01-sensor aktiveret (IR-temperaturdata vil afgive korrekte aflæsninger)
  • IR-temperaturperiode: F000AA03-0451-4000-B000-000000000000

    Intervallet mellem notifikationer.Den nedre grænse er 300 ms, den øvre grænse er 1000 ms. Karakteristikens værdi ganges med 10, så de understøttede værdier ligger mellem 30 og 100.

Det var alt, hvad vi ledte efter, så lad os gå til implementeringen!

Simpelste periferi

Den enkleste simulering accepterer enhver værdi og lykkes i alle operationer.

I Dart ser det således ud:

Kort og kortfattet, ligner næsten en JSON.

Her er, hvad der sker: Vi har oprettet en periferi ved navn SensorTag, som har et runtime-specificeret ID (enhver streng er fin, men det skal være unikt blandt periferier, der er kendt af BLEmulator). Mens periferisk scanning er slået til, vil den annoncere ved hjælp af standard scanningsinformationer hvert 800 millisekunder. Den indeholder en tjeneste, hvis UUID er indeholdt i annonceringsdataene. Tjenesten indeholder 3 egenskaber, ligesom på en rigtig enhed, og de kan alle læses. Den første af egenskaberne kan der ikke skrives til, men understøtter meddelelser; de to andre kan ikke overvåges, men kan skrives til. Der er ingen ugyldige værdier for karakteristika. Argumentet convenienceName bruges ikke på nogen måde af BLEmulatoren, men gør definitionen lettere at læse.

IR Temperature Config og IR Temperature Period accepterer og indstiller enhver værdi, der overføres til den. IR Temperature Data karakteristik understøtter notifikationer, men sender faktisk aldrig nogen, da vi ikke har defineret dem på nogen måde.

BLEmulator giver den grundlæggende adfærd ud af boksen, der tager sig af den lykkelige vej for alle dine konstruktioner, hvilket minimerer den nødvendige mængde arbejde. Selv om det kan være tilstrækkeligt til nogle tests og grundlæggende kontrol af overholdelse af specifikationerne, forsøger det ikke at opføre sig som en rigtig enhed.

Vi skal implementere en brugerdefineret adfærd!

Vi har bevaret både enkelhed og fleksibilitet, da vi skabte BLEmulator. Vi ønskede at give udviklerne den nødvendige kontrol over alle aspekter af den oprettede periferi, samtidig med at det krævede så lidt arbejde fra dem som muligt for at skabe en fungerende definition. For at nå dette mål har vi besluttet at oprette standardimplementeringen af alle de metoder, der kan være dit indgangspunkt for brugerdefineret adfærd, og lade dig bestemme, hvad der skal overskrives.

Nu skal vi tilføje noget logik.

Lad forbindelsen tage tid

En rigtig periferi vil sandsynligvis tage noget tid, før den får forbindelse. For at opnå dette behøver du kun at overskrive én metode:

Det er det hele. Nu tager forbindelsen 200 millisekunder!

Afvisning af forbindelse

Et lignende tilfælde. Vi ønsker at beholde forsinkelsen, men returnere en fejl om, at forbindelsen ikke kunne etableres. Du kunne overskrive metoden og selv kaste en SimulatedBleError, men du kan også gøre det:

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

Afbrydelse af forbindelsen initialiseret af periferien

Lad os sige, at du vil kontrollere genforbindelsesprocessen eller simulere, at du kommer uden for rækkevidde. Du kan bede en kollega om at løbe til den anden side af kontoret med et rigtigt periferiudstyr eller tilføje en eller anden debug-knap og i dens onPress gøre:

yourPeripheralInstance.onDisconnect();

(Selv om den første mulighed synes at være mere tilfredsstillende.)

Ændring af RSSI i scanningsoplysningerne

Okay, lad os sige, at vi ønsker at sortere periferier efter deres opfattede signalstyrke, og at vi skal teste det. Vi opretter et par simulerede perifere enheder, lader den ene have statisk RSSI og gør så i den anden:

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

Dermed kan du have et par enheder med variabel RSSI og teste funktionen.

Forhandling af MTU

BLEmulator udfører det meste af logikken selv og begrænser således MTU’en til det understøttede område fra 23 til 512, men hvis du har brug for at begrænse den yderligere, skal du tilsidesætte requestMtu()-metoden:

BLEmulator forhandler automatisk den højeste understøttede MTU på iOS.

Tvinger værdier til det understøttede område

For at begrænse værdier, der accepteres af en karakteristik, skal du oprette en ny klasse, der forlænger SimulatedCharacteristic.

Karakteristikken begrænser nu input til enten 0 eller 1 på den første byte, ignorerer alle yderligere bytes og returnerer en fejl, hvis værdien overstiger det understøttede område. For at understøtte returnering af en fejl skal en karakteristik returnere et svar på skriveoperationen, og derfor skal writableWithoutResponse indstilles til false.

Tænding af sensoren

Vi vil gerne vide, hvornår temperatursensoren er tændt eller slukket.

For at opnå dette opretter vi en ny tjeneste med hardcodede UUID’er:

Den egenskab, vi har defineret, kan ikke overvåges via FlutterBleLib, da den mangler isNotifiable: true, men den kan for nemheds skyld overvåges på BLEmulator-niveau. Dette gør det lettere at kontrollere det overordnede flow, vi simulerer, forenkler strukturen og lader os undgå unødvendige udvidelser af basisklasserne.

Udsendelse af meddelelser

Vi mangler stadig at udsende meddelelser fra karakteristikken IR-temperaturdata. Lad os tage os af det.

_emitTemperature() kaldes i konstruktøren for temperaturtjenesten og kører i en uendelig løkke. I hvert interval, der er angivet ved værdien af egenskaben IR Temperature Period, kontrolleres det, om der er en lytter (isNotifying). Hvis der er en, skrives der data (nuller eller en tilfældig værdi, afhængigt af om sensoren er tændt eller slukket) til karakteristikken IR Temperature Data. SimulatedCharacteristic.write() underretter eventuelle aktive lyttere om den nye værdi.

Avanceret periferi i aktion

Du kan finde et komplet eksempel på en mere avanceret periferi på BLEmulators repository. Hvis du gerne vil prøve det, skal du bare klone repositoriet og køre eksemplet.

Brug i automatiseret testning

En stor tak, her, til min kollega Polidean Paweł Byszewski for hans research af BLEmulators brug i automatiserede tests.

Flutter har en anden eksekveringskontekst for den testede app og selve testen, hvilket betyder, at du ikke bare kan dele en simuleret periferi mellem de to og ændre opførslen fra testen. Det, du kan gøre, er at tilføje en datahåndtering til testdriveren ved hjælp af enableFlutterDriverExtension(handler: DataHandler), videregive de simulerede perifere enheder til main() i din app og videregive strengmeddelelser til håndteringsprogrammet inden for appens eksekveringskontekst.

Det kan koges ned til:Wrapper for app

Din periferi

Din test

Takket være denne mekanisme kan du initialisere periferien, som du vil, og kalde en hvilken som helst adfærd, du har foruddefineret inde i din simulerede enhed.

Se selv

Det bedste ved alt dette er, at du ikke behøver at tro mig! Du kan selv tjekke det ud på GitHub. Brug det, hav det sjovt med det, prøv at bryde det, og lad os vide alt om det!

Vil du også gerne se det på andre platforme? Tag kontakt til os, så skal vi nok få det til at ske! Husk at tjekke vores andre biblioteker, og tøv ikke med at kontakte os, hvis du gerne vil have os til at arbejde på dit projekt.