didattica, make, outreach, ricerca, spin-off

BLE con Arduino

In questo post illustro il funzionamento (non proprio banale) di un Arduino con BLE (Bluetooth Low Energy). Come al solito, essendo la fisica il mio campo, non discuterò un esempio generico (l’equivalente di “Hello, World!”), ma una concreta applicazione di fisica, con tanto di discussione dei risultati. In certi passaggi, per brevità o per chiarezza, non sarò rigoroso, e i puristi potranno storcere il naso, ma, come dice A. Zee, “sometimes, too much rigor soon leads to rigor mortis.

Per imparare le basi della programmazione di Arduino, c’è il mio libro pubblicato da Zanichelli

Quanto scrivo si applica a tutte le specie di Arduino compatibili con il protocollo BLE. Io ho eseguito i test con un Arduino MKR 1010 Wifi, misurando pressione e temperatura attraverso un sensore BME280.

ATTENZIONE: questo è un post per lettori avanzati, che hanno già un po’ di esperienza con Arduino. Se non ne avete, difficilmente capirete qualcosa, ma sicuramente vi verrà voglia d’imparare. Per questo c’è il libro pubblicato da Zanichelli, destinato agli studenti delle superiori.

Cominciamo con la definizione delle variabili globali e delle costanti, e con l’inclusione delle librerie (il codice che propongo è stato parzialmente estratto dagli esempi disponibili nelle librerie per Arduino Science Journal, che ho depurato di tutta una serie di “distrattori” per evidenziarne gli aspetti relativi al funzionamento ordinario).

#include <ArduinoBLE.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>

Adafruit_BME280 sensor; 

#define SCIENCE_KIT_UUID(val) ("555a0002-" val 
                               "-467a-9538-01f0652c74e8")

BLEService                   service(SCIENCE_KIT_UUID("0000"));
BLEFloatCharacteristic       tempChar(SCIENCE_KIT_UUID("0014"),
                                      BLENotify);
BLEFloatCharacteristic       pressChar(SCIENCE_KIT_UUID("0015"),
                                       BLENotify);
BLECharacteristic            bme280Data(SCIENCE_KIT_UUID("0020"),
                                        BLENotify, 3 * sizeof(float));

Il codice è più semplice di quanto sembri (i programmatori, oggi, non tendono più a risparmiare sui caratteri immessi da tastiera, come si faceva qualche anno addietro, e questo produce un codice piuttosto prolisso, anche se più autoesplicativo).

Le tre direttive #include<...> servono, rispettivamente, a importare le librerie per usare il BLE, quella generica per i sensori I2C e quella specifica del sensore BME280. In quest’ultima è definita la classe Adafruit_BME280, di cui istanziamo1 un oggetto cui assegniamo il nome sensor, che rappresenta il nostro sensore.

La direttiva #define SCIENCE_KIT_UUID(val) è una macro. La stringa che la segue descrive la maniera in cui si costruisce una costante a partire dal valore assegnato a val. Poiché questa è "555a0002-" val "-467a-9538-01f0652c74e8", invocando SCIENCE_KIT_UUID("0001") si ottiene, di fatto, "555a0002-0001-467a-9538-01f0652c74e8", cioè la stringa che definisce la macro, nella quale val è stato sostituito da 0001. Questo strambo valore è un UUID (Universally Unique IDentifier). Gli UUID definiti da Arduino Science Journal si evincono dal codice presente nell’esempio fornito con le librerie. Se la vostra applicazione non deve interfacciarsi con altri sistemi, di fatto, potete scegliere l’UUID come una stringa casuale di 32 cifre esadecimali, con gli opportuni separatori nelle posizioni indicate nell’esempio.

In ogni applicazione BLE si identifica un dispositivo che fornisce il servizio di pubblicazione dei dati (service, identificato da un UUID), i quali sono detti caratteristiche, ciascuna delle quali è a sua volta rappresentata da un UUID.

L’UUID del servizio corrispondente alla pubblicazione di dati destinati all’Arduino Science Journal è “555a0002-0000-467a-9538-01f0652c74e8”, che è memorizzato nell’oggetto (che si può pensare come una variabile, se non conoscete la OOP) di nome service, della classe (di tipo, per usare la terminologia della programmazione procedurale) BLEService.

