Simulateur Bluetooth Low Energy-Un nouvel espoir pour le développement de l’IoT

Nous vivons au XXIe siècle, où les animaux ne nous parlent peut-être pas encore, mais les choses commencent définitivement à le faire. Nous nous promenons avec nos centres de contrôle de mission (alias smartphones) qui peuvent gérer notre argent, notre communication, nos nouvelles, nos maisons intelligentes et toutes les autres solutions intelligentes qui nous entourent, bref, nos vies.

Vous vous entraînez pour une course ? Vous pouvez le faire avec un moniteur intelligent muscle-oxygène Humon. Vous voulez améliorer la qualité de votre sommeil ? Procurez-vous un bracelet intelligent, une smartwatch ou un traceur de sommeil (qui utilisent tous le Bluetooth Low Energy et communiquent avec votre téléphone). Vous essayez de vous rendre quelque part à vélo ? Utilisez la navigation BLE pour minimiser les distractions comme les notifications tout en sachant comment vous rendre à destination. Vous aimeriez peut-être savoir quand le café du bureau est prêt ? Nous vous couvrons, mec !

Mais avec l’avènement de l’Internet des objets et l’interconnexion croissante du monde sont venus de nouveaux défis pour nous, développeurs.

Développement d’une application compatible BLE

Si vous avez déjà travaillé sur une application mobile qui se connecte à un appareil Bluetooth Low Energy, vous savez que ce n’est pas la tâche la plus facile. Il y a de nombreux écrans et états internes dans l’app qui sont liés au périphérique et à son comportement. L’application est souvent créée en même temps que le micrologiciel (parfois même le matériel ( !!!)) du périphérique, la connexion peut être capricieuse, l’interface externe instable, la logique interne boguée. En conséquence, l’équipe perd beaucoup de temps sur les Known Issues™ lorsqu’elle teste ses fonctionnalités.

En général, il y a deux façons d’aborder cela :

  1. Premièrement, il y a une façon facile et chronophage : utiliser un smartphone physique et un appareil BLE, passer par tous les tracas pour connecter et configurer l’appareil dans l’état où nous avons besoin qu’il soit et recréer les conditions de test.
  2. Et puis il y a la manière dure : abstraire le dispositif, créer une maquette simple qui peut cacher la manipulation BLE réelle. Cela vous permettra de travailler sur un émulateur Android/simulateur iOS, ce qui vous fera gagner du temps plus tard et vous permettra d’exécuter des tests automatisés sur vos CI. En même temps, cela augmente le coût de maintenance et introduit un nouveau risque en ne testant pas la communication réelle à chaque fois que vous exécutez votre code. Après tout, ce périphérique Bluetooth est probablement au cœur de notre application et il ne doit pas se déconnecter inopinément ou se comporter bizarrement.

Nos amis de Frontside-une société de conseil en architecture et ingénierie logicielle frontale basée à Austin-ont reconnu la nécessité d’une solution plus efficace. Ils nous ont demandé de développer une solution open source pour un développement fiable d’applications compatibles BLE dont tout le monde pourrait bénéficier.

Et c’est ainsi qu’arrive…

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

BLEmulator est là pour vous faciliter la vie ! Il gère tout votre code de production lié au BLE et simule le comportement de la pile Bluetooth d’un périphérique et d’un système réels. Il est simple et flexible, vous permettant de créer à la fois le mock de base et une simulation complète d’un périphérique compatible BLE. Et la meilleure chose – il est open sourced!

Il vous permet de tester le concept derrière votre appareil sans le coût du prototypage matériel. Il permet à votre équipe mobile d’avancer sans attendre de firmware ou de prototypes, juste avec une spécification. Il vous permet de travailler en utilisant uniquement un émulateur Android ou un simulateur iOS, ce qui permet une plus grande mobilité, un travail à distance plus facile et évite la disponibilité limitée des smartphones physiques. Il vous permet de tester vos applications dans des tests automatisés exécutés par votre CI.

Actuellement, BLEmulator est disponible pour Flutter et fonctionne uniquement avec notre FlutterBleLib.

Comment cela fonctionne

Flutter, en tant que framework multiplateforme, a besoin de dépendances natives pour les deux plateformes. Normalement, celles-ci feraient partie de la bibliothèque elle-même, mais nous avons utilisé une approche différente ici. Polidea a une bibliothèque React Native BLE, appelée react-native-ble-plx, un travail impressionnant de nos collègues. Nous avons décidé d’extraire toute la logique native de celle-ci dans une bibliothèque séparée, connue sous le nom de Multiplatform BLE Adapter. De cette façon, nous avons créé un noyau commun qui est utilisé à la fois par react-native-ble-plx et notre plugin Flutter, FlutterBleLib. Comme effet secondaire, nous avons créé une abstraction commune utilisée dans le pont natif, le BleAdapter, qui est un point d’entrée parfait pour la simulation !

