Bluetooth Low Energy Simulator-A New Hope in IoT Development

Vi lever på 2000-talet, där djuren kanske fortfarande inte pratar med oss, men saker och ting börjar definitivt göra det. Vi går runt med våra uppdragscentraler (alias smartphones) som kan hantera våra pengar, vår kommunikation, våra nyheter, våra smarta hem och alla andra smarta lösningar som omger oss, kort sagt våra liv.

Tränar du inför ett lopp? Det kan du göra med en smart muskel- och syremätare Humon. Vill du förbättra din sömnkvalitet? Skaffa ett smart armband, en smart klocka eller en sleep-tracker (som alla använder Bluetooth Low Energy och kommunicerar med din telefon). Försöker du ta dig någonstans på cykel? Använd BLE-aktiverad navigering för att minimera distraktioner som meddelanden men ändå veta hur du ska ta dig dit du vill. Du kanske vill veta när kaffet på kontoret är klart? Vi har allt för dig!

Men med tillkomsten av sakernas internet och den ökande sammankopplingen av världen kom nya utmaningar för oss utvecklare.

Utveckling av en BLE-aktiverad app

Om du någonsin har arbetat med en mobilapp som ansluter till en Bluetooth Low Energy-enhet vet du att det inte är den enklaste uppgiften. Det finns många skärmar och interna tillstånd i appen som är kopplade till periferin och dess beteende. Appen skapas ofta tillsammans med enhetens firmware (ibland även hårdvara (!!!)), anslutningen kan vara nyckfull, det externa gränssnittet instabilt, den interna logiken buggig. Som ett resultat slösar teamet mycket tid på kända problem™ när de testar sina funktioner.

I allmänhet finns det två sätt att närma sig detta:

  1. För det första finns det ett enkelt, tidskrävande sätt: Använd en fysisk smartphone och BLE-enhet, gå igenom allt krångel för att ansluta och ställa in enheten i det tillstånd som vi behöver ha den i och återskapa testförhållandena.
  2. Då finns det svåra sättet: abstrahera enheten, skapa en enkel mock-up som kan dölja den faktiska BLE-hanteringen. På så sätt kan du arbeta på en Android-emulator/iOS-simulator, vilket sparar tid senare och låter dig köra automatiserade tester på dina CIs. Samtidigt ökar underhållskostnaden och introducerar en ny risk genom att den faktiska kommunikationen inte testas varje gång du kör din kod. När allt kommer omkring är den där Bluetooth-periferin förmodligen kärnan i vår applikation och den får inte kopplas bort oväntat eller bete sig konstigt.

Våra vänner på Frontside-Austin-baserad konsultfirma för frontend-mjukvaruteknik och -arkitektur har insett behovet av en mer effektiv lösning. De bad oss att utveckla en lösning med öppen källkod för en tillförlitlig BLE-aktiverad apputveckling som alla kan dra nytta av.

Och så kommer in…

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

BLEmulator är här för att göra ditt liv enklare! Den hanterar all din BLE-relaterade produktionskod och simulerar beteendet hos en riktig kringutrustning och systemets Bluetooth-stack. Den är enkel och flexibel och låter dig skapa både en grundläggande mock och en fullständig simulering av en BLE-aktiverad enhet. Och det bästa är att den har öppen källkod!

Den låter dig testa konceptet bakom din enhet utan kostnaden för hårdvaruprototyper. Det låter ditt mobila team gå vidare utan att vänta på firmware eller prototyper, bara med en specifikation. Du kan arbeta med endast en Android-emulator eller iOS-simulator, vilket gör det möjligt att öka mobiliteten, underlätta distansarbete och undvika den begränsade tillgången till fysiska smartphones. Det låter dig testa dina appar i automatiserade tester som körs av din CI.

För närvarande är BLEmulator tillgänglig för Flutter och fungerar endast med vår FlutterBleLib.

Hur det fungerar

Flutter, som ett ramverk för flera plattformar, behöver nativa beroenden för båda plattformarna. Normalt sett skulle dessa vara en del av själva biblioteket, men vi använde ett annat tillvägagångssätt här. Polidea har ett React Native BLE-bibliotek som heter react-native-ble-plx, ett fantastiskt arbete av våra kollegor. Vi har bestämt oss för att extrahera all native logik från det till ett separat bibliotek, som kallas Multiplatform BLE Adapter. På så sätt har vi skapat en gemensam kärna som används av både react-native-ble-plx och vårt Flutter-plugin, FlutterBleLib. Som en bieffekt har vi skapat en gemensam abstraktion som används i den infödda bryggan, BleAdapter, vilket är en perfekt ingångspunkt för simulering!

