Nébuliseur (ou atomiseur) à ultrasons

Récemment, j’ai pu jouer avec un nouvel appareil : un nébuliseur (ou atomiseur) d’eau. Ce sont ces humidificateurs qui créent une petite brume d’eau. Il est possible d’ajouter quelques gouttes de parfum, par exemple sous forme d’huile essentielle pour parfumer l’atmosphère en même temps.

Celui-ci est un peu particulier puisqu’il est pilotable par Bluetooth, au vu de ses fonctionnalités :

  • Démarrage et extinction,
  • Éclairage coloré configurable (couleur fixe, changeante, ou éteint),
  • Débit faible ou élevé,
  • Mode de fonctionnement intermittent (30 secondes actif, 30 secondes arrêté)
  • Minuteur pour arrêt automatique,
  • Enceinte musicale sans fil.

Principe de fonctionnement

Il y a plusieurs mécanismes intéressants mis en jeu dans cet appareil. Chacune des parties ci-après va brièvement expliquer le fonctionnement.

Nébuliseur ultrasons

Ce dispositif créée véritablement une fumée de gouttelettes d’eau (et non de la vapeur d’eau car l’eau n’est pas sous forme de gaz), qui sont projeté en l’air. Pour faire jaillir des gouttelettes de taille microscopique, cet appareil utilise un transducteur piézo-électrique à ultra-sons. Typiquement, pour ce genre d’application, il s'agit de petits disques en céramique qui valent quelques euros pièce maximum.

  • Un transducteur est élément qui transforme une grandeur physique en une autre. Dans ce cas, il s’agit d’une tension électrique en un déplacement physique.
  • L’effet piézo-électrique s’observe pour les matériaux qui se déforment physiquement en fonction de la tension qu’on leur applique. Ici, c’est l’effet utilisé par ce transducteur (voir exemple du point précédent).
  • Ce type de disque pour être commandé pour vibrer, et l’idéal est de le faire sur la fréquence de résonance. Cette fréquence est donnée par le fabricant. Dans le cas des atomiseurs, le but est de projeter des goutelettes, et pour cela il faut une haute fréquence, supérieure à ce qui peut être entendu par un humain (au delà de 20 kHz). On en trouve à partir de 100 kHz jusqu’à plusieurs MHz.

bidouilles:atomiseur-ultrasons-principe.png

L’appareil en question a en plus un petit ventilateur pour forcer l’extraction du brouillard par la sortie .

Enceinte Bluetooth

Cet appareil présente en fait deux facettes lors d’un scan Bluetooth (Aromamusic et Aroma31) :

L’un est pour la partie de contrôle de l’appareil, et l’autre est exclusivement dédié à l’audio. Du coup, l’application fournie par le fabricant est non-nécessaire pour ce qui est de la partie musicale.

Commande Bluetooth

L’application de télécommande est téléchargeable pour Android et iOS. Elle est relativement simple, ce qui est compréhensible au vu du peu de fonctionnalités offertes. Néanmoins, elle est pas très stable, et pose parfois des soucis de fiabilité en témoigne les avis sur le Play Store.

Parenthèse vie privée : il s’agit d’une application presque exemplaire : elle n’accède pas d’accès à Internet (et donc, pas de compte à créer). Pour ce qui est des permissions, en voici la liste :

  • Localisation : nécessaire pour utiliser le Bluetooth, car en connaissant le lieux d’émetteurs Bluetooth, il est possible de savoir où est l’utilisateur, et ce sans GPS
    • ACCESS_COARSE_LOCATION
    • ACCESS_FINE_LOCATION
  • Bluetooth : pour l’interaction avec la puce Bluetooth (allumage, association, scan, communication, etc.)
    • BLUETOOTH
    • STATE_CHANGED
    • BLUETOOTH_ADMIN
  • Stockage : absolument inutile dans ce cas
    • READ_EXTERNAL_STORAGE
    • WRITE_EXTERNAL_STORAGE
  • État et identité du téléphone : absolument inutile encore une fois
    • READ_PHONE_STATE

