Bluetooth Low Energy Simulator-A New Hope in IoT Development

Wir leben im XXI Jahrhundert, wo Tiere vielleicht noch nicht mit uns sprechen, aber die Dinge fangen definitiv an, es zu tun. Wir laufen mit unseren Kontrollzentren (auch Smartphones genannt) herum, die unser Geld, unsere Kommunikation, unsere Nachrichten, unsere intelligenten Häuser und jede andere intelligente Lösung, die uns umgibt, verwalten können – kurz gesagt: unser Leben.

Training für ein Rennen? Das können Sie mit dem intelligenten Muskel-Sauerstoff-Monitor Humon tun. Möchten Sie Ihre Schlafqualität verbessern? Besorgen Sie sich ein Smartband, eine Smartwatch oder einen Schlaftracker (die alle Bluetooth Low Energy verwenden und mit Ihrem Telefon kommunizieren). Sie wollen mit dem Fahrrad irgendwohin fahren? Verwenden Sie eine BLE-fähige Navigation, um Ablenkungen wie Benachrichtigungen zu minimieren und trotzdem zu wissen, wie Sie ans Ziel kommen. Vielleicht möchten Sie wissen, wann der Kaffee im Büro fertig ist? Wir haben alles für Sie!

Aber mit dem Aufkommen des Internets der Dinge und der zunehmenden Vernetzung der Welt kommen neue Herausforderungen auf uns Entwickler zu.

Entwicklung einer BLE-fähigen App

Wenn Sie schon einmal an einer mobilen App gearbeitet haben, die sich mit einem Bluetooth Low Energy-Gerät verbindet, wissen Sie, dass dies nicht die einfachste Aufgabe ist. Es gibt viele Bildschirme und interne Zustände in der App, die mit dem Peripheriegerät und seinem Verhalten verbunden sind. Die App wird oft parallel zur Firmware (manchmal sogar zur Hardware (!!!)) des Geräts entwickelt, die Verbindung kann unbeständig sein, die externe Schnittstelle ist instabil, die interne Logik fehlerhaft. Infolgedessen vergeudet das Team beim Testen seiner Funktionen viel Zeit mit Known Issues™.

In der Regel gibt es zwei Möglichkeiten, dieses Problem anzugehen:

  1. Erstens gibt es einen einfachen, zeitaufwändigen Weg: ein physisches Smartphone und ein BLE-Gerät verwenden, sich die Mühe machen, das Gerät anzuschließen und so einzurichten, wie wir es brauchen, und die Testbedingungen nachstellen.
  2. Dann gibt es die harte Tour: Abstrahieren Sie das Gerät, erstellen Sie ein einfaches Mock-up, das die eigentliche BLE-Behandlung verbergen kann. Auf diese Weise können Sie mit einem Android-Emulator/iOS-Simulator arbeiten, was Ihnen später etwas Zeit spart und es Ihnen ermöglicht, automatisierte Tests auf Ihren CIs durchzuführen. Gleichzeitig erhöht es die Wartungskosten und führt ein neues Risiko ein, da die tatsächliche Kommunikation nicht jedes Mal getestet wird, wenn Sie Ihren Code ausführen. Schließlich ist das Bluetooth-Peripheriegerät wahrscheinlich das Herzstück unserer Anwendung, und es darf sich nicht unerwartet abschalten oder seltsam verhalten.

Unsere Freunde von Frontside – einem in Austin ansässigen Beratungsunternehmen für Frontend-Software-Engineering und -Architektur – haben den Bedarf an einer effektiveren Lösung erkannt. Sie haben uns gebeten, eine Open-Source-Lösung für eine zuverlässige BLE-fähige App-Entwicklung zu entwickeln, von der jeder profitieren kann.

Und so kommt…

BLEmulator /pronun.: bleh-mulator/, ein Bluetooth Low Energy Simulator.

BLEmulator ist hier, um Ihnen das Leben zu erleichtern! Er verwaltet Ihren gesamten BLE-bezogenen Code und simuliert das Verhalten eines echten Peripheriegeräts und des Bluetooth-Stacks des Systems. Er ist einfach und flexibel und ermöglicht es Ihnen, sowohl einen einfachen Mock als auch eine vollständige Simulation eines BLE-fähigen Geräts zu erstellen. Und das Beste daran: Es ist Open Source!

