Transfert bluetooth d’un feather nRF52840 à un PC Linux

Dans un article précédent, https://coinfocal.com/2022/10/16/bracelet-haptique-a-effet-peltier/, j’explique comment communiquer des données de commandes d’un ordi à un feather nRF52840. Récemment, j’ai eu besoin de transmettre des données dans le sens inverse, soit du microcontrôleur à l’ordi. C’est un peu plus compliqué, parce que je me suis rendu compte que la fonction bleuart.write(), de la librairie bluefruit d’Adafruit, a un comportement asynchrone. On ne peut donc pas utiliser la fonction delay() car cette dernière n’est plus bloquante. Il faut utiliser un timer, soit avec la fonction millis(), soit avec elapsedMillis(). Je préfère cette dernière, parce que je suis un fan de Teensy, mais les deux fonctionnent. AInsi, la seule manière de créer un délai, est de lire le temps du timer dans la boucle et d’agir si la valeur dépasse le temps souhaité.

Du côté de la réception, la librairie python contient probablement de l’asynchronisme en-dessous du capot, mais l’interface avec la fonction read() est bloquante. Fait intéressant, même si aucune donnée n’est transmise, un packet vide est reçu à chaque seconde.

Le code arduino est le suivant :

/*********************************************************************
 This is an example for our nRF52 based Bluefruit LE modules

 Pick one up today in the adafruit shop!

 Adafruit invests time and resources providing this open source code,
 please support Adafruit and open-source hardware by purchasing
 products from Adafruit!

 MIT license, check LICENSE for more information
 All text above, and the splash screen below must be included in
 any redistribution
*********************************************************************/
#include <bluefruit.h>
#include <Adafruit_LittleFS.h>
#include <InternalFileSystem.h>
#include "elapsedMillis.h"

// BLE Service
BLEDfu  bledfu;  // OTA DFU service
BLEDis  bledis;  // device information
BLEUart bleuart; // uart over ble
BLEBas  blebas;  // battery

#define SAMPLE_LEN 32 //Tel que configuré actuellement, bleuart peut seulement envoyer 64 bytes à chaque 1s
char sendBuffer[2*SAMPLE_LEN]; //Le buffer à envoyer par bluetooth
uint16_t* samplept = (uint16_t*)sendBuffer; //Lit le buffer en le considérant comme du 16 bits
uint8_t samplecnt = 0;
elapsedMicros bufferTimer; // Timer qui surveille le temps entre les enregistrements

void setup()
{
  analogReadResolution(14); //Les ADC lisent en 14 bits
  Serial.begin(115200);

#if CFG_DEBUG
  // Blocking wait for connection when debug mode is enabled via IDE
  while ( !Serial ) yield();
#endif
  
  Serial.println("Bluefruit52 BLEUART Example");
  Serial.println("---------------------------\n");

  // Setup the BLE LED to be enabled on CONNECT
  // Note: This is actually the default behavior, but provided
  // here in case you want to control this LED manually via PIN 19
  Bluefruit.autoConnLed(true);

  // Config the peripheral connection with maximum bandwidth 
  // more SRAM required by SoftDevice
  // Note: All config***() function must be called before begin()
  Bluefruit.configPrphBandwidth(BANDWIDTH_MAX);
  
  Bluefruit.begin();
  Bluefruit.setTxPower(4);    // Check bluefruit.h for supported values
  //Bluefruit.setName(getMcuUniqueID()); // useful testing with multiple central connections
  Bluefruit.Periph.setConnectCallback(connect_callback);
  Bluefruit.Periph.setDisconnectCallback(disconnect_callback);

  // To be consistent OTA DFU should be added first if it exists
  bledfu.begin();

  // Configure and Start Device Information Service
  bledis.setManufacturer("Adafruit Industries");
  bledis.setModel("Bluefruit Feather52");
  bledis.begin();

  // Configure and Start BLE Uart Service
  bleuart.begin();

  // Start BLE Battery Service
  blebas.begin();
  blebas.write(100);

  // Set up and start advertising
  startAdv();

  Serial.println("Please use Adafruit's Bluefruit LE app to connect in UART mode");
  Serial.println("Once connected, enter character(s) that you wish to send");
}

void startAdv(void)
{
  // Advertising packet
  Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE);
  Bluefruit.Advertising.addTxPower();

  // Include bleuart 128-bit uuid
  Bluefruit.Advertising.addService(bleuart);

  // Secondary Scan Response packet (optional)
  // Since there is no room for 'Name' in Advertising packet
  Bluefruit.ScanResponse.addName();
  
  /* Start Advertising
   * - Enable auto advertising if disconnected
   * - Interval:  fast mode = 20 ms, slow mode = 152.5 ms
   * - Timeout for fast mode is 30 seconds
   * - Start(timeout) with timeout = 0 will advertise forever (until connected)
   * 
   * For recommended advertising interval
   * https://developer.apple.com/library/content/qa/qa1931/_index.html   
   */
  Bluefruit.Advertising.restartOnDisconnect(true);
  Bluefruit.Advertising.setInterval(32, 244);    // in unit of 0.625 ms
  Bluefruit.Advertising.setFastTimeout(30);      // number of seconds in fast mode
  Bluefruit.Advertising.start(0);                // 0 = Don't stop advertising after n seconds  
}