Voici à quoi ressemble le flux de données de FlutterBleLib :

  1. Appeler une méthode sur un des objets de FlutterBleLib (BleManager, Peripheral, Service, Caractéristique)
  2. Le code Dart envoie un nom de méthode et ses paramètres sur un pont natif
  3. Le pont natif reçoit les données et les désérialise si nécessaire
  4. Le pont natif appelle une méthode appropriée sur l’instance BleAdapter de l’adaptateur BLE multiplateforme
  5. L’adaptateur BLE appelle la méthode sur RxAndroidBle ou RxBluetoothKit, selon la plate-forme
  6. RxAndroidBle/RxBluetoothKit appelle une méthode système
  7. Système renvoie une réponse à l’intermédiaire
  8. Intermédiaire renvoie la réponse au BleAdapter
  9. BleAdapter répond au pont natif
  10. Natif. bridge mappe la réponse et l’envoie à Dart
  11. Dart analyse la réponse et la renvoie à l’appelant original

Nous avions notre travail tout tracé – les points 4 à 9 sont les points d’entrée idéaux avec un contrat externe défini. En injectant une implémentation différente du BleAdapter, nous pouvons activer la simulation quand nous le voulons.

Puis, nous devions décider où la simulation devait avoir lieu. Nous avons opté pour garder autant de la simulation que possible dans Dart, pour deux raisons principales :

  1. Une définition pour les deux plateformes

Il est important de minimiser à la fois le nombre d’endroits où il est possible de faire une erreur et la quantité de travail nécessaire pour créer un périphérique simulé et le maintenir.

  1. Langage natif de la plateforme

Cela a quelques avantages. Premièrement, les développeurs travailleront plus efficacement avec un outil connu, nous devrions donc éviter d’introduire des langages supplémentaires. Deuxièmement, nous ne voulions pas limiter les choses qu’il est possible de faire sur le périphérique simulé. Si vous souhaitez demander une réponse à certains serveurs HTTP (peut-être avec une simulation plus avancée, exécutant le firmware lui-même ?), vous pouvez le faire sans aucun problème avec le même code que vous écririez pour toute autre communication HTTP dans votre application.

L’itinéraire d’appel simulé ressemble à ceci :

  1. Appeler une méthode sur l’un des objets FlutterBleLib (BleManager, Peripheral, Service, Caractéristique)
  2. Le code Dart envoie le nom de la méthode et ses paramètres sur le pont natif
  3. Le pont natif reçoit les données et les désérialise si nécessaire
  4. Le pont natif appelle la méthode appropriée sur le SimulatedAdapter

    Changed. partie commence maintenant

  5. BleAdapter – dans ce cas, celui du simulateur – transmet l’appel au pont natif du BLEmulator
  6. Le BLEmulator effectue lui-même la logique (si elle n’implique pas le périphérique) ou appelle la méthode appropriée sur le périphérique fourni par l’utilisateur
  7. La réponse est transmise au pont natif du BLEmulator
  8. Le pont natif du BLEmulator transmet la réponse au SimulatedAdapter
  9. Le SimulatedAdapter répond au pont natif

    Retour au flux original

  10. Le pont natif mappe la réponse et l’envoie à Dart
  11. Dart analyse la réponse et la renvoie à l’appelant original

De cette façon, vous utilisez tout votre code de manipulation BLE et travaillez sur les types fournis par FlutterBleLib, peu importe le backend que vous utilisez, que ce soit la pile BT du système réel ou la simulation. Cela signifie également que vous pouvez tester l’interaction avec un périphérique dans des tests automatisés sur votre CI !

Comment l’utiliser

Nous avons couvert son fonctionnement et les possibilités qu’il offre, alors sautons maintenant dans la façon de l’utiliser.

  1. Ajouter une dépendance à blemulator dans votre pubspec.yml
  2. Créer votre propre périphérique simulé en utilisant les classes SimulatedPeripheral fournies par le plugin, SimulatedService et SimulatedCharacteristic (je le couvrirai en détail dans la prochaine section)
  3. Ajouter le périphérique au BLEmulator en utilisant Blemulator.addPeripheral(SimulatedPeripheral)
  4. Appeler Blemulator.simulate() avant d’appeler BleManager.createClient() de FlutterBleLib

