11.4. Adverteren en scannen¶
Twee BLE-apparaten die elkaar nog nooit hebben ontmoet, moeten elkaar eerst vinden. Netwerken lost dat op door elk apparaat een adres uit een gedeelde pool te geven en beide kanten elkaar via routers te laten bereiken. BLE heeft geen routers, geen gedeelde pool en – tussen de meeste apparaatparen – helemaal geen eerdere relatie. Het Generic Access Profile (GAP) lost ontdekking in plaats daarvan op met een patroon van broadcasten-en-luisteren. De ene kant adverteert – hij verzendt met een regelmatig interval een kort pakket op de drie advertentiekanalen waarin hij beschrijft wie hij is. De andere kant scant – hij doorzoekt diezelfde drie kanalen op zoek naar die pakketten.
GAP definieert vier rollen rond dat patroon, elk een specifieke combinatie van adverteren en luisteren.
11.4.1. De vier GAP-rollen¶
De vier GAP-rollen. De verticale as geeft aan of het apparaat adverteert; de horizontale as geeft aan of het verbindingen accepteert (of initieert).¶
Een peripheral adverteert pakketten die zeggen “ik ben hier en je kunt met me verbinden”. Wanneer een ander apparaat een verbinding opent, stopt de peripheral met adverteren en begint hij GATT-verzoeken te bedienen. Hartslagbanden, thermometers en de meeste camera’s-als-sensoren gedragen zich als peripherals.
Een central scant naar peripherals, kiest er een en initieert een verbinding. Na het verbinden spreekt hij GATT als client. Telefoons, laptops en camera’s die als gegevensverzamelaars optreden zijn centrals.
Een broadcaster adverteert maar accepteert nooit verbindingen. De advertentie-payload is de data – er valt niets te verbinden. iBeacons en de meeste aanwezigheidsbeacons in winkels zijn broadcasters.
Een observer scant naar die advertenties en leest de payload, opnieuw zonder ooit te verbinden. Een camera die luistert naar beacons in de buurt en handelt naar wat hij hoort, is een observer.
Een enkel apparaat kan tegelijkertijd meer dan één rol spelen – een camera kan een peripheral zijn die zijn eigen toestand publiceert en een central die verbinding maakt met een nabije sensor. De radio multiplext het werk.
11.4.2. Wat een advertentiepakket bevat¶
Een advertentiepakket is klein: 31 bytes payload, of 62 als de adverteerder ook een scan response publiceert die scanners ter plekke kunnen opvragen. De payload is een lijst van korte getypeerde velden:
Flags. Verbindbaar of niet, general / limited discoverable.
Lokale naam. Een korte, voor mensen leesbare tekenreeks – de naam die het besturingssysteem op een telefoon of laptop in zijn Bluetooth-menu toont.
Service-UUID’s. Een lijst van GATT-service-identifiers die het apparaat host, zodat een scanner geschikte peripherals kan herkennen zonder eerst te verbinden. Een hartslagband adverteert
0x180D– de standaard UUID van de Heart-Rate-service – en een hartslag-app op de telefoon weet alleen daaruit al dat het apparaat de moeite van het verbinden waard is.Appearance. Een 16-bits waarde uit de lijst met toegewezen Bluetooth-nummers (sensor, generieke media, generiek horloge, …) – een hint aan de central over wat hij moet weergeven.
Fabrikantspecifieke data. Vrij in te delen bytes voorafgegaan door een bedrijfs-ID. iBeacons gebruiken dit veld om hun UUID, major en minor mee te dragen; aangepaste toepassingen kunnen hier zetten wat ze maar willen.
Advertentie-payloads zitten krap. De limiet van 31 bytes maakt de keuze van wat je opneemt tot een echte ontwerpbeslissing – een lange, voor mensen leesbare naam kan al snel geen ruimte meer overlaten voor service-UUID’s. De aioble.advertise()-API neemt elk van deze als trefwoordargument en assembleert de bytes voor je, waarbij hij automatisch overloopt in de scan response als het hoofdpakket vol raakt.
11.4.3. Actief en passief scannen¶
Een scanner kan passief draaien, waarbij hij luistert naar advertentiepakketten en parseert wat binnenkomt, of actief, waarbij hij ook een scan request naar elke adverteerder stuurt en de scan response parseert die terugkomt.
Passief scannen ziet alleen het initiële advertentiepakket (tot 31 bytes). Actief scannen verdubbelt dat – de scan response zijn nog eens 31 bytes die de peripheral kan gebruiken voor velden die niet pasten. Actief scannen kost ook stroom aan beide kanten, omdat de scanner zendt en de adverteerder een extra pakket zendt, dus het is een keuze in plaats van een standaardinstelling.
In de aioble-API schakelt active=True op aioble.scan() de modus om, en elk ScanResult stelt de gecombineerde adv_data plus resp_data beschikbaar, evenals helpers zoals result.name() en result.services() die het parseren op byteniveau verbergen.
Notitie
De attributen adv_data en resp_data zijn de ruwe advertentie- en scan-response-payloads (bytes). De helpers – name(), services(), manufacturer() – dekken de gangbare standaardvelden en zijn 99% van de tijd de juiste keuze. Grijp alleen naar de ruwe bytes wanneer je een leveranciersveld nodig hebt dat de helpers niet parseren (Eddystone-URL’s, iBeacon-UUID/major/minor, aangepaste advertentietypes). De byte-indeling is de standaard TLV-indeling: elk veld is length, type, value....
11.4.4. Het advertentie-interval¶
Hoe vaak de peripheral broadcast is een afweging tussen stroomverbruik en ontdekkingslatentie. Advertenties die elke 20 ms uitgaan worden vrijwel onmiddellijk door een scanner opgepikt, maar houden de radio bezig en putten de batterij uit; advertenties elke seconde gebruiken vrijwel geen stroom, maar zorgen ervoor dat een scanner het apparaat trager opmerkt.
interval_us op aioble.advertise() stelt het interval in microseconden in:
20.000 tot 100.000 us (20 ms - 100 ms) – snel koppelen, app verwacht een snelle reactie, apparaat met netvoeding.
250.000 tot 1.000.000 us (250 ms - 1 s) – een redelijke standaard voor een peripheral op batterijen die vindbaar wil zijn zonder lading te verbranden.
Boven 1.000.000 us – langzame achtergrondbroadcast, beacons die om de paar seconden een positie-update sturen.
De scannerkant heeft zijn eigen knoppen – aioble.scan() neemt interval_us en window_us (hoe vaak de scanner zijn radio ontwaakt en hoe lang hij telkens luistert). De standaardwaarden zijn prima; de enige gangbare wijziging is beide gelijk instellen voor een continue scan wanneer batterij geen probleem is.
11.4.5. Verbindingsloze patronen – broadcaster en observer¶
De pagina’s over Optreden als randapparaat en Optreden als central werken de verbindbare vorm van de API uit – waarbij een peripheral een verbinding accepteert en de twee kanten gegevens uitwisselen via GATT. De andere vorm is verbindingsloos: een broadcaster verzendt payload-als-advertentie, en elke observer binnen bereik kan die lezen zonder ooit te verbinden. Beacons, aanwezigheidssensoren en eenrichtingstelemetrie horen hier allemaal thuis.
Een broadcaster is aioble.advertise() met connectable=False. Fabrikantspecifieke data draagt de 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())
Het trefwoord timeout_ms beëindigt de advertise-aanroep na een seconde; de buitenste lus geeft hem opnieuw uit met het volgende volgnummer, zodat luisteraars verse data zien. De vlag connectable=False is wat de advertentie broadcaster-stijl maakt – de cam reageert niet op een verbindingsverzoek, zelfs niet als er een binnenkomt.
Een observer is de bijbehorende alleen-lezen scanner. Hij draait aioble.scan() eeuwig door, parseert binnenkomende advertenties en roept nooit connect() aan:
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 scant totdat de contextmanager afsluit; active=False houdt de eigen radio van de observer stil (geen scan-response-verzoeken) voor het laagste stroomverbruik. Het argument filter= op manufacturer() verwerpt elke advertentie die niet overeenkomt met de bedrijfs-ID, zodat de lus alleen afgaat voor het verkeer van de broadcaster.
11.4.6. Van ontdekking naar een verbinding¶
Zodra een central een peripheral kiest om mee te praten, stopt hij met luisteren, stuurt hij een connect request op het advertentiekanaal dat de peripheral het laatst gebruikte, en vallen beide kanten in de springende datakanalen van de link layer. De peripheral stopt op dit punt doorgaans met adverteren. Wat er daarna gebeurt – verbindingsparameters, GATT-ontdekking, de levensduur van de verbinding – staat op Verbindingen.