Det här är hur FlutterBleLibs dataflöde ser ut:

  1. Kalla en metod på ett av FlutterBleLib-objekten (BleManager, Peripheral, Service, Characteristic)
  2. Dartkoden skickar ett metodnamn och dess parametrar över till en native bridge
  3. Native bridge tar emot data och deserialiserar dem vid behov
  4. Native bridge anropar en lämplig metod på BleAdapter-instansen från BLE-adaptern för flera plattformar
  5. BleAdapter anropar metoden på antingen RxAndroidBle eller RxBluetoothKit, beroende på plattformen
  6. RxAndroidBle/RxBluetoothKit anropar en systemmetod
  7. Systemet returnerar ett svar till förmedlaren
  8. Medlaren returnerar svaret till BleAdapter
  9. BleAdapteren svarar till den inhemska bryggan
  10. Native bridge mappar svaret och skickar det till Dart
  11. Dart analyserar svaret och returnerar det till den ursprungliga anroparen

Vi hade vårt arbete utformat för oss – punkterna 4 till 9 är de idealiska ingångspunkterna med ett fastställt externt kontrakt. Genom att injicera en annan implementering av BleAdapter kan man slå på simuleringen när som helst när vi vill.

Nästan var vi tvungna att bestämma var simuleringen skulle äga rum. Vi valde att behålla så mycket av simuleringen som möjligt i Dart, av två huvudskäl:

  1. En definition för båda plattformarna

Det är viktigt att minimera både antalet ställen där det är möjligt att göra ett misstag och den arbetsinsats som krävs för att skapa en simulerad periferi och underhålla den.

  1. Språk som är inhemskt för plattformen

Det här har ett par fördelar. För det första kommer utvecklarna att arbeta effektivare med ett känt verktyg, så vi bör undvika att införa ytterligare språk. För det andra ville vi inte begränsa saker som är möjliga att göra på den simulerade periferin. Om du vill begära ett svar från några HTTP-servrar (kanske med en mer avancerad simulering, som kör själva den fasta programvaran?) kan du göra det utan problem med samma kod som du skulle skriva för all annan HTTP-kommunikation i din app.

Den simulerade anropsrutten ser ut så här:

  1. Kalla en metod på ett av FlutterBleLib-objekten (BleManager, Peripheral, Service, Characteristic)
  2. Dartkoden skickar metodens namn och dess parametrar över till den infödda bryggan
  3. Den infödda bryggan tar emot data och deserialiserar dem vid behov
  4. Den infödda bryggan anropar den lämpliga metoden på SimulatedAdapter

    Changed delen börjar nu

  5. BleAdapter – i detta fall den från simulatorn – vidarebefordrar anropet till BLEmulatorns infödda brygga
  6. BleAdapter gör antingen logiken själv (om den inte involverar periferin) eller anropar lämplig metod på den levererade periferin. av användaren
  7. Svaret skickas till BLEmulatorns native bridge
  8. BLEmulatorns native bridge skickar svaret till SimulatedAdapter
  9. SimulatedAdapter svarar till native bridge

    Tillbaka till det ursprungliga flödet

  10. Native bridge mappar svaret och skickar det till Dart
  11. Dart analyserar svaret och returnerar det till den ursprungliga anroparen

På så sätt använder du all din BLE-hanteringskod och arbetar med de typer som tillhandahålls av FlutterBleLib oavsett vilken backend du använder, oavsett om det är det verkliga systemet BT stack eller simuleringen. Detta innebär också att du kan testa interaktionen med en periferi i automatiserade tester på ditt CI!

Så här använder du det

Vi har täckt hur det fungerar och vilka möjligheter det ger, så låt oss nu hoppa in på hur du använder det.

  1. Lägg till beroende till blemulator i din pubspec.yml
  2. Skapa din egen simulerade periferi med hjälp av klasser som tillhandahålls av insticksmodulen SimulatedPeripheral, SimulatedService och SimulatedCharacteristic (jag kommer att täcka det i detalj i nästa avsnitt)
  3. Lägg till periferin till BLEmulatorn med hjälp av Blemulator.addPeripheral(SimulatedPeripheral)
  4. Kalla Blemulator.simulate() innan du anropar BleManager.createClient() från FlutterBleLib