Avec l’utilisation de l’excellente application nRF Connect, il s’avère que la commande sur fait donc avec l’appareil “Aroma31”, avec une communication Bluetooth Low Energy (“LE only” sur la capture d’écran ci-dessous). Effectivement, il y a très peu de données à transmettre, c’est une utilisation tout à fait adéquate.

Une fois connecté, nous pouvons voir les différents services Bluetooth mis à disposition, et pour chacun d’eux, chaque caractéristique (voir ça comme un canal de communication thématique, par exemple) :

Nous y voyons 3 services, avec peu de caractéristiques :

  1. Generic Access (défini par le standard Bluetooth) (0x1800) : profil Bluetooth de base (GAP)
    • Device Name (0x2A00) : le nom de l’appareil
  2. Generic Attribute (lui aussi standard) : profil Bluetooth permettant de définir ses propres attributs sous forme de services et caractéristiques (GATT)
    • Service Changed (0x2A05) : permet de notifier des modifications de services et caractéristiques lors de l’utilisation. Ici, en pratique, ce n’est pas utilisé
  3. Unknown service (0xAE00) : service créé par l’appareil pour pouvoir être télécommandé. J’ai choisi de le nommer “Aroma Service”
    • Unknown characteristic (0xAE01) en écriture uniquement : permet d’envoyer des données, sans en recevoir. C’est donc pour envoyer les commandes à l’appareil. Je l’ai renommée “Command”
    • Unknown characteristic (0xAE02) en lecture et notification : permet de recevoir en continu des données, dès qu’elles sont envoyées par l’appareil. C’est donc le canal utilisé pour dire à l’app quelle est la configuration actuelle de l’appareil. Je l’ai renommée “State”

Analyse de la communication

Nous savoir quoi chercher et où, reste donc à avoir un enregistrement de la communication, et des outils pour l’analyser. Nous allons commencer à faire de ingénierie inverse (reverse-engineering) du protocole de communication.

Capture des paquets Bluetooth HCI

Maintenant qu’on sait quoi chercher (une communication Bluetooth Low Energy), on va pouvoir interagir avec le brumisateur via l’app, et demander à Android de conserver l’historique de toute l’interaction avec l’adaptateur Bluetooth (HCI). Pour cela, on va utiliser l’application pour générer un trafic suffisamment spécifique pour tenter d’identifier quel paquet de la capture correspond à quelle action. Après avoir activé les options développeur d’Android et installé ADB pour communiquer avec le téléphone, on va demander gentiment l’historique :

adb shell dumpsys bluetooth_manager adb bugreport

On se retrouve avec une archive zip qu’on va extraire, pour aller chercher le fichier FS⁩/data⁩/log⁩/bt⁩/btsnoop_hci.log (dans mon cas, ça peut changer en fonction du téléphone utilisé).

Préparation de l'analyse avec Wireshark

Le fichier de capture récupéré lors de l’étape précédente est un fichier de capture au format pcap. On va pouvoir l’étudier avec Wireshark. La première chose après avoir ouvert le fichier est à faire à mon avis de mettre un peu de couleur en changeant le profil :

Maintenant, dans la mesure où l’on sait que toute la communication se fait avec l’Aroma Service 0xAE00, on va filtrer uniquement ce qu’on veut :

  • on peut choisir de ne conserver que les paquets qui sont en rapport avec l’Aroma Service : dans la barre d’en haut, saisir btatt.service_uuid16 == 0xae00
  • on peut choisir de ne conserver que les paquets d’écriture sur une caractéristique : btatt.opcode.method == 0x12
  • on peut choisir de combiner les deux filtres précédents pour n’avoir que les écritures concernant l’aroma service. Pour cela, il suffit d’utiliser l’opérateur logique && (un “et”) : btatt.service_uuid16 == 0xae00 && btatt.opcode.method == 0x12

Comment trouver la syntaxe pour faire un filtre ? Très bonne question, qui n’est pas triviale quand on ne le sait pas. Il faut commencer par trouver un paquet qui nous intéresse. Dans le panneau d’information sur le paquet, sélectionner l’élément qui sera utiliser pour le filtrage. Par exemple, pour filtrer les écritures sur une caractéristique, j’ai selectioné “Write Request”, puis dans le menu contextuel, choisir “Copy → As Filter” :

