Bluetooth Low Energy Simulator-A New Hope in IoT Development

We leven in de XXI eeuw, waar dieren misschien nog steeds niet met ons praten, maar dingen beginnen dat zeker wel te doen. We lopen rond met onze missie controle centra (aka smartphones) die ons geld, onze communicatie, ons nieuws, onze slimme huizen en elke andere slimme oplossing die ons omringt, in het kort-ons leven kunnen afhandelen.

Trainen voor een race? Dat kan met een slimme spier-zuurstofmonitor Humon. Wil je je slaapkwaliteit verbeteren? Koop een smartband, smartwatch of een slaap-tracker (die allemaal Bluetooth Low Energy gebruiken en communiceren met je telefoon). Wil je op de fiets ergens heen? Gebruik navigatie met BLE om afleidingen zoals meldingen tot een minimum te beperken en toch te weten hoe u komt waar u wilt. Misschien wil je weten wanneer de koffie op kantoor klaar is? We got you covered, man!

Maar met de komst van het Internet der Dingen en de toenemende onderlinge verbondenheid van de wereld kwamen er nieuwe uitdagingen voor ons, ontwikkelaars.

Het ontwikkelen van een BLE-enabled app

Als je ooit aan een mobiele app hebt gewerkt die verbinding maakt met een Bluetooth Low Energy-apparaat, weet je dat het niet de gemakkelijkste taak is. Er zijn veel schermen en interne toestanden in de app die verbonden zijn met het randapparaat en zijn gedrag. De app wordt vaak gemaakt naast de firmware (soms zelfs hardware (!!)) van het apparaat, de verbinding kan wispelturig zijn, de externe interface onstabiel, de interne logica buggy. Als gevolg daarvan verspilt het team veel tijd aan Known Issues™ bij het testen van hun features.

In het algemeen zijn er twee manieren om dit te benaderen:

  1. Eerst is er een makkelijke, tijdrovende manier: gebruik een fysieke smartphone en BLE-apparaat, ga door al het gedoe om het apparaat aan te sluiten en in te stellen in de staat waarin we het nodig hebben en recreëer de testomstandigheden.
  2. Dan is er nog de moeilijke manier: abstraheer het apparaat, maak een eenvoudige mock-up die de eigenlijke BLE afhandeling kan verbergen. Dit laat je werken op een Android emulator/iOS simulator, bespaart je later wat tijd en laat je geautomatiseerde tests uitvoeren op je CI’s. Tegelijkertijd verhoogt het de onderhoudskosten en introduceert het een nieuw risico door het niet testen van de eigenlijke communicatie elke keer dat je je code uitvoert. Per slot van rekening is dat Bluetooth-randapparaat waarschijnlijk het hart van onze toepassing en het mag niet onverwacht loskoppelen of zich vreemd gedragen.

Onze vrienden bij Frontside-Austin-Gebaseerde frontend software engineering en architectuur consultancy- hebben de behoefte aan een effectievere oplossing onderkend. Zij vroegen ons om een open source oplossing te ontwikkelen voor een betrouwbare BLE-enabled app ontwikkeling waar iedereen van kan profiteren.

En zo komt het…

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

BLEmulator is hier om uw leven gemakkelijker te maken! Het behandelt al uw productie BLE-gerelateerde code en simuleert het gedrag van een echt randapparaat en de Bluetooth-stack van een systeem. Het is eenvoudig en flexibel, en laat je zowel de basis mock als een volledige simulatie van een BLE-compatibel apparaat maken. En het beste is dat het open source is!

Hiermee kunt u het concept achter uw apparaat testen zonder de kosten van hardware prototyping. Het laat uw mobiele team verder gaan zonder te wachten op firmware of prototypes, alleen met een specificatie. Het laat je werken met alleen een Android emulator of iOS simulator, waardoor je mobieler bent, makkelijker op afstand kunt werken en de beperkte beschikbaarheid van fysieke smartphones kunt vermijden. Het laat je je apps testen in geautomatiseerde tests die door je CI worden uitgevoerd.

Huidig is BLEmulator beschikbaar voor Flutter en werkt alleen met onze FlutterBleLib.

Hoe het werkt

Flutter, als een multiplatform framework, heeft native afhankelijkheden nodig voor beide platforms. Normaal gesproken zouden die een onderdeel van de bibliotheek zelf zijn, maar we hebben hier een andere aanpak gebruikt. Polidea heeft een React Native BLE library, genaamd react-native-ble-plx, een geweldig werk van onze collega’s. We hebben besloten om alle native logica daaruit te halen en in een aparte bibliotheek onder te brengen, genaamd Multiplatform BLE Adapter. Op deze manier hebben we een gemeenschappelijke kern gecreëerd die wordt gebruikt door zowel react-native-ble-plx als onze Flutter plugin, FlutterBleLib. Als neveneffect hebben we een gemeenschappelijke abstractie gecreëerd die wordt gebruikt in de native bridge, de BleAdapter, wat een perfect startpunt is voor simulatie!