Sie können damit das Konzept Ihres Geräts testen, ohne die Kosten für das Hardware-Prototyping aufbringen zu müssen. So kann Ihr mobiles Team vorankommen, ohne auf Firmware oder Prototypen warten zu müssen, nur mit einer Spezifikation. Sie können nur mit einem Android-Emulator oder einem iOS-Simulator arbeiten, was eine größere Mobilität und einfachere Remote-Arbeit ermöglicht und die begrenzte Verfügbarkeit physischer Smartphones vermeidet. Es ermöglicht Ihnen, Ihre Apps in automatisierten Tests zu testen, die von Ihrem CI ausgeführt werden.

Zurzeit ist BLEmulator für Flutter verfügbar und funktioniert nur mit unserer FlutterBleLib.

Wie es funktioniert

Flutter, als Multiplattform-Framework, benötigt native Abhängigkeiten für beide Plattformen. Normalerweise wären diese ein Teil der Bibliothek selbst, aber wir haben hier einen anderen Ansatz gewählt. Polidea hat eine React Native BLE Bibliothek, genannt react-native-ble-plx, eine großartige Arbeit unserer Kollegen. Wir haben uns entschieden, die gesamte native Logik daraus in eine separate Bibliothek zu extrahieren, die als Multiplatform BLE Adapter bekannt ist. Auf diese Weise haben wir einen gemeinsamen Kern geschaffen, der sowohl von react-native-ble-plx als auch von unserem Flutter-Plugin, FlutterBleLib, verwendet wird. Als Nebeneffekt haben wir eine gemeinsame Abstraktion geschaffen, die in der nativen Bridge verwendet wird, die BleAdapter, die ein perfekter Einstiegspunkt für die Simulation ist!

So sieht der Datenfluss von FlutterBleLib aus:

  1. Aufruf einer Methode auf einem der FlutterBleLib-Objekte (BleManager, Peripheral, Service, Characteristic)
  2. Dart-Code sendet einen Methodennamen und seine Parameter an eine native Bridge
  3. Native Bridge empfängt die Daten und deserialisiert sie bei Bedarf
  4. Native Bridge ruft eine entsprechende Methode auf der BleAdapter-Instanz vom Multiplattform-BLE-Adapter auf
  5. BleAdapter ruft die Methode entweder auf RxAndroidBle oder RxBluetoothKit auf, abhängig von der Plattform
  6. RxAndroidBle/RxBluetoothKit ruft eine Systemmethode auf
  7. System gibt eine Antwort an den Vermittler zurück
  8. Vermittler gibt die Antwort an den BleAdapter zurück
  9. BleAdapter antwortet der nativen Brücke
  10. Native bridge mappt die Antwort und sendet sie an Dart
  11. Dart parst die Antwort und gibt sie an den ursprünglichen Aufrufer zurück

Wir hatten unsere Arbeit für uns vorbereitet – die Punkte 4 bis 9 sind die idealen Einstiegspunkte mit einem festgelegten externen Vertrag. Indem wir eine andere Implementierung des BleAdapters einfügen, können wir die Simulation jederzeit einschalten.

Als nächstes mussten wir entscheiden, wo die Simulation stattfinden sollte. Wir haben uns dafür entschieden, die Simulation so weit wie möglich in Dart zu belassen, und zwar aus zwei Hauptgründen:

  1. Eine Definition für beide Plattformen

Es ist wichtig, sowohl die Anzahl der Stellen zu minimieren, an denen man einen Fehler machen kann, als auch den Arbeitsaufwand, der für die Erstellung eines simulierten Peripheriegeräts und dessen Wartung erforderlich ist.

  1. Eigene Sprache für die Plattform

Das hat einige Vorteile. Erstens werden die Entwickler effizienter mit einem bekannten Tool arbeiten, so dass wir die Einführung zusätzlicher Sprachen vermeiden sollten. Zweitens wollten wir die Möglichkeiten des simulierten Peripheriegeräts nicht einschränken. Wenn Sie eine Antwort von einigen HTTP-Servern anfordern möchten (vielleicht mit einer fortschrittlicheren Simulation, auf der die Firmware selbst läuft?), können Sie dies ohne Probleme mit demselben Code tun, den Sie für jede andere HTTP-Kommunikation in Ihrer Anwendung schreiben würden.