Det är allt, bara fyra steg och du är igång! Nåväl, jag erkänner att det mest komplexa steget liksom har hoppats över, så låt oss prata om den andra punkten – att definiera periferin.

Periferikontrakt

Jag kommer att basera följande exempel på CC2541 SensorTag från Texas Instruments, med fokus på IR-temperatursensorn.

Vi måste veta hur UUID:erna för tjänsten och dess egenskaper ser ut. Vi är intresserade av två ställen i dokumentationen.

UUUIDs som vi är intresserade av:

  • IR-temperaturtjänst: F000AA00-0451-4000-B000-000000000000Denna tjänst innehåller alla egenskaper för temperatursensorn.
  • IR-temperaturdata: F000AA01-0451-4000-B000-000000000000

    Temperaturdata som kan läsas eller övervakas. Dataformatet är ObjectLSB:ObjectMSB:AmbientLSB:AmbientMSB. Den avger ett meddelande varje konfigurerbar period medan den övervakas.

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

    Varning/avstängning av sensorn. Det finns två giltiga värden för denna egenskap::

    • 00-sensor som läggs i vila (IR-temperaturdata kommer att vara fyra byte med nollor)
    • 01-sensor aktiverad (IR-temperaturdata kommer att avge korrekta avläsningar)
  • IR-temperaturperiod: F000AA03-0451-4000-B000-000000000000

    Intervallet mellan meddelanden.Den nedre gränsen är 300 ms, den övre gränsen är 1000 ms. Värdet för egenskapen multipliceras med 10, så de värden som stöds är mellan 30 och 100.

Det var allt vi letade efter, så låt oss gå till implementeringen!

Simpel periferi

Den enklaste simuleringen kommer att acceptera vilket värde som helst och lyckas med alla operationer.

I Dart ser det ut så här:

Kort och koncist, ser nästan ut som en JSON.

Här är vad som händer: Vi har skapat en kringutrustning som heter SensorTag och som har ett körtidsspecificerat ID (vilken sträng som helst går bra, men det måste vara unikt bland kringutrustning som är känd för BLEmulator). Medan periferisk skanning är aktiverad kommer den att annonsera med hjälp av standardskanningsinformation var 800 millisekund. Den innehåller en tjänst vars UUID finns i annonseringsdata. Tjänsten innehåller tre egenskaper, precis som på en riktig enhet, och alla är läsbara. Den första av egenskaperna kan inte skrivas till, men stöder meddelanden; de andra två kan inte övervakas men kan skrivas till. Det finns inga ogiltiga värden för egenskaperna. Argumentet convenienceName används inte på något sätt av BLEmulatorn, men gör definitionen lättare att läsa.

IR Temperature Config och IR Temperature Period accepterar och ställer in alla värden som överförs till dem. IR Temperature Data characteristic stöder meddelanden, men skickar aldrig några, eftersom vi inte har definierat dem på något sätt.

BLEmulator tillhandahåller det grundläggande beteendet direkt ur lådan och tar hand om den lyckliga vägen för alla dina konstruktioner, vilket minimerar den mängd arbete som behövs. Även om det kan räcka för vissa tester och grundläggande kontroller av att specifikationen följs, försöker den inte bete sig som en riktig enhet.

Vi måste implementera ett anpassat beteende!

Vi behöll både enkelhet och flexibilitet när vi skapade BLEmulator. Vi ville ge utvecklarna den nödvändiga kontrollen över varje aspekt av den skapade periferin, samtidigt som vi krävde så lite arbete från dem som möjligt för att skapa en fungerande definition. För att uppnå detta mål har vi beslutat att skapa standardimplementationen av alla metoder som kan vara din ingångspunkt för anpassat beteende och låta dig bestämma vad som ska åsidosättas.

Nu ska vi lägga till lite logik.

Gör att anslutningen tar tid

En riktig periferi kommer förmodligen att ta en viss tid innan den blir ansluten. För att uppnå detta behöver du bara åsidosätta en metod:

Det är allt. Nu tar anslutningen 200 millisekunder!

Förnekande av anslutning

Ett liknande fall. Vi vill behålla fördröjningen, men returnera ett felmeddelande om att anslutningen inte kunde upprättas. Du kan åsidosätta metoden och kasta en SimulatedBleError själv, men du kan också göra:

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