Zo ziet de datastroom van FlutterBleLib er uit:

  1. Aanroep een methode op een van de FlutterBleLib objecten (BleManager, Peripheral, Service, Characteristic)
  2. Dart code stuurt een methode naam en de parameters over naar een native bridge
  3. Native bridge ontvangt de gegevens en deserialiseert ze indien nodig
  4. Native bridge roept een geschikte methode op de BleAdapter instantie van de Multiplatform BLE Adapter
  5. BleAdapter roept de methode aan op ofwel RxAndroidBle of RxBluetoothKit, afhankelijk van het platform
  6. RxAndroidBle/RxBluetoothKit roept een systeemmethode op
  7. Systeem stuurt een antwoord terug naar de intermediair
  8. Intermediair stuurt het antwoord terug naar de BleAdapter
  9. BleAdapter stuurt een antwoord naar de native bridge
  10. Native bridge brengt het antwoord in kaart en stuurt het door naar Dart
  11. Dart parseert het antwoord en stuurt het terug naar de oorspronkelijke aanroeper

We hadden ons werk voor ons klaar liggen-punten 4 tot 9 zijn de ideale ingangspunten met een vast extern contract. Door een andere implementatie van de BleAdapter te injecteren kunnen we de simulatie aanzetten wanneer we maar willen.

Nog moesten we beslissen waar de simulatie zou moeten plaatsvinden. We hebben ervoor gekozen om zoveel mogelijk van de simulatie in Dart te houden, om twee belangrijke redenen:

  1. Een definitie voor beide platformen

Het is belangrijk om zowel het aantal plaatsen waar het mogelijk is om een fout te maken als de hoeveelheid werk die nodig is om een gesimuleerd randapparaat te maken en te onderhouden tot een minimum te beperken.

  1. Taal die eigen is aan het platform

Dit heeft een paar voordelen. Ten eerste zullen ontwikkelaars efficiënter werken met een bekend hulpmiddel, zodat we de introductie van extra talen kunnen vermijden. Ten tweede wilden we geen beperkingen opleggen aan dingen die mogelijk zijn op de gesimuleerde randapparatuur. Als je een antwoord wilt opvragen van een HTTP server (misschien met een meer geavanceerde simulatie, die de firmware zelf draait?), dan kun je dat zonder problemen doen met dezelfde code die je zou schrijven voor elke andere HTTP communicatie in je app.

De gesimuleerde call-route ziet er als volgt uit:

  1. Roep een methode aan op een van de FlutterBleLib objecten (BleManager, Peripheral, Service, Characteristic)
  2. Dart code stuurt de naam van de methode en de parameters naar de native bridge
  3. Native bridge ontvangt de gegevens en deserialiseert ze indien nodig
  4. Native bridge roept de juiste methode aan op de SimulatedAdapter

    Changed deel begint nu

  5. BleAdapter – in dit geval die van de simulator – stuurt de oproep door naar de native bridge van de BLEmulator
  6. BLEmulator doet de logica zelf (als deze geen betrekking heeft op het randapparaat) of roept de juiste methode aan op het randapparaat dat door de gebruiker
  7. Respons wordt doorgegeven aan de native bridge van de BLEmulator
  8. BLEmulator’s native bridge geeft het antwoord door aan de SimulatedAdapter
  9. SimulatedAdapter antwoordt aan de native bridge

    Terug naar oorspronkelijke flow

  10. Native bridge mapt het antwoord en stuurt het door naar Dart
  11. Dart parseert het antwoord en stuurt het terug naar de oorspronkelijke aanroeper

Op deze manier gebruikt u al uw BLE afhandelingscode en werkt u met typen die door FlutterBleLib worden geleverd, ongeacht welke backend u gebruikt, of dat nu het echte systeem BT stack of de simulatie is. Dit betekent ook dat u de interactie met een randapparaat kunt testen in geautomatiseerde tests op uw CI!

Hoe het te gebruiken

We hebben behandeld hoe het werkt en welke mogelijkheden het biedt, dus laten we nu springen in hoe het te gebruiken.

  1. Voeg de afhankelijkheid van blemulator in uw pubspec.yml
  2. Maak uw eigen gesimuleerde randapparaat met behulp van de door de plugin verschafte klassen SimulatedPeripheral, SimulatedService en SimulatedCharacteristic (ik zal het in detail behandelen in de volgende sectie)
  3. Voeg het randapparaat toe aan de BLEmulator met behulp van Blemulator.addPeripheral(SimulatedPeripheral)
  4. Roep Blemulator.simulate() aan voordat je BleManager.createClient() aanroept vanuit FlutterBleLib

