Autour du Bluetooth BLE

Ce message a pour but de faire découvrir et de tester le Bluetooth BLE embarqué sur les cartes Arduino.

Pour ce premier message, on testera l'émulation du port série (UART BLE).

La carte qui a servi, est une Arduino nano IOT 33, normalement, les modules bluetooth embarqués sur les cartes que ce soit Arduino ou ESP32 donne la possibilité d'utiliser le Bluetooth classique ou le Bluetooth BLE (Bluetooth Low Energy), mais ce n'est pas le cas pour l'Arduino (pas d'implémentation pour le Bluetooth classique), d'où l'utilité de cette mise en place.

Il n'y a pas de service dédié (donc reconnu comme tel) pour réaliser une connexion série sur le Bluetooth BLE, mais certains constructeurs (NRF Nordic par exemple) ont créé un service pour cela et qui est reconnu par certaines applications externes.

Sachez qu'en général, les applications (Terminal BLE) permettent manuellement de choisir le service et les caractéristiques associés pour réaliser une connexion série; donc ici, il s'agit de mettre en place un service connu pour ne pas avoir à le faire manuellement au niveau de l'appli.

Le code qui suit permet de créer une émulation du port série (SPP UART BLE), l'application android qui a servi est Serial Bluetooth Terminal (de Kai Morich), il y a très peu de terminal BLE qui permette de traiter les chaines de caractères au format UTF8, je ne connais que celui-ci. Pour ceux qui ont un Apple, je vous laisse le soin de trouver une application correspondante.

Il y a une limitation Bluetooth sur le nombre d'octets que l'on peut écrire sur une caractéristique qui est de 20 octets si l'option BLENotify ou BLEIndicate est ajouté, sinon il dépend du MTU pourrait atteindre 124 octets ou plus en charge utile.

Paramètres de l'application (Serial Bluetooth Terminal de Kai Morich) pour le test:

*** Sur certains téléphones, il peut être nécessaire d'activer le GPS pour que le scan s'effectue correctement, même si l'appli ne le demande pas! ***

Terminal Settings: Charset UTF8 (par défaut)
Display mode: Text (par défaut)

Receive Settings: CR ou LF (Ne pas mettre les deux), le Moniteur série d'arduino devra être configuré de la même façon

Send Settings: Character delay -> 0 ms dans ce cas, l'appli envoie les données en bloc (on sera donc limité au 20 premiers caractères par envoie)
ou Send Settings: Character delay -> 1 ms envoie des données au fil de l'eau

la longueur max de la chaine est définie par la variable tamponCompletedText initialisé a 64 octets que vous pourrez modifier

Si vous souhaitez tester le débordement ou envoyer plus de 20 caractères avec l'envoie par bloc (le débordement signifie que l'on aura atteint le nombre de caractère max que la chaine peut recevoir avant le marqueur de fin. Il suffira d'envoyé 20 caractères à la fois (ainsi nous n'aurons pas le caractère de fin de ligne qui est ajouté par l'appli ('\r' ou '\n').

Voici le code:

/*
  ******************************
  Nico78 - French Forum Arduino
  *********************************************************************************************
  Création d'un service BLE pour ajouter un UART BLE (Equivalent du SPP bluetooth Classic)
  *********************************************************************************************
  Exemple basé sur les informations du site web d'Adafruit, de Neil Kolban et de ThingEngineer:
  https://learn.adafruit.com/introducing-adafruit-ble-bluetooth-low-energy-friend/uart-service
  https://github.com/nkolban/esp32-snippets/blob/master/cpp_utils/tests/BLETests/Arduino/BLE_client/BLE_client.ino
  https://github.com/ThingEngineer/ESP32_BLE_client_uart
  *********************************************************************************************************************************
  Test effectué sur un Arduino nano IOT 33 avec smartphone Android et l'application Serial Bluetooth Terminal (Kai Morich)
  Paramètres de l'application pour le test:
  Terminal Settings: Charset UTF8 (par défaut)
  Display mode: Text (par défaut)
  Receive Settings: CR ou LF (Ne pas mettre les deux), le Moniteur série d'arduino devra être configuré de la même façon
  Send Settings: Character delay -> 0 ms dans ce cas, envoie des données par bloc de 20 caractères au maximum
  ou Send Settings: Character delay -> 1 ms envoie des données au fil de l'eau
  la longueur max de la chaine est définie par la variable tamponCompletedText initialisé a 64 octets que vous pouvez modifier
  *********************************************************************************************************************************
    Infos utiles en FR: https://blog.groupe-sii.com/le-ble-bluetooth-low-energy/
  *********************************************************************************************************************************
*/