Avbrytande av anslutning initierad av periferin

Vi kan säga att du vill kontrollera återanslutningsprocessen eller simulera att du hamnar utanför räckvidd. Du kan be en kollega att springa till andra sidan kontoret med en riktig kringutrustning, eller lägga till en felsökningsknapp och i dess onPress göra:

yourPeripheralInstance.onDisconnect();

(Även om det första alternativet verkar vara mer tillfredsställande.)

Modifiering av RSSI i skanningsinformationen

Okej, låt oss säga att vi vill sortera kringutrustningar efter deras uppfattade signalstyrka och att vi behöver testa det. Vi skapar några simulerade kringutrustning, lämnar en med statisk RSSI och i den andra gör vi:

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

Det här sättet kan du ha ett par enheter med varierande RSSI och testa funktionen.

Förhandla MTU

BLEmulator gör det mesta av logiken själv och begränsar därmed MTU till det stödda intervallet 23-512, men om du behöver begränsa det ytterligare bör du åsidosätta requestMtu()-metoden:

BLEmulator förhandlar automatiskt om den högsta MTU som stöds på iOS.

Tvinga värden till det stödda intervallet

För att begränsa värden som accepteras av en egenskap måste du skapa en ny klass som utökar SimulatedCharacteristic.

Kännetecknet begränsar nu inmatningen till antingen 0 eller 1 på den första byte, ignorerar alla ytterligare byte och returnerar ett fel om värdet överskrider det stödda intervallet. För att stödja att returnera ett fel måste en egenskap returnera ett svar på skrivoperationen, därför sätts writableWithoutResponse till false.

Slå på sensorn

Vi skulle vilja veta när temperatursensorn är påslagen eller avstängd.

För att uppnå detta skapar vi en ny tjänst med hårdkodade UUID:

Den egenskap som vi har definierat kan inte övervakas via FlutterBleLib, eftersom den saknar isNotifiable: true, men den kan, för din bekvämlighet, övervakas på BLEmulatornivå. Detta gör det lättare att kontrollera det övergripande flödet vi simulerar, förenklar strukturen och låter oss undvika onödiga utvidgningar av basklasserna.

Utsändning av meddelanden

Vi saknar fortfarande utsändning av meddelanden från egenskapen IR Temperature Data. Låt oss ta hand om det.

_emitTemperature() anropas i konstruktören för temperaturtjänsten och körs i en oändlig loop. Varje intervall, som specificeras av värdet på egenskapen IR Temperature Period, kontrollerar om det finns en lyssnare (isNotifying). Om det finns en skriver den data (nollor eller ett slumpmässigt värde, beroende på om sensorn är på eller av) till egenskapen IR Temperature Data. SimulatedCharacteristic.write() meddelar eventuella aktiva lyssnare om det nya värdet.

Avancerad periferi i aktion

Du kan hitta ett komplett exempel på en mer avancerad periferi i BLEmulators arkiv. Om du vill prova det är det bara att klona repositoriet och köra exemplet.

Användning i automatiserad testning

Ett stort tack, här, till min Polideankollega Paweł Byszewski för hans forskning om BLEmulators användning i automatiserade tester.

Flutter har en annan exekveringskontext för den testade appen och testet självt, vilket innebär att du inte bara kan dela en simulerad periferi mellan de två och modifiera beteendet från testet. Vad du kan göra är att lägga till en datahanterare till testdrivrutinen med hjälp av enableFlutterDriverExtension(handler: DataHandler), skicka den simulerade kringutrustningen till main() i din app och skicka strängmeddelanden till hanteraren i appens exekveringskontext.

Det kokar ner till:Wrapper för app

Din kringutrustning

Ditt test

Tack vare den här mekanismen kan du initialisera kringutrustningen hur som helst och anropa vilket beteende som helst av de beteenden som du har fördefinierat inne i din simulerade enhet.

Se själv

Det bästa med allt det här är att du inte behöver tro mig! Kolla själv på GitHub. Använd den, ha kul med den, försök att bryta den och låt oss veta allt om det!

Vill du se den på andra plattformar också? Hör av dig till oss så ser vi till att det blir verklighet! Se till att kolla in våra andra bibliotek, och tveka inte att kontakta oss om du vill att vi ska arbeta med ditt projekt.