void loop()
{
  if (bufferTimer >= 100) { //parce que le code BLE est asynchrone, on doit utiliser un timer au lieu du delay
    //À 50ms, on sample à 20Hz
    samplept[samplecnt] = analogRead(A0);
    samplecnt = samplecnt + 1;
    bufferTimer = 0;
  }
  if (samplecnt == SAMPLE_LEN) {
    bleuart.write( sendBuffer, SAMPLE_LEN * 2 );
    samplecnt = 0;
  }
}

// callback invoked when central connects
void connect_callback(uint16_t conn_handle)
{
  // Get the reference to current connection
  BLEConnection* connection = Bluefruit.Connection(conn_handle);

  char central_name[32] = { 0 };
  connection->getPeerName(central_name, sizeof(central_name));

  Serial.print("Connected to ");
  Serial.println(central_name);
}

/**
 * Callback invoked when a connection is dropped
 * @param conn_handle connection where this event happens
 * @param reason is a BLE_HCI_STATUS_CODE which can be found in ble_hci.h
 */
void disconnect_callback(uint16_t conn_handle, uint8_t reason)
{
  (void) conn_handle;
  (void) reason;

  Serial.println();
  Serial.print("Disconnected, reason = 0x"); Serial.println(reason, HEX);
}

Le code python ressemble à ça :

from adafruit_ble import BLERadio
from adafruit_ble.advertising.standard import ProvideServicesAdvertisement
from adafruit_ble.services.nordic import UARTService
import array
import numpy as np
import matplotlib.pyplot as plt
import time

ble = BLERadio()

uart_connection = None

plotbuffer = []

countdatalen = 0

while True:
    if not uart_connection:
        print("Trying to connect...")
        for adv in ble.start_scan(ProvideServicesAdvertisement):
            if UARTService in adv.services:
                uart_connection = ble.connect(adv)
                print("Connected")
                break
        ble.stop_scan()

    if uart_connection and uart_connection.connected:
        uart_service = uart_connection[UARTService]
        starttime = time.time()
        while uart_connection.connected:
            receiveddata = uart_service.read()
            countdatalen = countdatalen + len(receiveddata)
            print(countdatalen)
            #Test de bandwidth 
            if countdatalen >= 10000:
                print(time.time()-starttime)
                break
            #if receiveddata is not None:
                #print(len(receiveddata))
                #plotbuffer.append(array.array('H',receiveddata)) #convertit le bytearray en array de 16bits unsigned
                #plt.plot(array.array('H',receiveddata))
                #plt.show()

Pour une raison obscure, le packet envoyé doit être de 64 octets ou moins. En creusant un peu, je crois que c’est une limitation du bord de l’ordi. La librairie bleuart sur le nRF52840 est supposée supporter un packet de 247 octets. Le microcontrôleur agit comme un périphérique, il s’adapte donc aux limites du contrôleur (ici l’ordi). Cela limite également la bande passante. Dans un test, j’arrive à environ 640 octets/s. Je reçois plutôt exactement un paket de 64 octets à chaque 100ms. J’ai investigué un peu pour comprendre d’oû viennent ces limites.

Selon le standard BLE, l’intervalle de connexion peut descendre jusquà 7.5ms. Il est défini par le contrôleur. En essayant de changer cet intervalle dans le code python, cela me sort l’erreur suivante : NotImplemented pour la méthode setConnInterval. Probablement qu’il est par défaut à 100ms, afin de supporter le plus grand nombre possible d’appareils. Une autre façon de le calculer, c’est que dans le standard qui date d’avant 4.2, la taille du packet était limitée à 20 octets. Pour un intervalle de connection de 30ms, cela fait 1 packet * 20 bytes * 1/0.030 s = 667 octets/s. C’est le cas par exemple de la connxion entre un iPhone 5/6 + IOS 8.x et un nRF8001 (pas du tout ce que j’utilise, mais à des fins de comparaison). Les chiffres proviennent de https://learn.adafruit.com/introducing-adafruit-ble-bluetooth-low-energy-friend/faq

Probablement qu’en utilisant deux nrf52840 pour communiquer, on peut augmenter la bande passante de manière significative. Une autre piste serait d’utiliser le nrf52840 en mode contrôleur. Les nombreuses couches de wrapper (adafruit_ble, qui est en fait la librairie python adafruit-blinka-bleio, qui est elle basée sur la librairie bleak.) Bref, ça devient vite le cauchemar à déboguer, comme d’habitude. C’est pas pour rien que j’avais abandonné bluetooth en 2016 après plusieurs essais infructueux de la configuration du kernel, etc. Heureusement, Adafruit est venu à la rescousse entre temps.

En conclusion, je suis content que ça fonctionne à 640 octets/s. Ça fait partie de la technologie, d’avoir une bande passante limitée, mais une très faible consommation énergétique et une robustesse dans la transmission.