Dernier point pour faciliter l’étude des messages envoyés : les afficher dans une nouvelle colonne dans la liste des paquets. Pour cela, on ouvrir le menu contextuel des têtes de colonnes et choisir “Columns Preferences…”. Avec le bouton +, créer une nouvelle colonne avec comme titre Value (par exemple), de type Custom, qui affiche le champ field btatt.value.

Analyse du trafic

J’ai commencé par une séquence “Allumage - Extinction - Allumage - Extinction - Allumage”, sans chercher à être particulièrement rapide ni lent. Très rapidement, il semble y avoir ce motif qui ce dégage :

Les trois groupes de trois lignes sélectionnés sont probablement l’allumage de l’appareil, avec les deux groupes de 3 lignes intermédiaires correspondant à l’extinction, car le message envoyé (colonne Value) se répète. La colonne Delta affiche le temps en secondes depuis le dernier paquet envoyé : ici, il semblerait que j’ai attendu entre une demi-seconde et deux secondes entre chaque action.

Il est temps d’essayer ! Plutôt que de coder une application pour faire juste cette tentative, on va ressortir nRF Connect déjà utilisé avant : en cliquant sur la petite flèche vers le haut au niveau de la caractéristique Command, on peut choisir d’envoyer un message. Je renseigne donc un byte array qui contient ce que la capture Wireshark indique : “730101000075” et j’envoie. Bingo ! Ça fonctionne, même sans les deux message qui suivent. J’essaie de la même manière le 1er message du trio que je suppose être l’extinction, et une fois encore, ça fonctionne.

Je vais créer un nouveau filtre de couleur “View → Coloring Rules…” pour ces nouvelles découvertes pour rendre la liste de paquets plus lisible et chatoyante. Je continue ce travail pour la suite des messages, et c’est relativement facile d’identifier les différentes choses en jeu. J’obtiens un résultat plutôt satisfaisant :

Voici les règles de couleur utilisées :

La première règle de coloration de la liste utilise une syntaxe pas encore rencontrée : btatt.value[0:2] == 73:03. Les crochets permettent de découper un bout de btatt.value, conservant uniquement les deux premiers octets.

Résumé du commandes

Maintenant que presque tout est connu, il est possible de faire un récapitulatif :

Communication Bluetooth Low Energy, via le profil GATT, avec un service propriétaire d’UUID 0xAE00, et une caractéristique en écriture-seule d’UUID 0xAE01. Chaque commande contient un message sur six octets :

  • Octet 1 : systématiquement 73
  • Octet 2 : catégorie de commande
    • 01 : commande de fonctionnement (on/off)
      • Octets 3 : 1 pour on, 0 pour off
      • Octets 4-5 : 0
    • 02 : minuteur d’extinction automatique
      • Octet 3 : durée en heures avant extinction. Désactivation du minuteur si 30
      • Octet 4 : inconnu, potentiellement les minutes ?
      • Octet 5 : inconnu, potentiellement les secondes ?
    • 03 : contrôle couleur LED
      • Octet 3 : intensité pour la composante rouge
      • Octet 4 : intensité pour la composante verte
      • Octet 5 : intensité pour la composante bleue
    • 04 : mode de fonctionnement (continu ou non)
      • Octets 3 : 1 pour fonctionnement continu, 0 pour fonctionnement intermittent
      • Octets 4-5 : 0
    • 05 : débit de brumisation
      • Octets 3 : 1 pour débit élevé, 0 pour débit faible
      • Octets 4-5 : 0
    • 07 : inconnu, apparaît tout au début de la connexion de l’app
    • 08 : commande de la lumière
      • Octets 3 : 1 pour lumière allumée, couleur changeante, 0 pour lumière éteinte
      • Octets 4-5 : 0
    • 09 : inconnu, mais “encadre” la séquence de tests du minuteur
  • Octet 6 : somme de contrôle sur un octet (crccheck.checksum.Checksum8 en Python)

Maintenant, il ne reste plus qu’à faire une app qui fonctionne mieux :-)