Die simulierte Call-Route sieht so aus:

  1. Aufruf einer Methode auf einem der FlutterBleLib-Objekte (BleManager, Peripheral, Service, Characteristic)
  2. Dart-Code sendet den Namen der Methode und ihre Parameter an die native Bridge
  3. Native Bridge empfängt die Daten und deserialisiert sie wenn nötig
  4. Native Bridge ruft die entsprechende Methode auf dem SimulatedAdapter auf

    Changed Teil beginnt jetzt

  5. BleAdapter – in diesem Fall der vom Simulator – leitet den Aufruf an die native Bridge des BLEmulators weiter
  6. BLEmulator führt die Logik entweder selbst aus (wenn sie das Peripheriegerät nicht einbezieht) oder ruft die entsprechende Methode auf dem Peripheriegerät auf, das vom Benutzer
  7. Die Antwort wird an die native Bridge des BLEmulators übergeben
  8. Die native Bridge des BLEmulators übergibt die Antwort an den SimulatedAdapter
  9. SimulatedAdapter antwortet der nativen Bridge

    Zurück zum ursprünglichen Fluss

  10. Native Bridge mappt die Antwort und sendet sie an Dart
  11. Dart parst die Antwort und gibt sie an den ursprünglichen Aufrufer zurück

Auf diese Weise können Sie Ihren gesamten BLE-Behandlungscode verwenden und mit den von FlutterBleLib bereitgestellten Typen arbeiten, egal welches Backend Sie verwenden, sei es das reale System BT Stack oder die Simulation. Das bedeutet auch, dass Sie die Interaktion mit einem Peripheriegerät in automatisierten Tests auf Ihrer CI testen können!

Wie man es verwendet

Wir haben beschrieben, wie es funktioniert und welche Möglichkeiten es bietet, also lassen Sie uns jetzt dazu übergehen, wie man es verwendet.

  1. Fügen Sie die Abhängigkeit zu blemulator in Ihrer pubspec.yml
  2. Erstellen Sie Ihr eigenes simuliertes Peripheriegerät mit den vom Plugin bereitgestellten Klassen SimulatedPeripheral, SimulatedService und SimulatedCharacteristic (ich werde das im nächsten Abschnitt detailliert behandeln)
  3. Fügen Sie das Peripheriegerät mit Blemulator.addPeripheral(SimulatedPeripheral) zum BLEmulator hinzu
  4. Rufen Sie Blemulator.simulate() auf, bevor Sie BleManager.createClient() von der FlutterBleLib aufrufen

Das war’s, nur vier Schritte und Sie sind startklar! Nun, ich gebe zu, dass der komplexeste Schritt irgendwie übersprungen wurde, also lassen Sie uns über den zweiten Punkt sprechen – die Definition des Peripheriegerätes.

Peripheriegerät-Vertrag

Ich werde die folgenden Beispiele auf CC2541 SensorTag von Texas Instruments stützen und mich dabei auf den IR-Temperatursensor konzentrieren.

Wir müssen wissen, wie die UUIDs des Dienstes und seine Eigenschaften aussehen. Wir sind an zwei Stellen in der Dokumentation interessiert.

UUUIDs, die uns interessieren:

  • IR-Temperaturdienst: F000AA00-0451-4000-B000-000000000000Dieser Dienst enthält alle Merkmale, die den Temperatursensor betreffen.
  • IR-Temperaturdaten: F000AA01-0451-4000-B000-000000000000

    Die Temperaturdaten, die gelesen oder überwacht werden können. Das Format der Daten ist ObjectLSB:ObjectMSB:AmbientLSB:AmbientMSB. Während der Überwachung wird in jedem konfigurierbaren Zeitraum eine Meldung ausgegeben.

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

    Ein/Aus-Schalter für den Sensor. Es gibt zwei gültige Werte für dieses Merkmal::

    • 00-Sensor wird in den Ruhezustand versetzt (die IR-Temperaturdaten bestehen aus vier Bytes Nullen)
    • 01-Sensor aktiviert (die IR-Temperaturdaten geben korrekte Messwerte aus)
  • IR-Temperaturperiode: F000AA03-0451-4000-B000-000000000000

    Das Intervall zwischen den Meldungen.Die untere Grenze liegt bei 300 ms, die obere Grenze bei 1000 ms. Der Wert des Merkmals wird mit 10 multipliziert, so dass die unterstützten Werte zwischen 30 und 100 liegen.

Das ist alles, wonach wir gesucht haben, also gehen wir zur Implementierung über!