#include <ArduinoBLE.h>

// Déclaration du service offert et des caractéristiques associées
// Comprendre que TX et RX sont des caractéristiques offertes pour que le client puisse écrire et lire les données
// donc TX et RX sont vues coté client, par conséquent l'arduino lira dans TX et écrira dans RX
BLEService UARTService("6E400001-B5A3-F393-E0A9-E50E24DCCA9E");
BLECharacteristic TX("6E400002-B5A3-F393-E0A9-E50E24DCCA9E", BLEWrite, 20);
BLECharacteristic RX("6E400003-B5A3-F393-E0A9-E50E24DCCA9E", BLERead | BLENotify, 20);

//Ne pas changer (tampon max de 20 octets que l'on peut écrire dans une caractéristique, restriction du bluetooth)
const unsigned int tamponBluetooth = 20;
char receiveBluetooth[tamponBluetooth + 1] = {0,};

// Vous pouvez changer la valeur tamponCompletedText ci dessous
// suivant la chaine de longueur max que vous souhaitez pouvoir recevoir
const unsigned int tamponCompletedText = 64;
char completedText[tamponCompletedText + 1] = {0,}; // + 1 pour le '\0' de fin de chaine

BLEDevice central;

void setup() {
  Serial.begin(115200);
  while (!Serial);

  if (!BLE.begin()) {
    Serial.println("starting BLE failed!");
    while (1);
  }

  // Nom lié au service GENERIC ACCESS
  BLE.setDeviceName("UART Service");

  // Nom qui apparait lors d'un scan
  BLE.setLocalName("UART BLE");

  // Déclaration du service que l'on veut offrir
  //BLE.setAdvertisedServiceUuid(UARTService.uuid());
  BLE.setAdvertisedService(UARTService);

  UARTService.addCharacteristic(TX);
  UARTService.addCharacteristic(RX);

  BLE.addService(UARTService);

  // Démarrer la publication du service
  BLE.advertise();

  Serial.println("UART Service");
}

void loop() {
  int ret = 0;
  static int etat = 0;
  char receptionPortSerie[tamponBluetooth + 1] = {0,};

  // Nous sommes un périphérique esclave (serveur)
  // et nous attendons une connexion centrale maitre (client)

  // En attente de connexion d'un client
  central = BLE.central();

  // etat est utilisé pour éviter la répétition de l'information
  if (etat == 0) {
    etat = 1;
    Serial.println("périphérique esclave (serveur), en attente de connexion d'un client maitre");
    Serial.println("");
  }

  // Test si un appareil s'est connecté
  if (central) {
    Serial.print("Connecté à l'appareil suivant: ");
    Serial.println(central.address());
    Serial.println("");

    while (central.connected()) {
      // ***********************************************************************
      // **** Lecture des données reçues du port série *************************
      if (Serial.available() > 0) {
        memset(receptionPortSerie, 0, tamponBluetooth);
        for (unsigned int i = 0; i < 20; ++i) { // lecture par bloc de 20 max
          receptionPortSerie[i] = Serial.read();
          //Serial.println(receptionPortSerie[i]);
          if (receptionPortSerie[i] == '\r' || receptionPortSerie[i] == '\n' ) {
            break;
          }
        }
        // *********************************************************************
        // **** Envoie des données du port Série sur le module Bluetooth **
        // que le marqueur de fin soit reçu ou pas (pas de limite pour l'envoie)
        if (writeBleUART(receptionPortSerie))
        {
          Serial.print("Données envoyées: ");
          Serial.println(receptionPortSerie);
        } else {
          Serial.print("Une erreur s'est produite pour l'envoie des données");
        }
        // *********************************************************************
      }

      //***********************************************************************
      //**** Lecture des données du module Bluetooth **************************
      ret = readBleUART();
      if (ret == 1) {                     // données reçues
        Serial.print("Données reçues sur TX: ");
        Serial.print(receiveBluetooth);
        Serial.print("   Longueur: ");
        Serial.println(strlen(receiveBluetooth));
        Serial.println("");
      } else if (ret == 2) {              // chaine complète (marqueur de fin reçu)
        printSerial(completedText, ret);
      } else if (ret == 0) {              // erreur rencontrée, chaine incomplète
        printSerial(completedText, ret);
      } else {                            // -1  Pas de données!
        //Serial.println("Pas de données! ");
      }
      //*************************************************
      // ******* Votre code personnel ici ***************
      //*************************************************
    }

    Serial.print("Déconnecté du central: ");
    Serial.println(central.address());
    Serial.println("");
    etat = 0;
  }
}

