Table des matières

Arduino BLE : communication Bluetooth entre deux Arduinos

1. Introduction

Ce document montre comment établir une communication Bluetooth LE entre deux Arduinos nano 33 BLE, l'un des deux pouvant être Arduino nano 33 BLE SENSE (version comportant divers capteurs).

L'un des deux Arduinos joue le rôle de périphérique (celui qui dispose des capteurs), l'autre joue le rôle de centrale. Le rôle d'un périphérique est de lire des données provenant de différents capteurs ou bien d'agir sur l'extérieur, par exemple au moyen de LED ou de moteurs. La centrale peut se connecter à un ou plusieurs périphériques afin de récupérer des données ou bien de déclencher des actions physiques. L'Arduino jouant le rôle de centrale pourra être relié par port série à un PC, ce qui permettra à ce dernier d'accéder aux différentes périphériques via la centrale.

Dans l'exemple développé ci-dessous, le périphérique fournit à la centrale l'état d'une de ses entrées numériques et reçoit de la part de la centrale l'état d'une entrée qu'il modifie. Cet exemple simple revient à disposer des fonctions digitalRead et digitalWrite depuis la centrale mais celles-ci agissant sur des ports situés sur le périphérique.

2. Notions sur la communication Bluetooth LE

Dans une liaison Bluetooth LE, on distingue le périphérique et la centrale (rôle périphérique et rôle central). Le périphérique fournit des informations ou effectue des actions physiques, la centrale interroge un ou plusieurs périphériques pour centraliser ces informations et les exposer à l'utilisateur. Dans notre cas, le périphérique est un Arduino 33 BLE SENSE, qui collecte des informations physiques à l'aide de capteurs ou actionne des moteurs. La centrale est un Arduino 33 BLE. En général, une centrale peut se connecter à plusieurs périphériques et un périphérique peut recevoir des connexions de plusieurs centrales. Le logiciel qui tourne sur le périphérique est de type serveur, alors que la centrale est un client.

Un périphérique Bluetooth peut délivrer un ou plusieurs services. Chaque service est identifié par un numéro GUID (128 bits). Il existe des services standard mais on doit en général définir son propre identifiant GUID (que l'on peut générer avec ce générateur). Chaque service comporte une ou plusieurs caractéristiques, chacune ayant son propre numéro GUID (proche en principe du GUID du service). Une caractéristique comporte une valeur (de taille pouvant aller jusqu'à 512 octets) qui peut être lue ou écrite aussi bien par le périphérique que par la centrale. Il existe aussi un mécanisme de notification, qui informe la centrale lorsque le périphérique a modifié la valeur d'une caractéristique. Du côté du périphérique, il est aussi possible de savoir si une centrale a modifié la valeur d'une caractéristique.

Les données associées à une caractéristique sont :

La norme Bluetooth LE comporte aussi un protocole d'annonce (advertising), qui permet au périphérique de fournir les informations sur les services qu'il délivre. Une centrale peut scanner les périphériques qui sont à sa portée physique et, pour chaque périphérique trouvé, récupérer l'annonce. La communication est initiée par la centrale. Un périphérique doit donc être en permanence en attente de connexions de la part des centrales.

Voyons le cas développé ci-après, comportant un Arduino périphérique et un Arduino centrale. Le périphérique doit fournir à la centrale l'état de son entrée D2, sous la forme d'un entier 8 bits. La valeur de l'entrée D2 (0 ou 1) est envoyée à la centrale toutes les secondes (par le mécanisme de notification). Par ailleurs, la centrale peut envoyer un état (0 ou 1) qui est utilisé par le périphérique pour modifier l'état de sa sortie D3. Le schéma ci-dessous montre le service et les deux caractéristiques définis par le périphérique.

service.svgFigure pleine page

Le périphérique délivre un seul service (DigitalReadWrite), lequel comporte deux caractéristiques. La caractéristique DIGITAL_READ contient l'état de l'entrée D2 du périphérique. Le périphérique lit périodiquement (toutes les secondes) l'état de D2 et actualise la valeur de la caractéristique DIGITAL_READ en conséquence. La centrale peut lire cette valeur à tout moment mais il peut être aussi notifié lorsque la valeur est mise à jour par le périphérique (qu'elle change ou pas). La notification peut être utilisée lorsque la cadence de lecture d'une donnée est imposée par le périphérique et que la centrale a d'autres tâches importantes à réaliser en parallèle (c'est le cas dans l'exemple ci-dessous). La caractéristique DIGITAL_WRITE permet à la centrale d'envoyer au périphérique l'état de la sortie D3, que celui-ci modifie en conséquence. Remarquons qu'il n'y a pas de notifications allant de la centrale vers le périphérique. En effet, on considère que le périphérique est en permanence en communication avec la centrale et qu'il scrute en permanence la liaison pour savoir si la centrale a modifié une caractéristique.

3. Périphérique

Voici le programme Arduino pour le périphérique.

