Communication entre Windows 10 UWP et un module Bluetooth LE HM-1x

Beaucoup d’acronymes dans le titre: le but de cet article est d’illustrer comment simuler une communication type « terminal série » entre un Arduino et un PC avec Windows 10, via un module Bluetooth LE (Low Energy). La distance entre le PC et l’Arduino est d’une dizaine de mètres dans un appartement.

Cela a été l’un de mes hobbies pendant le confinement. Comme j’ai trouvé plus de difficultés que je ne pensais avant de commencer, j’ai cru bon d’en faire un article.

Les pré-requis

  • Arduino Uno-rev3 un Arduino Uno (ou un clône moins cher qui embarque le même chipset ATmega328P). N’importe quelle autre carte Arduino peut être utilisée (la Nano par exemple). La Uno est simplement la référence pour apprendre. J’ai utilisé un clône AZDelivery coutant 6€.
  • HM-18 un module Bluetooth compatible HM-10. Il existe plusieurs variantes. J’utilise un module plus récent HM-18 qui m’a couté 9€ et qui est compatible HM-10. HM-18 supporte Bluetooth 5 LE tandis que HM-10 est Bluetooth 4 LE. Dans les faits, la différence est la vitesse et la portée de transmission ainsi que la consommation électrique réduite à performances comparables.
  • breadboard une breadboard, ou « platine d’expérimentation » en bon français, (coût 3€) qui permet un câblage facile sans soudure.
  • des fils électriques type Dupont mâle-mâle pour la breadboard.
  • un câble USB 2.0 mâle type A d’un côté / B de l’autre (souvent fourni avec la carte Arduino).
  • un chargeur 9V, 1A à brancher sur la prise « Jack » de l’Arduino si vous souhaitez ne pas avoir à alimenter l’Arduino avec votre PC via un câble USB. Le mien m’a côuté 11€, il y a sans aucun doute moins cher.
  • un multimètre (ou voltmètre) est conseillé bien que pas indispensable. Si vous utilisez un chargeur 9V pas cher ou douteux, c’est une bonne idée de vérifier qu’il respecte ce qu’il affiche avec un voltmètre. Je n’en parlerai pas dans cet article, mais ça me semble indispensable pour vérifier les branchements d’un circuit un peu plus évolué que celui que je présente ici.
  • un ordinateur avec Bluetooth et Windows 10 (à peu près jour).
  • un attrait pour la programmation sur micro-contrôleur.

Quelques mots sur…

Si vous êtes déjà au fait des premières bases sur Arduino et le module HM-10, les paragraphes qui suivent ne vous intéresseront pas. Passez donc à la section suivante.

la programmation sur Arduino

L’Arduino est prévu pour le langage C++, avec un (petit) sous-ensemble des librairies standards du C. Cet article contient un programme d’exemple non détaillé. Personnellement, je n’ai eu aucune difficulté sur cette partie, avec un bon background C#, il faut simplement se reporter à la documentation notamment sur les types. Je pense qu’il est plus facile de développer en tant que débutant C++ sur Arduino que sur un programme « classique » pour ordinateur pour deux raisons:

  • il y a en fait beaucoup de contraintes qui ont pour effet de « cadrer »,
  • par extension au premier point: les contraintes font que le programme sera de petite taille (donc plus simple de raisonner sans avoir besoin d’une « structure » élaborée),
  • par extension au premier point: l’écosystème est riche, on trouve de nombreux exemples faciles à réutiliser, un IDE fonctionnel (Arduino IDE ou VS Code).

Pour aller loin, l’expérience n’est jamais sans importance, mais ce n’est pas un pré-requis pour démarrer.

