11.4. Advertising e scanning¶
Due dispositivi BLE che non si sono mai incontrati prima devono prima trovarsi a vicenda. Il networking risolve la cosa assegnando a ogni dispositivo un indirizzo da un pool condiviso e lasciando che ciascun lato raggiunga l’altro attraverso i router. Il BLE non ha router, non ha un pool condiviso e, tra la maggior parte delle coppie di dispositivi, nessuna relazione preesistente. Il Generic Access Profile (GAP) risolve invece la scoperta con un modello broadcast-e-ascolto. Un lato esegue l”advertising: trasmette un breve pacchetto sui tre canali di advertising a intervalli regolari, descrivendo chi è. L’altro lato esegue lo scanning: spazza gli stessi tre canali in ascolto di quei pacchetti.
Il GAP definisce quattro ruoli attorno a questo modello, ciascuno una combinazione specifica di advertising e ascolto.
11.4.1. I quattro ruoli GAP¶
I quattro ruoli GAP. L’asse verticale indica se il dispositivo esegue advertising; l’asse orizzontale indica se accetta (o avvia) connessioni.¶
Un peripheral esegue l’advertising di pacchetti che dicono «sono qui e puoi connetterti a me». Quando un altro dispositivo apre una connessione, il peripheral interrompe l’advertising e inizia a servire richieste GATT. Le fasce cardiache, i termometri e la maggior parte delle camere-come-sensori agiscono da peripheral.
Un central esegue lo scanning alla ricerca di peripheral, ne sceglie uno e avvia una connessione. Dopo essersi connesso parla GATT come client. Telefoni, laptop e camere che agiscono da raccoglitori di dati sono central.
Un broadcaster esegue l’advertising ma non accetta mai connessioni. Il suo payload di advertising è il dato: non c’è nulla a cui connettersi. Gli iBeacon e la maggior parte dei beacon di rilevamento presenza nei negozi sono broadcaster.
Un observer esegue lo scanning di quegli advertisement e ne legge il payload, anche in questo caso senza mai connettersi. Una camera che ascolta i beacon nelle vicinanze e agisce in base a ciò che sente è un observer.
Un singolo dispositivo può ricoprire più di un ruolo nello stesso momento: una camera può essere un peripheral che pubblica il proprio stato e un central che si connette a un sensore vicino. La radio multiplexa il lavoro.
11.4.2. Cosa contiene un pacchetto di advertising¶
Un pacchetto di advertising è piccolo: 31 byte di payload, o 62 se l’advertiser pubblica anche una scan response che gli scanner possono richiedere al volo. Il payload è un elenco di brevi campi tipizzati:
Flags. Connettibile o no, discoverable generale/limitato.
Local name. Una stringa breve e leggibile dall’uomo: il nome che il sistema operativo di un telefono o di un laptop mostra nel proprio menu Bluetooth.
Service UUID. Un elenco di identificatori di servizio GATT che il dispositivo ospita, così che uno scanner possa riconoscere i peripheral idonei senza connettersi prima. Una fascia cardiaca pubblicizza
0x180D– l’UUID standard del servizio Heart-Rate – e un’app per la frequenza cardiaca sul telefono sa solo da questo che vale la pena connettersi al dispositivo.Appearance. Un valore a 16 bit dall’elenco degli assigned-numbers Bluetooth (sensore, media generico, orologio generico, …): un suggerimento al central su cosa mostrare.
Dati specifici del produttore. Byte a forma libera preceduti da un company ID. Gli iBeacon usano questo campo per trasportare il proprio UUID, major e minor; le applicazioni personalizzate possono inserirvi tutto ciò che vogliono.
I payload di advertising sono compatti. Il limite di 31 byte rende la scelta di cosa includere una vera decisione progettuale: un nome leggibile lungo può rapidamente non lasciare spazio per i service UUID. L’API aioble.advertise() accetta ciascuno di questi come argomento keyword e assembla i byte per te, riversandoli automaticamente nella scan response se il pacchetto principale si riempie.
11.4.3. Scanning attivo e passivo¶
Uno scanner può funzionare in modalità passive, in cui ascolta i pacchetti di advertising e analizza ciò che arriva, oppure active, in cui invia anche una scan request a ciascun advertiser e analizza la scan response che torna indietro.
Lo scanning passivo vede solo il pacchetto di advertising iniziale (fino a 31 byte). Lo scanning attivo raddoppia tale valore: la scan response è altri 31 byte che il peripheral può usare per campi che non rientravano. Lo scanning attivo costa anche energia su entrambi i lati, poiché lo scanner trasmette e l’advertiser trasmette un pacchetto extra, quindi è una scelta anziché un’impostazione predefinita.
Nell’API aioble, active=True su aioble.scan() commuta la modalità, e ogni ScanResult espone gli adv_data combinati più i resp_data oltre a helper come result.name() e result.services() che nascondono il parsing a livello di byte.
Nota
Gli attributi adv_data e resp_data sono i payload grezzi di advertising e scan-response (bytes). Gli helper – name(), services(), manufacturer() – coprono i comuni campi standard e sono la scelta giusta nel 99% dei casi. Ricorri ai byte grezzi solo quando hai bisogno di un campo vendor che gli helper non analizzano (URL Eddystone, UUID/major/minor iBeacon, tipi di advertising personalizzati). Il layout dei byte è quello TLV standard: ogni campo è length, type, value....
11.4.4. L’intervallo di advertising¶
La frequenza con cui il peripheral trasmette è un compromesso tra consumo e latenza di scoperta. Gli advert che escono ogni 20 ms vengono captati quasi immediatamente da uno scanner ma tengono la radio occupata e scaricano la batteria; gli advert ogni secondo usano quasi nulla di energia ma rendono più lenta la scansione di uno scanner nel notare il dispositivo.
interval_us su aioble.advertise() imposta l’intervallo in microsecondi:
da 20.000 a 100.000 us (20 ms - 100 ms) – accoppiamento rapido, app che si aspetta una risposta veloce, dispositivo collegato all’alimentazione.
da 250.000 a 1.000.000 us (250 ms - 1 s) – un valore predefinito ragionevole per un peripheral alimentato a batteria che vuole essere rilevabile senza consumare carica.
Oltre 1.000.000 us – broadcast di sottofondo lento, beacon che inviano un aggiornamento di posizione ogni pochi secondi.
Il lato scanner ha le proprie manopole: aioble.scan() accetta interval_us e window_us (quanto spesso lo scanner sveglia la propria radio e per quanto tempo ascolta ogni volta). I valori predefiniti vanno bene; l’unica modifica comune è impostare entrambi uguali per una scansione continua quando la batteria non è un problema.
11.4.5. Modelli connectionless – broadcaster e observer¶
Le pagine Agire come periferica e Agire come central percorrono la forma connectable dell’API, in cui un peripheral accetta una connessione e i due lati si scambiano dati tramite GATT. L’altra forma è quella connectionless: un broadcaster trasmette il payload-come-advertisement, e qualsiasi observer a portata può leggerlo senza mai connettersi. Beacon, sensori di presenza e telemetria unidirezionale vivono tutti qui.
Un broadcaster è aioble.advertise() con connectable=False. I dati specifici del produttore trasportano il payload:
import aioble
import asyncio
import struct
_COMPANY_ID = const(0xFFFF) # 0xFFFF is "no specific vendor"
async def beacon():
seq = 0
while True:
seq = (seq + 1) & 0xFFFF
payload = struct.pack("<H", seq)
await aioble.advertise(
interval_us=500000,
connectable=False,
name="openmv-beacon",
manufacturer=(_COMPANY_ID, payload),
timeout_ms=1000, # one cycle, then loop
)
asyncio.run(beacon())
Il keyword timeout_ms termina la chiamata advertise dopo un secondo; il ciclo esterno la riemette con il numero di sequenza successivo, così che gli ascoltatori vedano dati freschi. Il flag connectable=False è ciò che rende l’advertisement in stile broadcaster: la camera non risponderà a una richiesta di connessione anche se ne arriva una.
Un observer è lo scanner di sola lettura corrispondente. Esegue aioble.scan() all’infinito, analizza gli advertisement in arrivo e non chiama mai connect()
import aioble
import asyncio
_COMPANY_ID = const(0xFFFF)
async def watch():
async with aioble.scan(duration_ms=0, active=False) as scanner:
async for result in scanner:
for company, data in result.manufacturer(filter=_COMPANY_ID):
print(result.device.addr_hex(),
"rssi", result.rssi, "data", data)
asyncio.run(watch())
duration_ms=0 esegue la scansione finché non esce il context manager; active=False mantiene silenziosa la radio dell’observer stesso (nessuna richiesta di scan-response) per il consumo più basso. L’argomento filter= su manufacturer() scarta ogni advertisement che non corrisponde al company ID, così il ciclo si attiva solo per il traffico del broadcaster.
11.4.6. Dalla scoperta a una connessione¶
Una volta che un central sceglie un peripheral con cui dialogare, smette di ascoltare, invia una connect request sul canale di advertising usato per ultimo dal peripheral, ed entrambi i lati passano ai canali dati con hopping del link layer. A questo punto il peripheral interrompe tipicamente l’advertising. Ciò che accade dopo – parametri di connessione, scoperta GATT, durata di vita del collegamento – è in Connessioni.