11.10. Optreden als central¶
De andere kant van het gesprek is de central – het apparaat dat scant op adverterende peripherals, er een kiest om mee te praten, een verbinding opent, de externe GATT-database doorloopt en kenmerken daarop leest of erop abonneert. Een camera die metingen verzamelt van een draagbare sensor, naar een baken luistert of met een begeleidende microcontroller praat, is een central.
Het central-patroon in aioble verloopt via vier fasen: scannen, verbinden, ontdekken, bedienen.
11.10.1. Scannen¶
aioble.scan() geeft een async context manager terug die tegelijk dienstdoet als een async iterator over ontdekte apparaten. Het typische gebruik is scannen tot een apparaat van belang verschijnt en dan uit de iteratie te breken:
import aioble
import asyncio
import bluetooth
HR_SERVICE = bluetooth.UUID(0x180D)
async def find_heart_rate():
async with aioble.scan(duration_ms=5000, active=True) as scanner:
async for result in scanner:
if HR_SERVICE in result.services():
return result.device
return None
duration_ms=5000 begrenst hoe lang de scan draait; duration_ms=0 scant eindeloos (tot de context manager wordt verlaten). active=True vraagt om scanresponsen, wat de payloadgrootte per apparaat verdubbelt ten koste van een kleine extra transmissie van beide kanten. De resterende keyword-argumenten interval_us / window_us stemmen de radio-duty-cycle van de scanner zelf af en worden zelden veranderd ten opzichte van de standaardwaarden.
Elke aioble.ScanResult stelt het apparaatadres, de laatste RSSI, de ruwe advertentie- en scanresponsbytes beschikbaar, en hulpmiddelen die de standaardvelden parseren:
result.device– eenaioble.Deviceklaar omconnect()op aan te roepen.result.rssi– ontvangen-signaalsterkte-indicator in dBm, nuttig voor “kies de dichtstbijzijnde”-logica.result.name()– de lokale-naamstring, ofNoneals die niet wordt geadverteerd.result.services()– een generator vanbluetooth.UUIDvoor elke service die het apparaat adverteert.result.manufacturer()– een generator van(company_id, data)-tuples voor de fabrikantspecifieke velden.result.connectable– of de meest recente advertentie er een was waarmee verbinding kon worden gemaakt.
Hetzelfde ScanResult wordt opnieuw teruggegeven naarmate er nieuwe advertentiedata binnenkomt voor hetzelfde apparaat, zodat een passieve luisteraar die alleen apparaten onbeperkt wil volgen de async iterator eindeloos kan laten draaien en op elke gebeurtenis kan reageren.
11.10.2. Verbinden¶
Zodra een doelapparaat is geïdentificeerd, is het openen van een verbinding één await
async def talk_to(device):
connection = await device.connect() # 10 s timeout
async with connection:
# ... do GATT work ...
pass
aioble.Device.connect() neemt timeout_ms (hoe lang te wachten tot de verbinding tot stand komt; standaard 10 s) en min_conn_interval_us / max_conn_interval_us (het gevraagde verbindingsintervalbereik uit Verbindingen).
11.10.2.1. Opnieuw verbinden met een bekende peer zonder te scannen¶
Zodra er een binding bestaat met een peer, is het adres al bekend en is een nieuwe scan-en-kies-ronde verspilde radiotijd. Construeer een aioble.Device rechtstreeks met het opgeslagen adres en spring direct door naar connect()
import aioble
KITCHEN_CAM = aioble.Device(aioble.ADDR_PUBLIC,
"aa:bb:cc:dd:ee:ff")
async def talk_to_kitchen():
async with await KITCHEN_CAM.connect() as connection:
# ... GATT work ...
pass
Het eerste argument is een van aioble.ADDR_PUBLIC (het fabrieksadres van een controller) of aioble.ADDR_RANDOM (een gegenereerd statisch of oplosbaar privéadres); het tweede is ofwel een zes-byte bytes-waarde of een hexstring met dubbelepunten als scheiding. De attributen addr_type en addr van elke Device (bijv. een die eerder uit een ScanResult is verkregen) kunnen worden bewaard en hier weer worden ingevoerd.
De teruggegeven aioble.DeviceConnection is waar de rest van het werk van de central aan hangt. async with zorgt ervoor dat de verbinding wordt gesloten wanneer het blok wordt verlaten – bij succes, bij annulering of bij elke uitzondering inclusief aioble.DeviceDisconnectedError doordat de peer wegvalt.
Als de central een grotere kenmerkwaarde nodig heeft dan de standaard 23-byte MTU toestaat, is dit de plek om die te onderhandelen:
await connection.exchange_mtu(512)
(exchange_mtu() geeft de daadwerkelijk onderhandelde MTU terug, wat het minimum is van de gevraagde waarde en wat de peer ondersteunt.)
11.10.3. Ontdekking¶
Ontdekking doorloopt de externe GATT-database om de services en kenmerken op hun UUID’s te vinden. Er zijn twee smaken: gericht (je kent de UUID en wilt één specifiek ding) en uitputtend (je wilt alles).
Gericht – het gangbare geval:
service = await connection.service(HR_SERVICE)
if service is None:
return # no such service
char = await service.characteristic(HR_MEASUREMENT)
if char is None:
return # no such characteristic
aioble.DeviceConnection.service() en aioble.ClientService.characteristic() nemen elk een bluetooth.UUID en geven het overeenkomende object terug (of None). Beide hebben een timeout_ms-keyword per ontdekking dat standaard 2 s is.
Uitputtend:
async for service in connection.services():
print("service:", service.uuid)
async for char in service.characteristics():
print(" characteristic:", char.uuid, "properties:", hex(char.properties))
Dit is wat generieke Bluetooth-verkenner-apps doen – nuttig voor ontwikkeling, minder voor productiecode die weet welke UUID’s hij verwacht.
11.10.3.1. Inspecteren wat een kenmerk ondersteunt¶
Ontdekking geeft de GATT-eigenschappenbitmask terug die de peer voor elk kenmerk adverteerde als properties. De bits zijn de GATT-gedefinieerde – read (0x02), write-without-response (0x04), write (0x08), notify (0x10), indicate (0x20) en consorten. Het inspecteren van de bitmask voordat een bewerking wordt uitgegeven, laat een generieke client zich aanpassen aan kenmerken waarvan hij de mogelijkheden niet van tevoren kent:
_PROP_READ = const(0x02)
_PROP_NOTIFY = const(0x10)
char = await service.characteristic(STATUS_UUID)
if char.properties & _PROP_NOTIFY:
await char.subscribe(notify=True)
value = await char.notified()
elif char.properties & _PROP_READ:
value = await char.read()
else:
value = None # nothing the client can do
Productiecode die het GATT-profiel van de peer al kent heeft dit meestal niet nodig – de UUID’s waren vooraf gedocumenteerd. Generieke / verkennende clients (een instellingenpagina die een onbekend apparaat doorloopt, een plugin-host) leunen erop.
11.10.4. Bedienen¶
Zodra de central een ClientCharacteristic vasthoudt, is elke GATT-bewerking één coroutine-aanroep:
Read. Geef een GATT-read uit en krijg de waarde terug:
value = await char.read() print("value:", value)
Lange reads (waarden groter dan de MTU) worden transparant afgehandeld.
Write. Stuur een nieuwe waarde naar de server:
await char.write(b"\\x01")response=Truewacht op een write-response en werptaioble.GattErrorop als de server de write afwijst.response=Falseis write-without-response: fire-and-forget.response=None(de standaard) kiest automatisch op basis van wat de peer adverteerde.Subscribe. Schakel notificaties of indicaties in door naar de CCCD van het kenmerk te schrijven:
await char.subscribe(notify=True)Nadat dit terugkeert, kan de central wachten op binnenkomende pushes.
Notified / indicated. Wacht op de volgende push van de server:
while True: data = await char.notified() print("push:", data)
timeout_ms=None(de standaard) wacht eindeloos; geef een geheel getal in milliseconden door om na een tijdje op te geven.
Het samenbrengen van de vier geeft het canonieke “verbinden, abonneren, streamen”-central-programma:
async def stream_heart_rate():
async with aioble.scan(duration_ms=5000, active=True) as scanner:
async for result in scanner:
if HR_SERVICE in result.services():
device = result.device
break
else:
return
async with await device.connect() as connection:
service = await connection.service(HR_SERVICE)
char = await service.characteristic(HR_MEASUREMENT)
await char.subscribe(notify=True)
while connection.is_connected():
data = await char.notified()
print("hr push:", data)
asyncio.run(stream_heart_rate())
Het geheel is ongeveer een dozijn regels en dekt de stroom van “geen Bluetooth draaiend” tot “live datastreaming”. De scan-iterator komt overeen met het broadcaster/observer-patroon, connect opent de GAP-verbinding, service / characteristic doorloopt de GATT-boom, subscribe schrijft de CCCD en notified wacht op pushes.
11.10.5. Verbroken verbindingen en heraansluiting¶
Alles wat er met de radioverbinding gebeurt, komt naar boven in de coroutine die erop wachtte. aioble.DeviceDisconnectedError is het signaal dat de peer wegviel of dat de supervisie-time-out vuurde; de uitzondering beëindigt welke read()-, write()- of notified()-aanroep ook onderweg was, en elk async with connection-blok wordt netjes verlaten.
Een central die bij verlies opnieuw moet verbinden, omhult het werk in zijn eigen buitenste loop:
async def keep_streaming():
while True:
try:
await stream_heart_rate()
except aioble.DeviceDisconnectedError:
print("disconnected, retrying...")
await asyncio.sleep(2)
11.10.5.1. Een reeks afbakenen met timeout()¶
Wanneer meerdere opeenvolgende GATT-bewerkingen allemaal binnen één budget moeten worden voltooid – niet elk afzonderlijk op zijn eigen timeout_ms – gebruik dan aioble.DeviceConnection.timeout() om ze te omhullen. De teruggegeven context manager annuleert zijn lichaam als het budget verstrijkt (waarbij asyncio.TimeoutError wordt opgeworpen) of als de peer de verbinding verbreekt (waarbij aioble.DeviceDisconnectedError wordt opgeworpen):
async with await device.connect() as connection:
try:
with connection.timeout(2000): # 2 s for the whole block
service = await connection.service(HR_SERVICE)
char = await service.characteristic(HR_MEASUREMENT)
await char.subscribe(notify=True)
except asyncio.TimeoutError:
print("discovery + subscribe took too long")
Dit is het schonere alternatief voor het afzonderlijk omhullen van elke aanroep in asyncio.wait_for() en vermijdt onechte successen waarbij elke aanroep zijn eigen deadline haalt maar de reeks als geheel de tijd overschrijdt. Het doorgeven van timeout_ms=None aan timeout() schakelt de deadline uit en laat alleen de disconnect-bewaking actief.