int writeBleUART(char text[]) {
  if (central.connected()) {
    if (RX.writeValue(text, strlen(text)))
      return 1;
  }
  return 0;
}

int readBleUART(void) {
  int etat = -1;
  unsigned int lenData = 0;
  static unsigned int align = 0;

  if (central.connected()) {
    if (TX.written()) {
      lenData = TX.valueLength();
      memset(receiveBluetooth, 0, tamponBluetooth);
      if (TX.readValue(receiveBluetooth, lenData)) {
        etat = 1;
        if (align == 0) {
          memset(completedText, 0, tamponCompletedText + 1);
        }
        if ((align + lenData) < (tamponCompletedText + 1) ) {
          memcpy(completedText + align, receiveBluetooth, lenData);
          align += lenData;
          if (completedText[align - 1] == '\r' || completedText[align - 1] == '\n')
          {
            align = 0;
            etat += 1;
          }
        } else {
          memcpy(completedText + align, receiveBluetooth, tamponCompletedText - align);
          align = 0;
          etat = 0;
        }
      }
    }
  }
  return etat;
}

void printSerial(char text[], int etat) {
  if (etat) {
    Serial.print("Chaine complète: ");
  } else {
    Serial.print("Erreur longueur max de la chaine atteinte: ");
    Serial.print(tamponCompletedText);
    Serial.println(" octets et marqueur de fin non reçu!");
    Serial.println("Rappel, le dernier caractère en byte doit être 13 pour '\\r' ou 10 pour '\\n'");
    Serial.print("*** Chaine incomplète *** : ");
  }
  Serial.print(text);
  Serial.print("   Longueur: ");
  Serial.print(strlen(text));
  Serial.print("   Dernier caractère (byte): ");
  Serial.println((byte)text[strlen(text) - 1]);
  Serial.println("");
}

Deuxième exemple de test

Création d'un service GATT 'Health Thermometer' pour la lecture de la température

Cet exemple est très similaire au Moniteur batterie fournit dans les exemples d'Arduino

Fichier->Exemples->ArduinoBle->Peripheral->BatteryMonitor

Il est intéressant car il nous faut convertir le float au format IEEE_11073 (Format des dispositifs médicaux)

J'ai mis un maximum d'infos sur le code source, bonne lecture, fichier disponible en téléchargement!

Extrait:

// Service du thermomètre
// rechercher le service 'Health Thermometer' ou la valeur '1809' dans la page du lien ci dessous:
// https://www.bluetooth.com/specifications/gatt/services/
// puis cliquer sur 'Health Thermometer' afin d'afficher les informations liées a ce service
// vous trouverer les informations des caractéristiques, 5 en tout
// la première est 'org.bluetooth.characteristic.temperature_measurement', les autres sont optionnelles
BLEService thermometreService("1809");

// Caractéristique liée au thermomètre
// sur la page des caractéristiques, https://www.bluetooth.com/specifications/gatt/characteristics/
// rechercher l'information trouvée précédemment'org.bluetooth.characteristic.temperature_measurement' ou la valeur '2A1C'
// puis cliquer sur 'Temperature Measurement' afin d'afficher le format des données pour cette caractéristique
BLECharacteristic thermometreLevel("2A1C", BLEIndicate, 5);

/*
 * Temperature Service        uiid->1809
 * Caractéristiques disponibles pour ce service
 * 
 * Temperature Measurement    uiid->2A1C
 * La première est nécessaire et les 4 autres sont optionnelles

  name                        Requirement Read    Write     Notify    Indicate
            
  Temperature Measurement     Mandatory Excluded  Excluded  Excluded  Mandatory
  Temperature Type            Optional  Mandatory Excluded  Excluded  Excluded
  Intermediate Temperature    Optional  Excluded  Excluded  Mandatory Excluded
  Measurement Interval        Optional  Mandatory Optional  Excluded  Optional
  Measurement Interval        Optional  Mandatory Optional  Excluded  Optional
 * 
 */