C’est tout, seulement quatre étapes et vous êtes opérationnel ! Bon, j’admets que l’étape la plus complexe a en quelque sorte été sautée, alors parlons du deuxième point – définir le périphérique.

Contrat de périphérique

Je vais baser les exemples suivants sur le CC2541 SensorTag de Texas Instruments, en me concentrant sur le capteur de température IR.

Nous devons savoir à quoi ressemblent les UUID du service et ses caractéristiques. Nous sommes intéressés par deux endroits dans la documentation.

UUIDs qui nous intéressent:

  • Service de température IR : F000AA00-0451-4000-B000-000000000000Ce service contient toutes les caractéristiques relatives au capteur de température.
  • Données de température IR : F000AA01-0451-4000-B000-000000000000

    Les données de température qui peuvent être lues ou surveillées. Le format des données est ObjectLSB:ObjectMSB:AmbientLSB:AmbientMSB. Il émettra une notification à chaque période configurable pendant la surveillance.

  • Configuration de la température IR : F000AA02-0451-4000-B000-000000000000

    Interrupteur marche/arrêt pour le capteur. Il y a deux valeurs valides pour cette caractéristique::

    • 00-capteur mis en veille (les données de température IR seront quatre octets de zéros)
    • 01-capteur activé (les données de température IR émettront des lectures correctes)
  • Période de température IR : F000AA03-0451-4000-B000-000000000000

    L’intervalle entre les notifications.La limite inférieure est de 300 ms, la limite supérieure est de 1000 ms. La valeur de la caractéristique est multipliée par 10, donc les valeurs supportées sont comprises entre 30 et 100.

C’est tout ce que nous cherchions, alors passons à la mise en œuvre!

Périphérique le plus simple

La simulation la plus simple acceptera n’importe quelle valeur et réussira toutes les opérations.

En Dart, cela ressemble à ceci :

Court et concis, ressemble presque à un JSON.

Voici ce qui se passe : nous avons créé un périphérique nommé SensorTag qui a un ID spécifié par l’exécution (n’importe quelle chaîne est bien, mais elle doit être unique parmi les périphériques connus de BLEmulator). Lorsque le balayage du périphérique est activé, il s’annonce en utilisant les informations de balayage par défaut toutes les 800 millisecondes. Il contient un service, dont l’UUID est contenu dans les données de l’annonce. Le service contient 3 caractéristiques, comme sur un vrai appareil, et toutes sont lisibles. La première des caractéristiques ne peut pas être écrite, mais supporte les notifications ; les deux autres ne peuvent pas être contrôlées mais peuvent être écrites. Il n’y a pas de valeurs invalides pour les caractéristiques. L’argument convenienceName n’est en aucun cas utilisé par le BLEmulator, mais rend la définition plus facile à lire.

IR Temperature Config et IR Temperature Period acceptent et définissent toute valeur qui leur est passée. La caractéristique IR Temperature Data supporte les notifications, mais n’en envoie jamais réellement, puisque nous ne les avons pas définies de quelque manière que ce soit.

BLEmulator fournit le comportement de base hors de la boîte, en prenant soin du chemin heureux pour toutes vos constructions, en minimisant la quantité de travail nécessaire. Bien qu’il puisse suffire pour certains tests et vérifications de base du respect des spécifications, il n’essaie pas de se comporter comme un véritable appareil.

Nous devons implémenter un comportement personnalisé !

Nous avons conservé à la fois la simplicité et la flexibilité lors de la création de BLEmulator. Nous voulions donner aux développeurs le contrôle nécessaire sur chaque aspect du périphérique créé, tout en exigeant d’eux le moins de travail possible pour créer une définition fonctionnelle. Pour atteindre cet objectif, nous avons décidé de créer l’implémentation par défaut de toutes les méthodes qui pourraient être votre point d’entrée pour un comportement personnalisé et vous laisser décider ce qu’il faut surcharger.

Maintenant, ajoutons un peu de logique.

Faire que la connexion prenne du temps

Un vrai périphérique prendra probablement un certain temps avant de se connecter. Pour y parvenir, il suffit de surcharger une méthode :

C’est tout. Maintenant, la connexion prend 200 millisecondes!

Défaut de connexion

Un cas similaire. Nous voulons conserver le délai, mais renvoyer une erreur indiquant que la connexion n’a pas pu être établie. Vous pourriez surcharger la méthode et lancer un SimulatedBleError vous-même, mais vous pouvez aussi faire :

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