Dat is het, slechts vier stappen en je bent up and running! Nou, ik geef toe dat de meest complexe stap een beetje is overgeslagen, dus laten we het hebben over het tweede punt-definiëren van het periferie.

Periferie contract

Ik zal de volgende voorbeelden baseren op CC2541 SensorTag van Texas Instruments, gericht op IR temperatuur sensor.

We moeten weten hoe de UUID’s van de service en zijn kenmerken eruit zien. We zijn geïnteresseerd in twee plaatsen in de documentatie.

UUID’s waarin we geïnteresseerd zijn:

  • IR-temperatuurservice: F000AA00-0451-4000-B000-000000000000Deze service bevat alle kenmerken van de temperatuursensor.
  • IR-temperatuurgegevens: F000AA01-0451-4000-B000-000000000000

    De temperatuurgegevens die kunnen worden gelezen of bewaakt. Het formaat van de gegevens is ObjectLSB:ObjectMSB:AmbientLSB:AmbientMSB. Het zendt een melding uit elke configureerbare periode terwijl het wordt bewaakt.

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

    Aan/uit-schakelaar voor de sensor. Er zijn twee geldige waarden voor deze eigenschap::

    • 00-sensor in slaapstand (IR-temperatuurgegevens zullen vier bytes nullen zijn)
    • 01-sensor ingeschakeld (IR-temperatuurgegevens zullen correcte lezingen uitzenden)
  • IR-temperatuurperiode: F000AA03-0451-4000-B000-000000000000

    Het interval tussen de meldingen. De ondergrens is 300 ms, de bovengrens is 1000 ms. De waarde van de karakteristiek wordt vermenigvuldigd met 10, dus de ondersteunde waarden liggen tussen 30 en 100.

Dat is alles wat we zochten, dus laten we naar de implementatie gaan!

Eenvoudigste randapparaat

De eenvoudigste simulatie accepteert elke waarde en slaagt in alle bewerkingen.

In Dart ziet het er zo uit:

Kort en bondig, lijkt bijna op een JSON.

Hier gebeurt het volgende: we hebben een randapparaat gemaakt met de naam SensorTag dat een runtime gespecificeerd ID heeft (elke string is prima, maar het moet uniek zijn onder randapparaten die bekend zijn bij BLEmulator). Als de scan van het randapparaat is ingeschakeld, zal het elke 800 milliseconden reclame maken met standaard scaninformatie. Het bevat een service, waarvan de UUID is opgenomen in de advertentie gegevens. De service bevat 3 karakteristieken, net als op een echt apparaat, en ze zijn allemaal leesbaar. De eerste van de karakteristieken kan niet worden beschreven, maar ondersteunt notificaties; de andere twee kunnen niet worden gemonitord, maar er kan wel naar worden geschreven. Er zijn geen ongeldige waarden voor de kenmerken. Het argument convenienceName wordt op geen enkele manier gebruikt door de BLEmulator, maar maakt de definitie gemakkelijker leesbaar.

IR Temperatuur Config en IR Temperatuur Periode accepteren en stellen elke waarde in die er aan wordt doorgegeven. IR Temperatuur Data karakteristiek ondersteunt meldingen, maar stuurt er nooit daadwerkelijk een, omdat we ze op geen enkele manier hebben gedefinieerd.

BLEmulator levert het basis gedrag uit de doos, het verzorgen van de happy path voor al uw constructs, het minimaliseren van de hoeveelheid werk die nodig is. Hoewel het kan volstaan voor een aantal tests en basis controles op de naleving van de specificatie, probeert het zich niet te gedragen als een echt apparaat.

We moeten een aangepast gedrag implementeren!

We hebben zowel eenvoud als flexibiliteit behouden bij het maken van BLEmulator. We wilden ontwikkelaars de nodige controle geven over elk aspect van het gemaakte randapparaat, maar tegelijkertijd zo weinig mogelijk werk van hen vragen om een werkende definitie te maken. Om dit doel te bereiken, hebben we besloten om een standaard implementatie te maken van alle methoden die een ingang kunnen zijn voor aangepast gedrag en u te laten beslissen wat u wilt overschrijven.

Nu, laten we wat logica toevoegen.

Laat verbinding tijd kosten

Een echt randapparaat zal waarschijnlijk enige tijd nodig hebben voordat het verbinding krijgt. Om dit te bereiken, hoef je maar één methode te overriden:

Dat is het. Nu duurt de verbinding 200 milliseconden!

Verbinding weigeren

Een soortgelijk geval. We willen de vertraging behouden, maar geven een foutmelding dat de verbinding niet tot stand kon worden gebracht. U zou de methode kunnen overriden en zelf een SimulatedBleError kunnen gooien, maar u kunt ook:

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