In altre parole (e continuando a usare un linguaggio che si rifà alla programmazione procedurale, più comune), la riga

BLEService service(SCIENCE_KIT_UUID("0000"));

definisce una variabile di nome service di tipo BLEservice. Contestualmente alla definizione si assegna alla variabile il valore corrispondente a SCIENCE_KIT_UUID("0000").

Arduino Science Journal, inoltre, si aspetta di ricevere alcune caratteristiche che rappresentano, tra le altre, una pressione (il cui UUID è rappresentato dall’oggetto pressChar, che vale “555a0002-0015-467a-9538-01f0652c74e8”) e una temperatura (l’UUID della quale è tempChar, che vale “555a0002-0014-467a-9538-01f0652c74e8”). Entrambe queste caratteristiche, essendo numeri con la virgola (floating point), appartengono alla classe BLEFloatCharacteristic.

Oltre a queste due ne prevediamo una generica, cui assegniamo l’UUID “555a0002-0020-467a-9538-01f0652c74e8”, appartenente alla classe BLECharacteristic, cui diamo il nome bme280Data. Questa caratteristica servirà per “impacchettare” i dati raccolti in un un’unica struttura dati. Le caratteristiche semplici sarebbero sufficienti, ma qui le definiamo in maniera ridondante per illustrare la tecnica.

Quando si definisce una caratteristica, tra parentesi si passano, nell’ordine, l’UUID e le proprietà. Nel caso delle caratteristiche floating point (BLEFloatCharacteristic), l’unica proprietà riguarda il metodo di pubblicazione, che può essere BLERead, BLEWrite o BLENotify. Quest’ultima proprietà garantisce che il servizio notifichi, al dispositivo che vi si aggancia, la disponibilità di una nuova caratteristica. Nel caso delle caratteristiche generiche, invece, oltre a questa proprietà, occorre fornire la taglia della caratteristica, cioè lo spazio, in byte, necessario per immagazzinarne il valore. Nell’esempio richiediamo che bme280Data sia capace di immagazzinare dati per una lunghezza equivalente a quella di tre numeri floating point (con la virgola) che saranno: il tempo di acquisizione, la pressione e la temperatura.

void setup() {
  BLE.begin();
  sensor.begin(0x76);

  delay(2000);

  BLE.setLocalName("ArduinoGO");
  BLE.setDeviceName("ArduinoGO");
  BLE.setAdvertisedService(service); 
  service.addCharacteristic(temperatureCharacteristic);
  service.addCharacteristic(pressureCharacteristic);
  service.addCharacteristic(bme280Data);

  BLE.addService(service); 
  BLE.advertise();  
}

Il codice di setup() inizializza la comunicazione via Bluetooth (BLE.begin()) e il sensore (sensor.begin()), il cui indirizzo, che si evince dalla documentazione, è 0x76. Dopo aver atteso un paio di secondi per completare le operazioni, assegniamo un nome al servizio e al dispositivo (ArduinoGO). Questo nome sarà visibile ai dispositivi Bluetooth che, nella fase di ricerca, vogliono accoppiarsi.

Quindi, diciamo al servizio quali caratteristiche deve pubblicare (sono quelle che abbiamo definito sopra). Infine, chiediamo ad Arduino di attivare il servizio con BLE.addService() e di renderlo disponibile per l’accoppiamento (pairing) con BLE.advertise().

bool firstConnection = true;

void loop() {
  while (BLE.connected()) {
    // once a client connects stay in the loop and publish data
    if (firstConnection) {
      BLEDevice central = BLE.central();
      firstConnection = false;
    }
    publishData();
    delay(1000);
  }
}

Nella funzione loop() gestiamo la connessione dei dispositivi esterni, che nel gergo BLE sono detti Central. Quando un dispositivo esterno si connette ad Arduino (BLE.connected()) restiamo nel loop fino a quando la connessione non termina. Se è la prima volta che entriamo in questo ciclo (firstConnection) otteniamo i dati del dispositivo e li memorizziamo nell’oggetto central della classe BLEDevice. Nell’esempio non usiamo l’oggetto central, ma può essere utile. Nel ciclo otteniamo e pubblichiamo i dati attraverso la funzione publishData() e ripetiamo quest’operazione ogni secondo. publishData() è definita come segue.