Einfachstes Peripheriegerät

Die einfachste Simulation akzeptiert jeden Wert und ist bei allen Operationen erfolgreich.

In Dart sieht das so aus:

Kurz und prägnant, sieht fast wie ein JSON aus.

Hier ist, was passiert: wir haben ein Peripheriegerät namens SensorTag erstellt, das eine zur Laufzeit festgelegte ID hat (jede beliebige Zeichenkette ist in Ordnung, aber sie muss unter den dem BLEmulator bekannten Peripheriegeräten eindeutig sein). Während der Peripherie-Scan eingeschaltet ist, wird er alle 800 Millisekunden mit den Standard-Scan-Informationen bekannt gegeben. Er enthält einen Dienst, dessen UUID in den Ankündigungsdaten enthalten ist. Der Dienst enthält 3 Merkmale, genau wie bei einem echten Gerät, und alle sind lesbar. Das erste Merkmal kann nicht beschrieben werden, unterstützt aber Benachrichtigungen; die beiden anderen können nicht überwacht werden, können aber beschrieben werden. Es gibt keine ungültigen Werte für die Merkmale. Das Argument convenienceName wird vom BLEmulator in keiner Weise verwendet, macht die Definition aber leichter lesbar.

IR Temperature Config und IR Temperature Period akzeptieren und setzen jeden Wert, der ihnen übergeben wird. IR Temperature Data unterstützt Benachrichtigungen, sendet aber nie welche, da wir sie nicht definiert haben.

BLEmulator bietet das grundlegende Verhalten von Haus aus und kümmert sich um den glücklichen Pfad für alle Ihre Konstrukte, was die benötigte Menge an Arbeit minimiert. Während es für einige Tests und grundlegende Überprüfungen der Einhaltung von Spezifikationen ausreicht, versucht es nicht, sich wie ein echtes Gerät zu verhalten.

Wir müssen ein benutzerdefiniertes Verhalten implementieren!

Wir haben bei der Entwicklung von BLEmulator sowohl auf Einfachheit als auch auf Flexibilität geachtet. Wir wollten den Entwicklern die nötige Kontrolle über jeden Aspekt des erstellten Peripheriegeräts geben und ihnen gleichzeitig so wenig Arbeit wie möglich abverlangen, um eine funktionierende Definition zu erstellen. Um dieses Ziel zu erreichen, haben wir uns entschlossen, die Standardimplementierung aller Methoden zu erstellen, die der Einstiegspunkt für benutzerdefiniertes Verhalten sein könnten, und Sie entscheiden zu lassen, was Sie überschreiben möchten.

Nun fügen wir etwas Logik hinzu.

Die Verbindung soll Zeit in Anspruch nehmen

Ein echtes Peripheriegerät wird wahrscheinlich einige Zeit brauchen, bevor es verbunden wird. Um dies zu erreichen, müssen Sie nur eine Methode überschreiben:

Das war’s. Jetzt dauert die Verbindung 200 Millisekunden!

Verbindung verweigern

Ein ähnlicher Fall. Wir wollen die Verzögerung beibehalten, aber einen Fehler zurückgeben, dass die Verbindung nicht hergestellt werden konnte. Sie könnten die Methode überschreiben und selbst ein SimulatedBleError auslösen, aber Sie können auch Folgendes tun:

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

Unterbrechung der Verbindung durch das Peripheriegerät initialisiert

Angenommen, Sie wollen den Prozess der Wiederherstellung der Verbindung überprüfen oder simulieren, dass Sie außer Reichweite geraten. Sie können einen Kollegen bitten, mit einem echten Peripheriegerät auf die andere Seite des Büros zu gehen, oder Sie fügen eine Debug-Schaltfläche hinzu und tun in deren onPress:

yourPeripheralInstance.onDisconnect();

(Obwohl die erste Option befriedigender zu sein scheint.)

RSSI in den Scan-Informationen ändern

Angenommen, wir wollen Peripheriegeräte nach ihrer wahrgenommenen Signalstärke sortieren und müssen dies testen. Wir erstellen ein paar simulierte Peripheriegeräte, lassen eines mit statischem RSSI und machen dann im anderen:

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

Auf diese Weise können Sie ein paar Geräte mit variablem RSSI haben und die Funktion testen.

MTU aushandeln