On commence par définir les identifiants du service et des deux caractéristiques associées (ainsi que les numéros des deux entrées/sortie numériques utilisées) :

digitalReadWrite-peripheral.ino
#include <ArduinoBLE.h>
#define SERVICE_UUID "19B10000-E8F2-537E-4F6C-D104768A1214"
#define DIGITAL_READ_CHARACT_UUID "19B10001-E8F2-537E-4F6C-D104768A1214"
#define DIGITAL_WRITE_CHARACT_UUID "19B10002-E8F2-537E-4F6C-D104768A1214"
#define PIN_IN 2
#define PIN_OUT 3
            

Le service est implémenté par la classe BLEService :

BLEService digitalService(SERVICE_UUID);   
            

Nous devons aussi créer deux caractéristiques, implémentées par la classe BLECharacteristic. La taille d'une caractéristique peut aller jusqu'à 512 octets. Dans le cas présent, les deux caractéristiques ont une taille de un octet et nous pouvons donc utiliser BLEByteCharacteristic.

BLEByteCharacteristic digitalReadChar(DIGITAL_READ_CHARACT_UUID,BLERead|BLENotify);
BLEByteCharacteristic digitalWriteChar(DIGITAL_WRITE_CHARACT_UUID,BLEWrite);
             

Dans les propriétés spécifiées (second argument du constructeur), le masque binaire BLERead signifie que la centrale peut lire dans cette caractéristique. Le masque BLEWrite signifie que la centrale peut écrire dans cette caractéristique. Il est bien sur possible de définir une caractéristique accessible à la fois en lecture et en écriture par la centrale (BLERead | BLEWrite). BLENotify indique que la caractéristique comporte une notification, qui est envoyée à la centrale lorsque le périphérique la modifie.

Nous définissons aussi un descripteur pour chaque caractéristique (bien que le descripteur ne soit pas obligatoire) :

BLEDescriptor digitalReadDescriptor("2901","digitalRead");
BLEDescriptor digitalWriteDescriptor("2901","digitalWrite");
               

Dans la fonction setup, on configure le service. La commande BLE.advertise() permet au périphérique de publier une annonce, ce qui permet à une centrale de le détecter et de récupérer des informations sur les services et les caractéristiques qu'ils contient. Lorsqu'une centrale scanne les périphériques qui sont à sa portée physique, elle voit les annonces.

void setup() {
  Serial.begin(9600);
  while (!Serial);  
  pinMode(PIN_IN,INPUT);
  pinMode(PIN_OUT,OUTPUT);
  if (!BLE.begin()) {
    Serial.println("échec du démarrage de BLE");
    while (1);
  }
  digitalReadChar.addDescriptor(digitalReadDescriptor);
  digitalWriteChar.addDescriptor(digitalWriteDescriptor);
  BLE.setLocalName("DigitalRead");
  BLE.setAdvertisedService(digitalService);
  digitalService.addCharacteristic(digitalReadChar);
  digitalService.addCharacteristic(digitalWriteChar);
  BLE.addService(digitalService);
  BLE.advertise();
}
               