void publishData() {
  unsigned long t0 = micros();
  float T = sensor.readTemperature();
  float p = sensor.readPressure()/1.0e2;
  unsigned long t1 = micros();
  
  // publish to BLE
  temperatureCharacteristic.writeValue(T);
  pressureCharacteristic.writeValue(p);

  float pTdata[3];
  pTdata[0] = 0.5*(t0+t1);
  pTdata[1] = p;
  pTdata[2] = T;
  bme280Data.writeValue((byte*)pTdata, sizeof(pTdata));
} 

Prima di tutto otteniamo temperatura (in gradi centigradi) e pressione (in hPa) dal sensore BME280, attraverso le funzioni dedicate. Come tempo della misura prendiamo la media tra l’inizio e la fine di queste misure.

Assegniamo i valori delle caratteristiche relative a temperatura e pressione usando il metodo writeValue() della classe BLEFloatCharacteristic. Quindi mettiamo nelle tre componenti di un array di numeri floating point, rispettivamente, il tempo, la pressione e la temperatura. Un array è una struttura di dati costituita di una sequenza di byte adiacenti. Poiché un numero floating point si rappresenta, nella memoria di un computer, come una sequenza di 32 bit, sono necessari 24 byte per pTdata. Passiamo quindi l’indirizzo del primo byte dell’array come primo argomento del metodo writeValue() di bme280Data ((byte*)pTdata), che si aspetta un puntatore a un byte, e, come secondo argomento, la lunghezza dell’array, misurata in byte. Il nome dell’array pTdata ne rappresenta il puntatore (cioè l’indirizzo presso il quale i dati sono memorizzati nella memoria della macchina). Dal momento che ptData è un array di float, il puntatore è un puntatore a float. Occorre perciò trasformarlo in un puntatore a byte attraverso l’operatore di casting, rappresentato da (byte*)2.

In questo modo, ogni volta che Arduino misura pressione e temperatura, qualora qualche dispositivo BLE sia collegato, notifica a quest’ultimo la disponibilità dei singoli valori di temperatura e pressione, nonché la disponibilità di un array di tre elementi, il cui primo rappresenta il tempo di acquisizione, il secondo la pressione e il terzo la temperatura.

Il prossimo post spiegherà come intercettare questi valori e come interpretarne il significato fisico. Stay tuned.


1In Italiano, il verbo istanziare non esiste o, più correttamente, si può considerare un neologismo derivante dall’inglese to instantiate, che vuol dire rappresentare attraverso un elemento concreto (instance). La parola instance si potrebbe tradurre con istanza, che tuttavia in Italiano è quello che si dice un false friend: la parola istanza, infatti, non ha esattamente la stessa accezione che il termine instance ha in inglese. Istanziare, dunque, nel linguaggio della programmazione OO, significa creare un oggetto appartenente alla classe specificata.

2Se non capite queste ultime frasi significa che non sapere cos’è un puntatore o, quanto meno, non ne dominate l’utilizzo. Non è un grosso problema: basta limitarsi a copiare (se lo fate in maniera intelligente non c’è niente di male), ma ovviamente se siete voi a dominare la materia è meglio rispetto al caso in cui sia questa a dominare voi. Suggerisco, in questo caso, di studiare su “Programmazione Scientifica“, ed. Pearson (autopromo).

1 pensiero su “BLE con Arduino”

Rispondi

Inserisci i tuoi dati qui sotto o clicca su un'icona per effettuare l'accesso:

Logo di WordPress.com

Stai commentando usando il tuo account WordPress.com. Chiudi sessione /  Modifica )

Google photo

Stai commentando usando il tuo account Google. Chiudi sessione /  Modifica )

Foto Twitter

Stai commentando usando il tuo account Twitter. Chiudi sessione /  Modifica )

Foto di Facebook

Stai commentando usando il tuo account Facebook. Chiudi sessione /  Modifica )

Connessione a %s...