Verbinding verbreken geïnitialiseerd door het randapparaat

Stel dat u het herverbindingsproces wilt controleren of wilt simuleren dat u buiten bereik komt. U kunt een collega vragen om naar de andere kant van het kantoor te rennen met een echt randapparaat, of een debug-knop toevoegen en in zijn onPress doen:

yourPeripheralInstance.onDisconnect();

(Hoewel de eerste optie meer bevredigend lijkt.)

RSSI wijzigen in de scan-informatie

Al goed, laten we zeggen dat we randapparaten willen sorteren op hun waargenomen signaalsterkte en dat we dit moeten testen. We maken een paar gesimuleerde randapparaten, laten er een met statische RSSI en dan in de andere doen:

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

Op deze manier kun je een paar apparaten met variabele RSSI hebben en de functie testen.

Onderhandelen over MTU

BLEmulator doet het grootste deel van de logica zelf, waardoor de MTU wordt beperkt tot het ondersteunde bereik van 23 tot 512, maar als u het verder wilt beperken, moet u de methode requestMtu() overschrijven:

BLEmulator zal automatisch onderhandelen over de hoogste ondersteunde MTU op iOS.

Waarden dwingen tot ondersteund bereik

Om waarden te beperken die door een eigenschap worden geaccepteerd, moet u een nieuwe klasse maken die SimulatedCharacteristic uitbreidt.

De eigenschap beperkt nu de invoer tot ofwel 0 of 1 op de eerste byte, negeert eventuele extra bytes, en retourneert een fout als de waarde het ondersteunde bereik overschrijdt. Om het retourneren van een fout te ondersteunen, moet een karakteristiek een antwoord op de schrijfoperatie retourneren, vandaar dat writableWithoutResponse op false wordt gezet.

De sensor aanzetten

We willen graag weten wanneer de temperatuursensor aan of uit wordt gezet.

Om dit te bereiken, maken we een nieuwe service met hardcoded UUIDs:

De karakteristiek die we hebben gedefinieerd kan niet worden bewaakt via FlutterBleLib, omdat het isNotifiable: true mist, maar het kan, voor uw gemak, worden bewaakt op het niveau van de BLEmulator. Dit maakt het gemakkelijker om de totale stroom die we simuleren te controleren, vereenvoudigt de structuur en laat ons onnodige uitbreidingen van de basisklassen vermijden.

Emitting notifications

We missen nog steeds de emitting notifications van de IR Temperature Data karakteristiek. Laten we daar voor zorgen.

_emitTemperature() wordt aangeroepen in de constructor van de Temperatuursdienst en loopt in een oneindige lus. Elk interval, gespecificeerd door de waarde van het kenmerk IR Temperatuurperiode, wordt gecontroleerd of er een luisteraar is (isNotifying). Als er een is, dan schrijft hij data (nullen of een willekeurige waarde, afhankelijk van of de sensor aan of uit staat) naar de IR Temperatuur Data karakteristiek. SimulatedCharacteristic.write() brengt alle actieve luisteraars op de hoogte van de nieuwe waarde.

Geavanceerd randapparaat in actie

U kunt een compleet voorbeeld van een geavanceerder randapparaat vinden op de BLEmulator’s repository. Als u het eens wilt proberen, kloon dan de repository en voer het voorbeeld uit.

Gebruik in geautomatiseerd testen

Hiervoor hartelijk dank aan mijn mede-Polidean Paweł Byszewski voor zijn onderzoek naar het gebruik van de BLEmulator in geautomatiseerde testen.

Flutter heeft een verschillende uitvoeringscontext voor de geteste app en de test zelf, wat betekent dat je niet zomaar een gesimuleerd randapparaat tussen de twee kunt delen en het gedrag van de test kunt wijzigen. Wat u kunt doen is een data handler toevoegen aan het teststuurprogramma door enableFlutterDriverExtension(handler: DataHandler) te gebruiken, de gesimuleerde randapparatuur door te geven aan de main() van uw app en string-berichten door te geven aan de handler binnen de uitvoeringscontext van de app.

Het komt neer op:Wrapper voor app

Uw randapparaat

Uw test

Dankzij dit mechanisme kunt u het randapparaat initialiseren zoals u wilt en elk gedrag aanroepen dat u vooraf hebt gedefinieerd binnen uw gesimuleerde apparaat.

Zie het zelf

Het beste van dit alles is dat u mij niet hoeft te geloven! Bekijk het zelf op GitHub. Gebruik het, heb er plezier mee, probeer het te kraken, en laat ons er alles over weten!

Wilt u het ook op andere platforms zien? Neem contact met ons op en we zorgen dat het gebeurt! Zorg ervoor dat u onze andere bibliotheken bekijkt, en aarzel niet om contact met ons op te nemen als u wilt dat wij aan uw project werken.