BLEmulator erledigt den größten Teil der Logik selbst, wodurch die MTU auf den unterstützten Bereich von 23 bis 512 begrenzt wird, aber wenn man sie weiter einschränken möchte, sollte man die requestMtu()-Methode außer Kraft setzen:

BLEmulator handelt automatisch die höchste unterstützte MTU auf iOS aus.

Werte in den unterstützten Bereich zwingen

Um die von einem Merkmal akzeptierten Werte zu begrenzen, müssen Sie eine neue Klasse erstellen, die SimulatedCharacteristic erweitert.

Das Merkmal begrenzt nun die Eingabe auf 0 oder 1 im ersten Byte, ignoriert alle weiteren Bytes und gibt einen Fehler zurück, wenn der Wert den unterstützten Bereich überschreitet. Um die Rückgabe eines Fehlers zu unterstützen, muss ein Merkmal eine Antwort auf einen Schreibvorgang zurückgeben, daher wird writableWithoutResponse auf false gesetzt.

Einschalten des Sensors

Wir würden gerne wissen, wann der Temperatursensor ein- oder ausgeschaltet ist.

Um dies zu erreichen, erstellen wir einen neuen Dienst mit fest kodierten UUIDs:

Das von uns definierte Merkmal kann nicht durch die FlutterBleLib überwacht werden, da es fehlt isNotifiable: true, aber es kann, der Einfachheit halber, auf der BLEmulator-Ebene überwacht werden. Das macht es einfacher, den Gesamtfluss, den wir simulieren, zu kontrollieren, vereinfacht die Struktur und lässt uns unnötige Erweiterungen der Basisklassen vermeiden.

Emittieren von Benachrichtigungen

Wir vermissen immer noch das Emittieren von Benachrichtigungen von dem Merkmal IR-Temperaturdaten. Kümmern wir uns darum.

_emitTemperature() wird im Konstruktor des Temperaturdienstes aufgerufen und läuft in einer Endlosschleife. In jedem Intervall, das durch den Wert des Merkmals IR Temperature Period angegeben wird, wird geprüft, ob es einen Hörer gibt (isNotifying). Ist dies der Fall, so werden Daten (Nullen oder ein Zufallswert, je nachdem, ob der Sensor ein- oder ausgeschaltet ist) in das Merkmal IR-Temperaturdaten geschrieben. SimulatedCharacteristic.write() benachrichtigt alle aktiven Listener über den neuen Wert.

Fortgeschrittenes Peripheriegerät in Aktion

Ein vollständiges Beispiel für ein fortgeschrittenes Peripheriegerät finden Sie im Repository des BLEmulators. Wenn Sie es ausprobieren möchten, klonen Sie einfach das Repository und führen Sie das Beispiel aus.

Verwendung in automatisierten Tests

Ein großes Dankeschön an dieser Stelle an meinen Kollegen Paweł Byszewski für seine Nachforschungen über die Verwendung von BLEmulator in automatisierten Tests.

Flutter hat einen unterschiedlichen Ausführungskontext für die getestete Anwendung und den Test selbst, was bedeutet, dass Sie nicht einfach ein simuliertes Peripheriegerät zwischen den beiden austauschen und das Verhalten des Tests ändern können. Sie können dem Testtreiber einen Datenhandler hinzufügen, indem Sie enableFlutterDriverExtension(handler: DataHandler) verwenden, die simulierten Peripheriegeräte an main() Ihrer Anwendung übergeben und String-Nachrichten an den Handler im Ausführungskontext der Anwendung übergeben.

Es läuft darauf hinaus:Wrapper für die App

Ihr Peripheriegerät

Ihr Test

Dank dieses Mechanismus können Sie das Peripheriegerät so initialisieren, wie Sie wollen, und jedes der Verhaltensweisen aufrufen, die Sie in Ihrem simulierten Gerät vordefiniert haben.

Überzeugen Sie sich selbst

Das Beste an all dem ist, dass Sie mir nicht glauben müssen! Probieren Sie es selbst auf GitHub aus. Benutzt es, habt Spaß damit, versucht es zu knacken und lasst uns alles darüber wissen!

Wollt ihr es auch auf anderen Plattformen sehen? Sprechen Sie uns an und wir werden es möglich machen! Schauen Sie sich auch unsere anderen Bibliotheken an, und zögern Sie nicht, uns zu kontaktieren, wenn Sie möchten, dass wir an Ihrem Projekt arbeiten.