Déconnexion initialisée par le périphérique

Disons que vous voulez vérifier le processus de reconnexion ou simuler le fait de sortir de la portée. Vous pouvez demander à un collègue de courir à l’autre bout du bureau avec un vrai périphérique, ou ajouter quelque bouton de débogage et dans son onPress faire :

yourPeripheralInstance.onDisconnect();

(Bien que la première option semble être plus satisfaisante.)

Modifier le RSSI dans les informations de balayage

D’accord, disons que nous voulons trier les périphériques par leur force de signal perçue et que nous devons le tester. Nous créons quelques périphériques simulés, laissons l’un d’entre eux avec un RSSI statique et ensuite, dans l’autre, faisons :

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

De cette façon, vous pouvez avoir un couple de périphériques avec un RSSI variable et tester la fonctionnalité.

Négociation du MTU

BLEmulator fait la plupart de la logique par lui-même, limitant ainsi le MTU à la plage supportée de 23 à 512, mais si vous avez besoin de le limiter davantage, vous devez surcharger la méthode requestMtu():

BLEmulator négociera automatiquement le MTU supporté le plus élevé sur iOS.

Forcer les valeurs à la plage supportée

Pour limiter les valeurs acceptées par une caractéristique, vous devez créer une nouvelle classe étendant SimulatedCharacteristic.

La caractéristique limite maintenant l’entrée à 0 ou 1 sur le premier octet, ignore tout octet supplémentaire, et renvoie une erreur si la valeur dépasse la plage supportée. Pour supporter le retour d’une erreur, une caractéristique doit retourner une réponse à l’opération d’écriture, d’où la mise de writableWithoutResponse à false.

Mise en marche du capteur

Nous aimerions savoir quand le capteur de température est mis en marche ou arrêté.

Pour y parvenir, nous allons créer un nouveau service avec des UUID codés en dur :

La caractéristique que nous avons définie ne peut pas être surveillée par FlutterBleLib, car il lui manque isNotifiable: true, mais elle peut être, pour votre commodité, surveillée au niveau du BLEmulator. Cela facilite le contrôle du flux global que nous simulons, simplifie la structure et nous permet d’éviter les extensions inutiles des classes de base.

Émission de notifications

Il nous manque toujours les notifications d’émission de la caractéristique IR Temperature Data. Occupons-nous en.

_emitTemperature() est appelé dans le constructeur du service de température et s’exécute dans une boucle infinie. À chaque intervalle, spécifié par la valeur de la caractéristique IR Période de température, il vérifie s’il y a un auditeur (isNotifying). S’il y en a un, il écrit des données (des zéros ou une valeur aléatoire, selon que le capteur est activé ou désactivé) dans la caractéristique IR Temperature Data. SimulatedCharacteristic.write() notifie tout auditeur actif de la nouvelle valeur.

Périphérique avancé en action

Vous pouvez trouver un exemple complet d’un périphérique plus avancé sur le dépôt du BLEmulator. Si vous souhaitez l’essayer, il suffit de cloner le dépôt et d’exécuter son exemple.

Utilisation dans les tests automatisés

Un grand merci, ici, à mon camarade Polidean Paweł Byszewski pour ses recherches sur l’utilisation du BLEmulator dans les tests automatisés.

Flutter a un contexte d’exécution différent pour l’app testée et le test lui-même, ce qui signifie que vous ne pouvez pas simplement partager un périphérique simulé entre les deux et modifier le comportement à partir du test. Ce que vous pouvez faire, c’est ajouter un gestionnaire de données au pilote de test en utilisant enableFlutterDriverExtension(handler: DataHandler), passer les périphériques simulés au main() de votre app et passer des messages de chaîne au gestionnaire à l’intérieur du contexte d’exécution de l’app.

Cela se résume à :Wrapper pour app

Votre périphérique

Votre test

Grâce à ce mécanisme, vous pourriez initialiser le périphérique comme vous le souhaitez et appeler n’importe lequel des comportements que vous avez prédéfinis à l’intérieur de votre périphérique simulé.

Voyez par vous-même

La meilleure chose à propos de tout cela est que vous n’avez pas à me croire ! Vérifiez-le vous-même sur GitHub. Utilisez-le, amusez-vous avec, essayez de le casser, et faites-nous part de tout cela !

Vous aimeriez le voir sur d’autres plateformes également ? Contactez-nous et nous ferons en sorte que cela se produise ! Assurez-vous de consulter nos autres bibliothèques, et n’hésitez pas à nous contacter si vous souhaitez que nous travaillions sur votre projet.