Sans surprise, c’est du « bas niveau »: il faut se montrer plus rigoureux car l’intellisense et les avertissements du compilateur sont, à mon goût, beaucoup moins « intelligents » que pour C#; il est très facile de compiler un programme qui ne fonctionnera pas comme prévu en se trompant dans les types de variables.
Une autre difficulté sans surprise également est la contrainte des ressources dont vous disposez: le programme doit être petit (comparativement à un programme pour ordinateur classique), que ce soit en termes de lignes de codes ou de données en mémoire, vous disposez de très peu de ressources. Par exemple vous ne pouvez pas, de base, stocker 1440 entiers de type uint16_t (équivalent à ushort en C#, pour des valeurs de 0 à 65535) en mémoire , ce qui peut correspondre à une mesure par minute sur 24 heures, c’est peu pour un ordinateur, mais trop pour un Arduino (mémoire RAM limitée à 2048 octets!).

le Bluetooth LE

Bluetooth est une spécification assez monstrueuse dont je n’ai pas encore tout lu. Ce que j’en ai retenu à ce stade est qu’elle est séparée en deux sous-spécifications:

  • Bluetooth « Classic »
  • Bluetooth Low Energy (LE)

Dans cet article, je ne parle que du Bluetooth LE, qui est adapté à la transmission d’une faible quantité de données, en mode discontinu, où la vitesse de transmission n’est pas importante. Par exemple, si vous avez besoin de diffuser les mesures d’un capteur toutes les 30 secondes (ou moins), Bluetooth LE est un choix pertinent (pas le seul). En revanche, on ne transmet pas (pour autant que je sache) de l’audio ou de la vidéo. Pour un flux, on utilise le Bluetooth « Classic » en mode connecté.

le module HM-10

Le HM-10 est un composant SoC (System On a Chip), c’est-à-dire un micro-contrôleur autonome que l’on relie à un micro-contrôleur principal, ici l’Arduino. Noter qu’il y a eu un grand nombre de mises à jour de firmware pour le HM-10: si vous en achetez un aujourd’hui, il ne devrait pas y avoir de difficulté.

Il y a d’autres moyens de faire du Bluetooth LE avec l’Arduino (regardez l’ESP32 par exemple), cet article est spécialement rédigé pour un module compatible HM-10.
Si vous achetez un autre module Bluetooth LE, assurez-vous auparavant de vérifier que vous trouvez une documentation suffisamment détaillée, en particulier l’identifiant de Service et Characteristic GATT. Ces identifiants ressemblent soit à un GUID (ou UUID), soit ont la forme de 4 caractères hexadécimaux. Par exemple, pour le module HM-10, c’est FFE0 pour le service et FFE1 pour la characteristic (équivalent aux GUID 0000FFE0-1000-8000-00805F9B34FB et 0000FFE1-1000-8000-00805F9B34FB respectivement). La dernière partie 1000-8000-00805F9B34FB est connue sous le terme d’UUID de base définie par la spécification Bluetooth (plus précisément Service Discovery Protocol (SDP) dans la « Core Specification »).

Du point de vue de la carte Arduino, ce module HM-10 « abstrait » le Bluetooth derrière des commandes « AT ». Cela m’a rappelé de vieux souvenirs d’un de mes premiers jobs où je développais des programmes qui devaient communiquer avec différents types de modems GPRS. Je ne saurai dire si un module Bluetooth peut être assimilé à un modem, mais j’avoue avoir été surpris que ce moyen soit utilisé. Historiquement en tout cas, le modem était relié à l’interface de communication RS-232 dite « série ». Ensuite on pouvait entrer ces fameuses commandes AT depuis un terminal (une fenêtre de commandes textes où toute ligne tapée est transmise et toute réception est affichée ligne par ligne dans la fenêtre, un peu comme SSH au niveau de l’expérience utilisateur en plus basique, ce n’est pas un shell).

Le fait que le Bluetooth soit abstrait rend les choses à la fois plus faciles et plus compliquées (voire impossible). Tout dépend ce que vous souhaitez faire. Pour faire simple, vous ne contrôlez pas le Bluetooth avec le HM-10, vous envoyez et recevez des messages via une communication série (physique ou simulée). Lorsque le module n’est pas connecté à un autre module Bluetooth, les messages que vous transmettez sont les commandes AT à destination du module lui-même, et les messages que vous recevez sont les acquittements de ces commandes. Ces commandes servent typiquement à changer ses paramètres ou son mode de fonctionnement, ou à initier une connexion. Lorsque le module est connecté à un autre module Bluetooth, les messages que vous transmettez seront retransmis par Bluetooth au module distant. Et les messages que vous recevez sont soit ceux reçus par Bluetooth, soit des « notifications » du module (similaires dans leur forme aux acquittements AT mais ce ne sont pas des acquittements dans le sens où vous pouvez les recevoir sans avoir envoyé de commande AT). Ces notifications sont typiquement celles de la connexion d’un module Bluetooth distant ou de sa déconnexion. L’inconvénient le plus évident donc est que vous ne pouvez plus vraiment contrôler le module lorsqu’une communication Bluetooth est établie. Dans les faits, il semble que la commande AT » permette de couper une connexion et de reprendre le contrôle du module, mais j’ai constaté que ce n’était pas fiable à tous les coups. Je pense que la meilleure alternative, pour couper la connexion et reprendre le contrôle du module est de couper son alimentation grâce à un montage électronique et au programme de l’Arduino. Ce serait peut-être le sujet d’un autre article car le sujet m’intéresse.

Windows 10 et le Bluetooth

J’ai été surpris de trouver un support très limité du Bluetooth au niveau de .NET. Pour autant que je sache, il n’est possible de communiquer en Bluetooth via C# que depuis une application UWP (Universal Windows Platform) ce qui est très limitant.

Autre surprise: l’API à disposition n’est pas des plus simples à utiliser, voire même mal faite à mon avis. Un exemple parmi d’autres: la méthode BluetoothLEDevice.FromIdAsync() censée retourner une instance de BluetoothLEDevice peut retourner null (sans que cela ne soit documenté, et même si l’identifiant utilisé en argument est parfaitement valide). De plus, au moins dans ma version de Windows 10 (2004) et inférieures, il semble y avoir un bug si vous tentez de communiquer en Bluetooth LE avec un périphérique déjà appairé à Windows. Il faut donc ne pas associer le module Bluetooth à Windows pour que la communication fonctionne (sinon il suffit de rompre cette association).

Pour démarrer, le circuit électronique

Je n’ai pas fait de schéma illustratif, mais le câblage est rudimentaire puisqu’il n’y a qu’un composant à brancher à l’Arduino. Un point important sur lequel faire attention est l’alimentation: le module HM-18 que j’utilise accepte une tension de 3,3V. Il ne faut donc pas utiliser la sortie 5V de la carte mais bien la sortie 3,3V (ces broches se trouvent au niveau de la sérigraphie « POWER » sur la carte).

Si vous ne savez pas utiliser une Breadboard, commencez par apprendre, c’est très rapide en cherchant un peu (par exemple sur cette page). Je ne détaille pas ici.

Pour la communication série entre le module et l’Arduino, j’ai choisi les broches digitales 12 et 13. On peut en utiliser d’autres, il faut simplement éviter les broches 1 et 2 qui sont reliées au port série de l’USB. Je conseille aussi d’éviter les broches PWM qui fonctionneront mais dont on n’a pas besoin de leur caractéristique PWM (autant les laisser libre en cas de besoin ultérieurement). Egalement, il peut être judicieux de laisser la broche 3 disponible si on souuhaite s’en servir plus tard pour son support interrupt (sur la Uno, les broches 2 et 3 sont compatibles interrupt).

Dans mon cas, j’ai relié:

  • La broche 13 de l’Arduino à la broche RX du HM-18. La broche 13 sera donc TX (transmission) du point de vue du programme de l’Arduino.
  • La broche 12 de l’Arduino à la broche TX du HM-18. La broche 12 sera donc RX (réception) du point de vue du programme de l’Arduino.

Pour l’alimentation, la broche GND (Ground) du HM-18 est reliée au GND de l’Arduino et la broche VCC (qui signifie je suppose Voltage Continuous Current) du HM-18 est reliée au 3.3V de l’Arduino.

Ensuite, que vous alimentiez l’Arduino via son port USB ou via un adaptateur secteur 9V sur sa prise Jack, vous devriez voir le module bluetooth s’allumer également. Dans mon cas, sa LED clignote dès qu’il transmet. A la la mise sous tension, il clignote, c’est parce qu’il est en mode Advertisement, c’est-à-dire qu’il publie son identité régulièrement afin d’être découvrable par d’autres périphériques Bluetooth (qui seraient en mode Scanner). Si la LED est allumée en continue, cela indique une connexion active. Si elle est éteinte, cela indique que le module est en veille: il reste possible de s’y connecter mais il ne peut plus être « découvert »car il ne diffuse plus son identité en continu.

Pour tester si le bluetooth fonctionne, il existe plusieurs apps iOS ou Android de test. Dans mon cas j’ai utilisé « DSD TECH Bluetooth » sur iOS qui simule un terminal série sur l’iPhone. L’app commence par scanner les périphériques Bluetooth à proximité et permet de s’y connecter. Vous devriez trouver votre module dès qu’il est alimenté (aucun programme Arduino n’est requis car le module est autonome). Une fois connecté, vous ne recevrez rien car le module ne transmet aucun message, mais vous devriez pouvoir envoyer des commandes AT, le module vous répondra alors par un acquittement. Par exemple « AT » va retourner « OK ». La commande « AT » est une commande de test qui n’a normalement aucune action. A savoir que sur le HM-18, j’ai constaté que cette commande pouvait couper la connexion Bluetooth si elle est transmise au module depuis le programme Arduino, mais ça ne semble pas fonctionner à tous les coups. La connexion ne sera pas rompue en tout cas si vous envoyez cette commande par Bluetooth via l’app iOS.

Ensuite, le Sketch Arduino

Le sketch est le terme utilisé par Arduino pour désigner son programme embarqué.

Le programme d’exemple que j’utilise est disponible sur Github (demo-hm10.ino).

Je ne vais pas m’étendre sur comment transmettre le programme sur l’Arduino et sur les bases de sa programmation. Le plus simple est de commencer avec Arduino IDE, vous pourrez ensuite passer à VS Code très facilement (Arduino IDE reste plus facile pour démarrer, mais devient vite très limité car il n’y a aucun intellisense). Pour les bases de programmation, Arduino IDE contient un grand nombre d’exemples, et l’on trouve de nombreux guides sur Internet.

Voici les instructions principales pour travailler avec le module Bluetooth:


// importe la librairie SoftwareSerial pour simuler un port Serie avec deux broches digitales
// cf. https://www.arduino.cc/en/Reference/softwareSerial
#include <SoftwareSerial.h> 

// déclare un (faux) port SoftwareSerial nommé HM18 avec les broches 12 en réception et 13 en transmission
SoftwareSerial HM18(12, 13);

// initialise la communication série HM18 à la vitesse 9600 bauds
HM18.begin(9600);

// envoie un message via le port série HM18
HM18.print(data);

// reçoit un message via le port série HM18
String received = HM18.readString();

Le programme est plus développé bien sûr, mais ce sont les instructions principales pour communiquer avec le module, print() pour envoyer une chaîne, readString() pour en recevoir une. Il est également possible d’envoyer des octets et non une chaîne. C’est d’ailleurs une bonne idée pour un cas d’utilisation réelle. Néanmoins l’échange de chaînes simplifie beaucoup les choses pour commencer, en particulier pour simuler un terminal série comme je le fais dans cet article.

Une fois mon programme d’exemple chargé sur l’Arduino, si vous testez de nouveau l’app de votre téléphone, vous devriez recevoir toutes les 30 secondes un petit message d’exemple. De même, la commande « .state? » vous retournera un message d’état, et la commande « .reboot » va faire redémarrer l’Arduino (simule un soft reset). Cette dernière commande va évidemment interrompre la connexion.

Enfin le programme Windows

Pour communiquer avec l’Arduino via Bluetooth, depuis un programme en C#, il semble qu’on soit limité actuellement à une application UWP. Pour ma part, ça n’a pas été une expérience réjouissante. Pour faire court, le SDK UWP n’avance pas au même rythme que .NET (et .NET Core en particulier), il est même plutôt en retard, mais UWP a accès aux API WinRT qui sont la version moderne de la couche API Win32 de Windows et exposée via les interfaces COM (accessible via P/Invoke en .NET). Un programme .NET classique n’a pas accès aux API WinRT. Certes en UWP, il n’y a plus besoin de P/Invoke, ce qui est confortable, mais l’expérience de développement actuelle (avec UWP) reste toujours en deçà d’un programme .NET traditionnel (dans le sens non UWP).

Pour lire la documentation de Microsoft, la page Bluetooth est un bon point d’entrée.

Avant d’aller plus loin, il est important d’appréhender quelques termes de la spécification Bluetooth.

Concepts importants

Rôles Central et Peripheral

Pour commencer, il y a la notion de rôle Central et Peripheral. Un périphérique Bluetooth est soit Central, soit Peripheral. Pour ce que j’en ai compris à ce stade, cette notion est importante pour l’établissement de la connexion radio. Le Central est celui qui initie et contrôle la connexion, le Peripheral est celui qui diffuse sa présence (mode advertisement) et répond aux demandes de connexion. Un même périphérique peut parfois jouer les deux rôles (pour communiquer avec différents périphériques Bluetooth). Dans le cas de cet article, le module HM-18 est Peripheral et l’ordinateur (ou le téléphone) est Central.

Services GATT

Même si on parle de « connexion » entre deux périphériques Bluetooth, le mode de fonctionnement en Bluetooth LE est en quelque sorte déconnecté dans l’approche qu’on en a depuis Windows. Windows gère la communication Bluetooth bas niveau, et expose le modèle GATT (Generic Attribute Profile) qui est un protocole du Bluetooth.

Pour résumer à l’extrême, le modèle GATT expose des Services qui contiennent des Characteristics. Une Characteristic contient une valeur qui peut être lue, et parfois écrite (selon le contrat de la Characteristic). Une Characteristic peut également exposer un contrat de notification pour observer le changement de sa valeur.

Le modèle GATT définit également la notion de rôle Server et Client. Le Server est le périphérique qui expose les services GATT, le Client est celui qui souhaite les consommer. Rien n’empêche cependant au client d’envoyer des messages au serveur mais, par exemple, je crois que la Notification de mise à jour d’une Characteristic ne se fait que du Server vers le Client. Dans notre cas, l’ordinateur sous Windows est un client tandis que l’Arduino est le server. D’après ma compréhension, il n’y a pas de lien entre le rôle GATT (Client ou Server) et le rôle Bluetooth (Central ou Peripheral). Tel que je me le représente, le rôle GATT a trait en quelques sortes à la logique applicative (en terme d’échange de données) et le rôle Bluetooth a trait à la logique de communication radio (en terme de connexion). On pourrait parler de Server GATT et de Client GATT d’un coté, et de Peripheral Bluetooth et Central Bluetooth de l’autre.

Il semble y avoir une certaine confusion dans ces termes, et j’espère justement ne pas m’être trompé dans cette description. La documentation du module HM-18 que j’utilise par exemple semble fusionner Peripheral avec Server et Central avec Client (seuls les termes Peripheral et Central sont utilisés). Il semble possible de changer son rôle Bluetooth, mais cela affecte alors son rôle GATT. Leur documentation utilise aussi parfois les termes Master ou Slave, je pense qu’ils l’utilisent également comme synonyme pour Server et Client respectivement. De façon générale, la documentation des modules type HM-10 requiert une petite gymnastique intellectuelle…

L’échange de message en Bluetooth LE se fera donc par l’intermédiaire d’une Characteristic. Pour y accéder facilement il est important de connaître son identifiant (ainsi que celui du service qui l’expose). C’est un détail à trouver dans la documentation du module Bluetooth. Dans le cas du HM-18, c’est FFE0 pour le service et FFE1 pour la characteristic comme dit plus haut.

Programme

Le code source est sur Github (UWP-console).

Je me suis largement appuyé sur l’exemple fourni par Microsoft également sur Github.

Pour tester le programme d’exemple, vous devriez pouvoir le compiler. Vous aurez probablement besoin de Visual Studio 2019 (16.8 ou supérieur) car j’ai utilisé C# 9 (tant qu’à faire). Sinon vous pouvez adapter légèrement le code du projet pour un IDE de version antérieure.

Vous aurez également besoin de Windows 10 version 1903 minimum (j’ai simplement sélectionné ma version Windows 10 comme minimum). Si votre version est inférieure, vous pouvez essayer de changer cette valeur dans le fichier « .csproj » (élément <TargetPlatformMinVersion>10.0.18362.0</TargetPlatformMinVersion>).

Lorsque le programme démarre, il tente de scanner les périphériques Bluetooth compatibles HM-10 (cette logique est dans la classe Hm1xDeviceScanner). Cette notion de compatibilité s’appuie sur le service GATT requis, exposé par le module HM-10, sur le fait que le périphérique est présent (détecté allumé à proximité), et qu’il n’est pas associé (appairé) à Windows. J’ai ajouté cette dernière condition car il y a semble-t-il un bug dans l’API Bluetooth de Windows qui empêche de se connecter à un périphérique Bluetooth LE si on l’a associé. La raison est que Windows s’attend à ce que l’on associe un périphérique Bluetooth (classique) mais pas LE (Low Energy). C’est je pense un bug dans l’absolu car sauf erreur, la spécification Bluetooth permet à un même périphérique de supporter le mode LE et le mode classique.

Si plusieurs périphériques sont trouvés, vous pourrez sélectionner celui auquel se connecter. S’il n’y en a qu’un, il sera affiché pour information. Il suffira de valider pour s’y connecter.

Une fois connecté, un terminal série est simulé (la logique est dans la classe Hm1xConsoleTerminal). Pour envoyer une commande (par exemple « .state? » si vous utilisez mon programme d’exemple sur l’Arduino ou simplement « AT » pour recevoir l’acquittement « OK » du module), tapez la commande sur une ligne en validant avec la touche Entrée. Si l’Arduino transmet un message, il sera affiché sur une nouvelle ligne du terminal.

Si vous coupez l’alimentation de l’Arduino, vous verrez que l’application tente de s’y reconnecter. Si vous rebranchez l’Arduino, la connexion devrait se retrouver au bout d’une minute maximum.

Pour quitter proprement, tapez la commande « exit ». L’identifiant du périphérique Bluetooth sera persisté dans les paramètres locaux de l’application. Au prochain démarrage, si le périphérique est accessible, le programme s’y connectera directement sans passer par un scan.

Lors de mes essais, j’ai pu communiquer avec mon PC depuis le rez-de-chaussée avec l’Arduino au deuxième étage d’une maison.

Si vous faites votre propre application UWP, la première chose importante à vérifier est de déclarer la capability « bluetooth.genericAttributeProfile » dans le fichier Package.appxmanifest. Sans cette capability, la méthode BluetoothLEDevice.FromIdAsync() qu’on utilise retournera null.

Voici la section correspondante dans le fichier de mon exemple:


  <Capabilities>
    <Capability Name="internetClient" />
    <DeviceCapability Name="bluetooth.genericAttributeProfile"/>
  </Capabilities>

Quelques liens

Voici les ressources qui m’ont été le plus utiles: