Simulatore Bluetooth Low Energy-Una nuova speranza nello sviluppo dell’IoT

Viviamo nel XXI secolo, dove gli animali forse non ci parlano ancora, ma le cose cominciano decisamente a farlo. Andiamo in giro con i nostri centri di controllo delle missioni (alias smartphone) che possono gestire i nostri soldi, le nostre comunicazioni, le nostre notizie, le nostre case intelligenti e ogni altra soluzione intelligente che ci circonda, insomma la nostra vita.

Allenamento per una gara? Puoi farlo con un monitor intelligente dell’ossigeno muscolare Humon. Vuoi migliorare la qualità del tuo sonno? Prendi una smartband, uno smartwatch o uno sleep-tracker (tutti usano il Bluetooth Low Energy e comunicano con il tuo telefono). Stai cercando di andare da qualche parte in bicicletta? Usa la navigazione abilitata a BLE per ridurre al minimo le distrazioni come le notifiche, ma sapere comunque come arrivare dove vuoi. Forse ti piacerebbe sapere quando il caffè dell’ufficio è pronto? Ti abbiamo coperto, amico!

Ma con l’avvento dell’Internet delle cose e la crescente interconnessione del mondo sono arrivate nuove sfide per noi sviluppatori.

Sviluppare un’app abilitata BLE

Se hai mai lavorato su un’app mobile che si collega a un dispositivo Bluetooth Low Energy, sai che non è il compito più facile. Ci sono molte schermate e stati interni nell’app che sono collegati alla periferica e al suo comportamento. L’app viene spesso creata insieme al firmware (a volte anche all’hardware (!!!)) del dispositivo, la connessione può essere volubile, l’interfaccia esterna instabile, la logica interna buggata. Di conseguenza, il team sta perdendo un sacco di tempo su Known Issues™ durante il test delle loro funzionalità.

In generale, ci sono due modi per affrontare questo problema:

  1. Primo, c’è un modo facile, che richiede tempo: usare uno smartphone fisico e un dispositivo BLE, passare attraverso tutto il fastidio per collegare e impostare il dispositivo allo stato in cui abbiamo bisogno che sia e ricreare le condizioni di test.
  2. Poi c’è il modo difficile: astrarre il dispositivo, creare un semplice mock-up che possa nascondere l’effettiva gestione BLE. Questo vi permetterà di lavorare su un emulatore Android / simulatore iOS, risparmiandovi del tempo in seguito e vi permetterà di eseguire test automatici sui vostri CI. Allo stesso tempo, aumenta il costo di manutenzione e introduce un nuovo rischio non testando la comunicazione effettiva ogni volta che si esegue il codice. Dopo tutto, quella periferica Bluetooth è probabilmente il cuore della nostra applicazione e non deve disconnettersi inaspettatamente o comportarsi in modo strano.

I nostri amici di Frontside – una società di consulenza di architettura e ingegneria del software frontend con sede a Austin – hanno riconosciuto la necessità di una soluzione più efficace. Ci hanno chiesto di sviluppare una soluzione open source per uno sviluppo affidabile di app abilitate a BLE di cui tutti possano beneficiare.

E così arriva…

BLEmulator /pronun.: bleh-mulator/, un simulatore Bluetooth Low Energy.

BLEmulator è qui per renderti la vita più facile! Gestisce tutto il vostro codice di produzione relativo a BLE e simula il comportamento di una vera periferica e dello stack Bluetooth del sistema. È semplice e flessibile, permettendovi di creare sia il mock di base che una simulazione completa di un dispositivo abilitato a BLE. E la cosa migliore è che è open sourced!

Ti permette di testare il concetto alla base del tuo dispositivo senza il costo della prototipazione dell’hardware. Permette al vostro team mobile di andare avanti senza aspettare firmware o prototipi, solo con una specifica. Permette di lavorare usando solo l’emulatore Android o il simulatore iOS, consentendo così una maggiore mobilità, un lavoro a distanza più facile ed evitando la disponibilità limitata di smartphone fisici. Ti permette di testare le tue app in test automatizzati eseguiti dalla tua CI.

Attualmente BLEmulator è disponibile per Flutter e funziona solo con la nostra FlutterBleLib.

Come funziona

Flutter, come framework multipiattaforma, ha bisogno di dipendenze native per entrambe le piattaforme. Normalmente, queste sarebbero una parte della libreria stessa, ma qui abbiamo usato un approccio diverso. Polidea ha una libreria React Native BLE, chiamata react-native-ble-plx, un lavoro fantastico dei nostri colleghi. Abbiamo deciso di estrarre tutta la logica nativa da essa in una libreria separata, conosciuta come Multiplatform BLE Adapter. In questo modo abbiamo creato un nucleo comune che è usato sia da react-native-ble-plx che dal nostro plugin Flutter, FlutterBleLib. Come effetto collaterale abbiamo creato un’astrazione comune usata nel bridge nativo, il BleAdapter, che è un perfetto punto di ingresso per la simulazione!

Questo è l’aspetto del flusso di dati di FlutterBleLib:

  1. Chiamo un metodo su uno degli oggetti FlutterBleLib (BleManager, Peripheral, Service, Characteristic)
  2. Il codice Dart invia il nome del metodo e i suoi parametri a un bridge nativo
  3. Il bridge nativo riceve i dati e li deserializza se necessario
  4. Il bridge nativo chiama un metodo appropriato sull’istanza BleAdapter dell’adattatore BLE multipiattaforma
  5. BleAdapter chiama il metodo su RxAndroidBle o RxBluetoothKit, a seconda della piattaforma
  6. RxAndroidBle/RxBluetoothKit chiama un metodo di sistema
  7. Il sistema restituisce una risposta all’intermediario
  8. L’intermediario restituisce la risposta al BleAdapter
  9. BleAdapter risponde al bridge nativo
  10. Il bridge nativo bridge mappa la risposta e la invia a Dart
  11. Dart analizza la risposta e la restituisce al chiamante originale

Abbiamo fatto il nostro lavoro: i punti da 4 a 9 sono i punti di ingresso ideali con un contratto esterno stabilito. Iniettando una diversa implementazione del BleAdapter è possibile accendere la simulazione quando vogliamo.

Poi, dovevamo decidere dove la simulazione doveva avvenire. Abbiamo optato per mantenere la maggior parte possibile della simulazione in Dart, per due ragioni principali:

  1. Una definizione per entrambe le piattaforme

È importante ridurre al minimo sia il numero di posti in cui è possibile commettere un errore sia la quantità di lavoro necessaria per creare una periferica simulata e mantenerla.

  1. Lingua nativa della piattaforma

Questo ha alcuni vantaggi. In primo luogo, gli sviluppatori lavoreranno in modo più efficiente con uno strumento conosciuto, quindi dovremmo evitare di introdurre linguaggi aggiuntivi. In secondo luogo, non volevamo limitare le cose che è possibile fare sulla periferica simulata. Se volete richiedere una risposta da qualche server HTTP (forse con una simulazione più avanzata, eseguendo il firmware stesso?), potete farlo senza problemi con lo stesso codice che scrivereste per qualsiasi altra comunicazione HTTP nella vostra app.

Il percorso di chiamata simulato assomiglia a questo:

  1. Chiamare un metodo su uno degli oggetti FlutterBleLib (BleManager, Peripheral, Service, Characteristic)
  2. Il codice Dart invia il nome del metodo e i suoi parametri al bridge nativo
  3. Il bridge nativo riceve i dati e li deserializza se necessario
  4. Il bridge nativo chiama il metodo appropriato sul SimulatedAdapter

    Changed parte ora

  5. BleAdapter – in questo caso quello del simulatore – inoltra la chiamata al bridge nativo del BLEmulator
  6. BLEmulator o fa la logica stessa (se non coinvolge la periferica) o chiama il metodo appropriato sulla periferica fornita dall’utente
  7. La risposta viene passata al bridge nativo del BLEmulator
  8. Il bridge nativo del BLEmulator passa la risposta al SimulatedAdapter
  9. SimulatedAdapter risponde al bridge nativo

    Torna al flusso originale

  10. Ponte nativo mappa la risposta e la invia a Dart
  11. Dart analizza la risposta e la restituisce al chiamante originale

In questo modo usi tutto il tuo codice di gestione BLE e lavori sui tipi forniti da FlutterBleLib non importa quale backend stai usando, che sia il sistema reale BT stack o la simulazione. Questo significa anche che puoi testare l’interazione con una periferica nei test automatici sul tuo CI!

Come usarlo

Abbiamo coperto come funziona e quali possibilità fornisce, quindi ora saltiamo a come usarlo.

  1. Aggiungi dipendenza a blemulator nel tuo pubspec.yml
  2. Crea la tua periferica simulata usando le classi SimulatedPeripheral fornite dal plugin, SimulatedService e SimulatedCharacteristic (lo coprirò in dettaglio nella prossima sezione)
  3. Aggiungi la periferica al BLEmulator usando Blemulator.addPeripheral(SimulatedPeripheral)
  4. Chiama Blemulator.simulate() prima di chiamare BleManager.createClient() da FlutterBleLib

Tutto qui, solo quattro passi e sei pronto a correre! Bene, ammetto che il passo più complesso è stato un po’ saltato, quindi parliamo del secondo punto: definire la periferica.

Contratto di periferica

Baserò i seguenti esempi sul CC2541 SensorTag di Texas Instruments, concentrandomi sul sensore di temperatura IR.

Dobbiamo sapere come sono gli UUID del servizio e le sue caratteristiche. Ci interessano due posti nella documentazione.

UUUID che ci interessano:

  • Servizio temperatura IR: F000AA00-0451-4000-B000-000000000000Questo servizio contiene tutte le caratteristiche relative al sensore di temperatura.
  • Dati di temperatura IR: F000AA01-0451-4000-B000-000000000000

    I dati di temperatura che possono essere letti o monitorati. Il formato dei dati è ObjectLSB:ObjectMSB:AmbientLSB:AmbientMSB. Emetterà una notifica ogni periodo configurabile mentre è monitorato.

  • Configurazione temperatura IR: F000AA02-0451-4000-B000-000000000000

    Interruttore on/off per il sensore. Ci sono due valori validi per questa caratteristica::

    • 00-sensore messo a dormire (i dati di temperatura IR saranno quattro byte di zeri)
    • 01-sensore abilitato (i dati di temperatura IR emetteranno letture corrette)
  • Periodo temperatura IR: F000AA03-0451-4000-B000-000000000000

    L’intervallo tra le notifiche.Il limite inferiore è 300 ms, il limite superiore è 1000 ms. Il valore della caratteristica è moltiplicato per 10, quindi i valori supportati sono tra 30 e 100.

Questo è tutto quello che stavamo cercando, quindi andiamo all’implementazione!

Periferica più semplice

La simulazione più semplice accetterà qualsiasi valore e riuscirà in tutte le operazioni.

In Dart si presenta così:

Corto e conciso, sembra quasi un JSON.

Ecco cosa succede: abbiamo creato una periferica chiamata SensorTag che ha un ID specificato a runtime (qualsiasi stringa va bene, ma deve essere unica tra le periferiche note a BLEmulator). Mentre la scansione della periferica è attivata, essa farà pubblicità usando le informazioni di scansione predefinite ogni 800 millisecondi. Contiene un servizio, il cui UUID è contenuto nei dati dell’annuncio. Il servizio contiene 3 caratteristiche, proprio come in un dispositivo reale, e tutte sono leggibili. La prima delle caratteristiche non può essere scritta, ma supporta le notifiche; le altre due non possono essere monitorate ma possono essere scritte. Non ci sono valori non validi per le caratteristiche. L’argomento convenienceName non è usato in alcun modo dal BLEmulator, ma rende la definizione più facile da leggere.

IR Temperature Config e IR Temperature Period accettano e impostano qualsiasi valore passato. La caratteristica IR Temperature Data supporta le notifiche, ma in realtà non ne invia mai, dato che non le abbiamo definite in alcun modo.

BLEmulator fornisce il comportamento di base fuori dalla scatola, prendendosi cura del felice percorso per tutti i tuoi costrutti, riducendo al minimo la quantità di lavoro necessario. Mentre può essere sufficiente per alcuni test e controlli di aderenza alle specifiche di base, non cerca di comportarsi come un dispositivo reale.

Abbiamo bisogno di implementare un comportamento personalizzato!

Abbiamo mantenuto sia la semplicità che la flessibilità quando abbiamo creato BLEmulator. Volevamo dare agli sviluppatori il controllo necessario su ogni aspetto della periferica creata, pur richiedendo loro il minor lavoro possibile per creare una definizione funzionante. Per raggiungere questo obiettivo, abbiamo deciso di creare l’implementazione di default di tutti i metodi che potrebbero essere il vostro punto d’ingresso per un comportamento personalizzato e lasciarvi decidere cosa sovrascrivere.

Ora, aggiungiamo un po’ di logica.

Fai in modo che la connessione richieda del tempo

Una vera periferica probabilmente impiegherà del tempo prima di essere collegata. Per ottenere questo, avete solo bisogno di sovrascrivere un metodo:

Ecco fatto. Ora la connessione impiega 200 millisecondi!

Negare la connessione

Un caso simile. Vogliamo mantenere il ritardo, ma restituire un errore che la connessione non può essere stabilita. Potreste sovrascrivere il metodo e lanciare voi stessi un SimulatedBleError, ma potete anche fare:

 @override Future<bool> onConnectRequest() async { await Future.delayed(Duration(milliseconds: 200)); return false; }

Disconnessione inizializzata dalla periferica

Diciamo che volete controllare il processo di riconnessione o simulare di essere fuori portata. Puoi chiedere a un collega di correre dall’altra parte dell’ufficio con una periferica reale, o aggiungere qualche pulsante di debug e nel suo onPress fare:

yourPeripheralInstance.onDisconnect();

(Anche se la prima opzione sembra essere più soddisfacente.)

Modificare l’RSSI nelle informazioni di scansione

Va bene, diciamo che vogliamo ordinare le periferiche in base alla loro potenza di segnale percepita e dobbiamo testarlo. Creiamo alcune periferiche simulate, ne lasciamo una con RSSI statico e poi nell’altra facciamo:

@overrideScanResult scanResult() { scanInfo.rssi = -20 - Random.nextInt(50); return super.scanResult();}

In questo modo, puoi avere un paio di dispositivi con RSSI variabile e testare la funzione.

Negoziare MTU

BLEmulator fa la maggior parte della logica da solo, limitando così l’MTU all’intervallo supportato da 23 a 512, ma se hai bisogno di limitarlo ulteriormente, dovresti sovrascrivere il metodo requestMtu():

BLEmulator negozierà automaticamente il più alto MTU supportato su iOS.

Forzare i valori all’intervallo supportato

Per limitare i valori accettati da una caratteristica, devi creare una nuova classe che estenda SimulatedCharacteristic.

La caratteristica ora limita l’input a 0 o 1 sul primo byte, ignora qualsiasi byte aggiuntivo e restituisce un errore se il valore supera l’intervallo supportato. Per supportare la restituzione di un errore, una caratteristica deve restituire una risposta all’operazione di scrittura, quindi impostando writableWithoutResponse a false.

Accensione del sensore

Vorremmo sapere quando il sensore di temperatura è acceso o spento.

Per ottenere questo, creeremo un nuovo servizio con UUID hardcoded:

La caratteristica che abbiamo definito non può essere monitorata attraverso FlutterBleLib, poiché manca isNotifiable: true, ma può essere, per comodità, monitorata a livello del BLEmulator. Questo rende più facile controllare il flusso complessivo che stiamo simulando, semplifica la struttura e ci permette di evitare inutili estensioni delle classi base.

Emissione di notifiche

Manca ancora l’emissione di notifiche dalla caratteristica IR Temperature Data. Occupiamocene.

_emitTemperature() è chiamato nel costruttore del servizio Temperatura e gira in un ciclo infinito. Ogni intervallo, specificato dal valore della caratteristica IR Temperature Period, controlla se c’è un ascoltatore (isNotifying). Se c’è, allora scrive i dati (zeri o un valore casuale, a seconda che il sensore sia acceso o spento) nella caratteristica IR Temperature Data. SimulatedCharacteristic.write() notifica qualsiasi ascoltatore attivo del nuovo valore.

Periferica avanzata in azione

Puoi trovare un esempio completo di una periferica più avanzata sul repository di BLEmulator. Se volete provarlo, basta clonare il repository ed eseguire il suo esempio.

Uso nei test automatici

Un grande ringraziamento, qui, al mio collega Polidean Paweł Byszewski per la sua ricerca sull’uso di BLEmulator nei test automatici.

Flutter ha un contesto di esecuzione diverso per l’app testata e il test stesso, il che significa che non si può semplicemente condividere una periferica simulata tra i due e modificare il comportamento del test. Quello che puoi fare è aggiungere un gestore di dati al driver di test usando enableFlutterDriverExtension(handler: DataHandler), passare le periferiche simulate al main() della tua app e passare messaggi di stringa al gestore all’interno del contesto di esecuzione dell’app.

Si riduce a:Wrapper per l’app

La tua periferica

Il tuo test

Grazie a questo meccanismo, puoi inizializzare la periferica come vuoi e chiamare qualsiasi comportamento che hai predefinito all’interno del tuo dispositivo simulato.

Guarda tu stesso

La cosa migliore di tutto questo è che non devi credermi! Controllate voi stessi su GitHub. Usalo, divertiti, prova a romperlo e facci sapere tutto!

Vuoi vederlo anche su altre piattaforme? Contattateci e faremo in modo che accada! Assicuratevi di controllare le nostre altre librerie, e non esitate a contattarci se volete che lavoriamo sul vostro progetto.