11.4. Difuzare și scanare¶
Două dispozitive BLE care nu s-au mai întâlnit niciodată trebuie mai întâi să se găsească reciproc. Rețeaua rezolvă acest lucru atribuind fiecărui dispozitiv o adresă dintr-un fond comun și permițând fiecărei părți să o contacteze pe cealaltă prin routere. BLE nu are routere, nu are fond comun și – între majoritatea perechilor de dispozitive – nu are deloc o relație prealabilă. Generic Access Profile (GAP) rezolvă descoperirea printr-un tipar de tip difuzare-și-ascultare. O parte difuzează – transmite un pachet scurt pe cele trei canale de difuzare la un interval regulat, descriind cine este. Cealaltă parte scanează – baleiază aceleași trei canale ascultând acele pachete.
GAP definește patru roluri în jurul acestui tipar, fiecare o combinație specifică de difuzare și ascultare.
11.4.1. Cele patru roluri GAP¶
Cele patru roluri GAP. Axa verticală arată dacă dispozitivul difuzează; axa orizontală arată dacă acceptă (sau inițiază) conexiuni.¶
Un peripheral difuzează pachete care spun „sunt aici și te poți conecta la mine”. Când un alt dispozitiv deschide o conexiune, peripheral-ul oprește difuzarea și începe să deservească cereri GATT. Benzile pentru ritmul cardiac, termometrele și majoritatea camerelor-ca-senzori acționează ca peripheral.
Un central scanează după peripheral-uri, alege unul și inițiază o conexiune. După conectare, vorbește GATT ca un client. Telefoanele, laptopurile și camerele care acționează ca colectoare de date sunt central-uri.
Un broadcaster difuzează, dar nu acceptă niciodată conexiuni. Sarcina sa utilă de difuzare este datele – nu există nimic la care să te conectezi. iBeacon-urile și majoritatea beacon-urilor de prezență în magazine sunt broadcaster-e.
Un observer scanează acele difuzări și citește sarcina utilă, din nou fără a se conecta vreodată. O cameră care ascultă beacon-urile din apropiere și acționează în funcție de ce aude este un observer.
Un singur dispozitiv poate juca mai multe roluri în același timp – o cameră poate fi un peripheral care își publică propria stare și un central care se conectează la un senzor din apropiere. Radioul multiplexează activitatea.
11.4.2. Ce conține un pachet de difuzare¶
Un pachet de difuzare este mic: 31 de octeți de sarcină utilă sau 62 dacă difuzorul publică și un scan response pe care scanerele îl pot solicita din mers. Sarcina utilă este o listă de câmpuri scurte și tipizate:
Flags. Conectabil sau nu, descoperibil general / limitat.
Numele local. Un șir scurt, prietenos pentru oameni – numele pe care sistemul de operare al unui telefon sau laptop îl afișează în meniul său Bluetooth.
UUID-uri de serviciu. O listă de identificatori de servicii GATT pe care dispozitivul le găzduiește, astfel încât un scaner să poată recunoaște peripheral-urile capabile fără a se conecta mai întâi. O bandă pentru ritmul cardiac difuzează
0x180D– UUID-ul standard al serviciului Heart-Rate – iar o aplicație de ritm cardiac de pe telefon știe doar din asta că dispozitivul merită conectat.Appearance. O valoare pe 16 biți din lista de numere atribuite Bluetooth (senzor, media generic, ceas generic, …) – un indiciu pentru central despre ce să afișeze.
Date specifice producătorului. Octeți cu format liber, prefixați cu un ID de companie. iBeacon-urile folosesc acest câmp pentru a transporta UUID-ul, major și minor; aplicațiile personalizate pot pune aici orice doresc.
Sarcinile utile de difuzare sunt strânse. Limita de 31 de octeți face ca alegerea a ceea ce să incluzi să fie o decizie reală de proiectare – un nume lung, lizibil pentru oameni, poate rămâne rapid fără loc pentru UUID-urile de serviciu. API-ul aioble.advertise() preia fiecare dintre acestea ca argument cu cuvânt-cheie și asamblează octeții pentru tine, revărsându-se automat în scan response dacă pachetul principal se umple.
11.4.3. Scanare activă și pasivă¶
Un scaner poate rula pasiv, unde ascultă pachetele de difuzare și analizează ce sosește, sau activ, unde trimite de asemenea o scan request fiecărui difuzor și analizează scan response-ul care vine înapoi.
Scanarea pasivă vede doar pachetul de difuzare inițial (până la 31 de octeți). Scanarea activă dublează acest lucru – scan response-ul este încă 31 de octeți pe care peripheral-ul îi poate folosi pentru câmpuri care nu au încăput. Scanarea activă costă de asemenea energie pe ambele părți, întrucât scanerul transmite, iar difuzorul transmite un pachet suplimentar, deci este o alegere, nu o valoare implicită.
În API-ul aioble, active=True pe aioble.scan() comută modul, iar fiecare ScanResult expune combinația adv_data plus resp_data, precum și utilitare precum result.name() și result.services() care ascund analiza la nivel de octet.
Notă
Atributele adv_data și resp_data sunt sarcinile utile brute de difuzare și scan response (bytes). Utilitarele – name(), services(), manufacturer() – acoperă câmpurile standard uzuale și sunt alegerea potrivită în 99% din cazuri. Recurge la octeții bruți doar când ai nevoie de un câmp de la furnizor pe care utilitarele nu îl analizează (URL-uri Eddystone, UUID/major/minor de iBeacon, tipuri personalizate de difuzare). Aranjamentul octeților este cel standard TLV: fiecare câmp este length, type, value....
11.4.4. Intervalul de difuzare¶
Cât de des difuzează peripheral-ul este un compromis între energie și latența de descoperire. Difuzările trimise la fiecare 20 ms sunt preluate aproape imediat de un scaner, dar mențin radioul ocupat și consumă bateria; difuzările la fiecare secundă folosesc aproape deloc energie, dar fac ca un scaner să observe dispozitivul mai lent în baleiajul său.
interval_us pe aioble.advertise() setează intervalul în microsecunde:
20.000 - 100.000 us (20 ms - 100 ms) – asociere rapidă, aplicația așteaptă un răspuns prompt, dispozitiv conectat la priză.
250.000 - 1.000.000 us (250 ms - 1 s) – o valoare implicită rezonabilă pentru un peripheral alimentat de la baterie, care vrea să fie descoperibil fără a consuma încărcarea.
Peste 1.000.000 us – difuzare lentă în fundal, beacon-uri care trimit o actualizare de poziție la fiecare câteva secunde.
Partea scanerului are propriile sale reglaje – aioble.scan() preia interval_us și window_us (cât de des își trezește scanerul radioul și cât timp ascultă de fiecare dată). Valorile implicite sunt potrivite; singura modificare uzuală este setarea ambelor la valori egale pentru o scanare continuă atunci când bateria nu este o preocupare.
11.4.5. Tipare fără conexiune – broadcaster și observer¶
Paginile despre Funcționarea ca periferic și Acționând ca central parcurg forma conectabilă a API-ului – unde un peripheral acceptă o conexiune și cele două părți fac schimb de date prin GATT. Cealaltă formă este fără conexiune: un broadcaster transmite sarcina utilă ca difuzare, iar orice observer în rază o poate citi fără a se conecta vreodată. Beacon-urile, senzorii de prezență și telemetria unidirecțională se află toate aici.
Un broadcaster este aioble.advertise() cu connectable=False. Datele specifice producătorului transportă sarcina utilă:
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())
Cuvântul-cheie timeout_ms încheie apelul de difuzare după o secundă; bucla exterioară îl reemite cu următorul număr de secvență, astfel încât ascultătorii să vadă date proaspete. Flag-ul connectable=False este cel care face ca difuzarea să fie de tip broadcaster – camera nu va răspunde la o cerere de conectare nici dacă sosește vreuna.
Un observer este scanerul corespunzător, doar pentru citire. Rulează aioble.scan() la nesfârșit, analizează difuzările sosite și nu apelează niciodată 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 scanează până când managerul de context se închide; active=False păstrează tăcut propriul radio al observer-ului (fără cereri scan-response) pentru cel mai mic consum de energie. Argumentul filter= de pe manufacturer() elimină fiecare difuzare care nu se potrivește cu ID-ul companiei, astfel încât bucla se declanșează doar pentru traficul broadcaster-ului.
11.4.6. De la descoperire la o conexiune¶
Odată ce un central alege un peripheral cu care să comunice, oprește ascultarea, trimite o cerere de conectare pe canalul de difuzare folosit ultima dată de peripheral, iar ambele părți trec în canalele de date cu salt de frecvență ale stratului de legătură. Peripheral-ul oprește de obicei difuzarea în acest moment. Ce se întâmplă în continuare – parametrii conexiunii, descoperirea GATT, durata de viață a legăturii – se află în Conexiuni.