/*  
 * Données pour la caractéristique Temperature Measurement
 * 
 * byte flag; bit 0 à 0 pour indiquer des valeurs en °Celsius
 * unsigned long conversionFloat; Valeur de la température
 *
name  InformativeText Requirement   Format  index size  name                   key  value

Flags                 Mandatory     8bit      0     1   Temperature Units Flag  0   Temperature Measurement Value in units of Celsius
Flags                 Mandatory     8bit      0     1   Temperature Units Flag  1   Temperature Measurement Value in units of Fahrenheit
Flags                 Mandatory     8bit      1     1   Time Stamp Flag         0   Time Stamp field not present
Flags                 Mandatory     8bit      1     1   Time Stamp Flag         1   Time Stamp field present
Flags                 Mandatory     8bit      2     1   Temperature Type Flag   0   Temperature Type field not present
Flags                 Mandatory     8bit      2     1   Temperature Type Flag   1   Temperature Type field present
Flags                 Mandatory     8bit          
Flags                 Mandatory     8bit          
Flags                 Mandatory     8bit          
Flags                 Mandatory     8bit          
Flags                 Mandatory     8bit          
Temperature Measurement Value (Celsius)     32bit FLOAT   This field is only included if the flags bit 0 is 0.  C1  FLOAT         
Temperature Measurement Value (Fahrenheit)  32bit FLOAT   This field is only included if the flags bit 0 is 1.  C2  FLOAT         
Time Stamp                                  TIME  FORMAT  If the flags bit 1 is 1 this field is included. If it is 0, this field is not included  C3            
Temperature Type                             8bit UINT8   If the flags bit 2 is set to 1 this field is included. If it is 0, this field is not included C4 

TIME FORMAT (7 octets au total) -> (16bit année) + (8bit mois) + (8bit jour) + (8bit heure) + (8bit minute) + (8bit seconde)
*/

BLE_Temperature.ino (8.62 KB)

Merci.

Je suis justement en train mettre en place un service BLE UART pour configurer et récupérer les données sur une carte adafruit feather nRF 52840. Je vais bien lire ton tuto. Par exemple, je découvre la limite à 20 octets d'un coup ::slight_smile: .

Dans l'exemple, il s'agissait de mettre en oeuvre une émulation série SPP BLE qui soit reconnue par les applis, donc le programme respecte les conditions suivantes:

Une caractéristique pour la lecture avec notification et une caractéristique pour l'écriture avec le service spécifique.

BLECharacteristic TX("6E400002-B5A3-F393-E0A9-E50E24DCCA9E", BLEWrite, 20);
BLECharacteristic RX("6E400003-B5A3-F393-E0A9-E50E24DCCA9E", BLERead | BLENotify, 20);

Si vous aviez la possibilité de créer vous même votre propre interface de connexion, vous pourriez mettre en place plusieurs caractéristiques (je ne connais pas la limite).

C'est sûr que c'est plus facile de tout traiter sur une seule trame, mais il n'y a pas de limite au nombre de trame (c'est le buffer de réception qui limite l'envoie du nombre d'octets qu'il peut recevoir).

Si vous souhaitez rester sur une seule trame, en prenant l'exemple de plusieurs capteurs pour être plus clair, il serait aisé de créer une trame pour 1 ou 2 capteurs (ou par type d'information), le premier octet de la trame permettrait d'identifier le capteur par exemple ainsi on aurait une trame complète pour chaque capteur dont on souhaite obtenir les informations. Je ne sais pas si cela irait dans votre cas!

C'est mon projet d'enregistreur pluviomètre et/ou d'autres grandeurs physiques plus tard. Pour le moment, j'enregistre les impulsions du pluviomètre et la tension de batterie, toutes les minutes, dans un fichier texte, dans la puce de mémoire flash de la carte adafruit nRF52840. Un seul fichier pour toutes les données. Une ligne par donnée. Une ligne commence par une lettre pour le type de donnée (R=rain, V=voltage...). Ensuite, date/heure de mesure et enfin la mesure elle-même. Tout ça uniquement en ASCII.

Ensuite, l'opérateur vient faire le relevé annuel avec son smartphone. Il appuie sur un bouton de l'enregistreur. Ça allume le Bluetooth. Le smartphone se connecte en BLE UART. Il envoie une commande "getdata". Alors l'enregistreur ouvre le fichier et renvoie toutes les lignes les unes après les autres au smartphone.

Pour le moment, je fais les essais avec Bluefruit Connect. Quand j'envoie "voltage", ça me renvoie la tension de la batterie :slight_smile: . Par contre, je peux avoir des lignes du fichier qui font plus de 20 caractères. Il faut que je prévois un mécanisme de fractionnement.

Je viens d'en lire un peu plus, alors la spécification permettrait l'envoie de 512 octets sur une caractéristique mais avec l'option notify ou indicate, la restriction passe à 20 octets.

Je viens de faire l'essai sur l'exemple en enlevant notify sur la caractéristique RX mais du coup l'appli android ne reconnait plus la liaison série comme valide.

Je n'ai aucune pratique concernant la manipulation d'octets pour un fichier, cela dit je ne pense pas que ça puisse être un problème, on doit forcément être en mesure de lire en bloc depuis un fichier ou zone mémoire.

Nouvel exemple ajouté, voir le 3ème message.