La communication est établie dans la fonction loop. Si une centrale est détectée, on entre dans une boucle et on y reste tant que la centrale reste connectée (la centrale est bien sûr à l'initiative des connexions et déconnexions). Toutes les secondes, l'état de PIN_IN est recopié dans la caractéristique digitalReadChar, ce qui a pour effet de déclencher une notification en direction de la centrale. Le test if (digitalWriteChar.written()) permet de savoir si la centrale a écrit une donnée dans la caractéristique digitalWriteChar. Le cas échéant, la valeur écrite est utilisée pour mettre à jour la sortie PIN_OUT. L'inconvénient de cette manière de procéder est d'introduire un délai d'une seconde entre l'écriture par la centrale et l'action déclenchée par le périphérique. Si l'on souhaite une réponse du périphérique plus rapide, il faut enlever le délai de une seconde dans la boucle et placer l'écriture dans la caractéristique digitalReadChar dans une fonction appelée périodiquement par interruption. Autrement dit, un code correct devra traiter séparément les opérations d'écriture et celles de lecture.

void loop() {
  BLEDevice central = BLE.central();
  Serial.println("BLE central détecté...");
  
  if (central) {
    Serial.println("Connexion au BLE central ");
    Serial.print("adresse MAC : ");
    Serial.println(central.address());
    Serial.println(" ");

    while(central.connected()) {
        delay(1000);
        digitalReadChar.writeValue(digitalRead(PIN_IN));
        if (digitalWriteChar.written()) {
            uint8_t state;
            state = digitalWriteChar.value();
            Serial.print("Digital write = ");
            Serial.println(state);
            digitalWrite(PIN_OUT,state);
        }
    }
    Serial.println("déconnexion du BLE central");

  }

}
               

4. Centrale

Voyons à présent le code de la centrale, c'est-à-dire de l'Arduino qui joue le rôle de BLE central.

On doit commencer par définir les identifiants, évidemment identiques à ceux définis dans le code du périphérique.

digitalReadWrite-central.ino
#include <ArduinoBLE.h>
#define SERVICE_UUID "19B10000-E8F2-537E-4F6C-D104768A1214"
#define DIGITAL_READ_CHARACT_UUID "19B10001-E8F2-537E-4F6C-D104768A1214"
#define DIGITAL_WRITE_CHARACT_UUID "19B10002-E8F2-537E-4F6C-D104768A1214"

byte value;
            

Dans la fonction setup, on initialise le Bluetooth et on publie une annonce (BLE.advertise()).

void setup() {
  value = 0;
  Serial.begin(9600);
  while (!Serial);
  if (!BLE.begin()) {
    Serial.println("* Starting Bluetooth® Low Energy module failed!");
    while (1);
  }
  BLE.setLocalName("Nano 33 BLE (Central)"); 
  BLE.advertise();

}      
            

Dans la fonction loop, on appelle une fonction qui établit la liaison avec le périphérique :

void connectToPeripheral() {
  BLEDevice peripheral;
  Serial.println("recherche du périphérique");
  do
  {
    BLE.scanForUuid(SERVICE_UUID);
    peripheral = BLE.available();
  } while (!peripheral);

  if (peripheral) {
    Serial.println("périphérique découvert");
    Serial.print("adresse : ");
    Serial.println(peripheral.address());
    Serial.print("Nom du périphérique : ");
    Serial.println(peripheral.localName());
    Serial.print(" UUID publié : ");
    Serial.println(peripheral.advertisedServiceUuid());
    Serial.println(" ");
    BLE.stopScan();
    controlPeripheral(peripheral);

    }
}
            

Le périphérique étant découvert, on appelle la fonction controlPeripheral, implémentée ci-dessous. Cette fonction retourne si quelque chose d'anormal se produit, auquel cas la boucle de la fonction loop reprend. Si tout se passe normalement, on entre dans une boucle qui reste active tant que le périphérique reste connecté (boucle while (peripheral.connected()). La seule raison qui provoquerait une interruption de la liaison est une interruption matérielle, par exemple une mise hors tension du périphérique, ou bien un arrêt de son programme (au moment de sa mise à jour). Il est impératif que cette éventualité soit prise en compte pour éviter un blocage du programme lorsqu'on met à jour le code du périphérique (ce qui arrive fréquemment pendant le travail de débogage).

Le mécanisme de notification associée à la caractéristique digitalReadChar fonctionne de la manière suivante : la centrale doit tout d'abord s'enregistrer pour cette caractéristique avec digitalReadChar.subscribe(). Dans la boucle while (peripheral.connected()), la fonction bleCharacteristic.valueUpdated() permet de savoir si une notification a été émise pour la caractéristique. Si c'est le cas, on lit la valeur de la caractéristique avec bleCharacteristic.readValue(). Là encore, ce code n'est pas optimal à cause du délai introduit dans la boucle de communication. Dans un code correct, les actions se produisant périodiquement doivent être déclenchées par interruption au moyen d'un Timer.

void controlPeripheral(BLEDevice peripheral) {
   uint8_t digital;
    if (peripheral.connect()) {
      Serial.println("connexion au périphérique ");
      Serial.println(" ");
    } else {
      Serial.println("échec de la connexion au périphérique ");
      Serial.println(" ");
      return;
    }
    Serial.println("recherche des attributs du périphérique");
    if (peripheral.discoverAttributes()) {
      Serial.println("attributs du périphériques trouvés ");
      Serial.println(" ");
    } else {
      Serial.println("échec de la recherche d'attributs");
      Serial.println(" ");
      peripheral.disconnect();
      return;
    }
    BLECharacteristic digitalReadChar = peripheral.characteristic(DIGITAL_READ_CHARACT_UUID);
    if (!digitalReadChar) {
      Serial.println("le périphérique n'a pas de caractéristique digitalReadChar");
      peripheral.disconnect();
      return;
    }
    else if (!digitalReadChar.canSubscribe()) {
      Serial.println("simple key characteristic is not subscribable!");
      peripheral.disconnect();
      return;
    } 
    else if (!digitalReadChar.subscribe()) {
    Serial.println("subscription failed!");
    peripheral.disconnect();
    return;
  }
  
  BLECharacteristic digitalWriteChar = peripheral.characteristic(DIGITAL_WRITE_CHARACT_UUID);
  if (!digitalWriteChar) {
      Serial.println("le périphérique n'a pas de caractéristique digitalWriteChar");
      peripheral.disconnect();
      return;
    }
    
    while (peripheral.connected()) {
        delay(1000);
        if (value==0) value=1;
        else value=0;
        digitalWriteChar.writeValue(value);
        if (digitalReadChar.valueUpdated()) {
          uint8_t digital;
          digitalReadChar.readValue(digital);
          Serial.print("Digital = ");
          Serial.println(digital);
        }
    }
  }

  

     
            
Creative Commons LicenseTextes et figures sont mis à disposition sous contrat Creative Commons.