11.10. Att agera som en central¶
Den andra sidan av konversationen är centralen – enheten som skannar efter annonserande kringutrustningar, väljer en att tala med, öppnar en anslutning, går igenom den fjärranslutna GATT-databasen och läser eller prenumererar på karaktäristiker på den. En kamera som samlar in avläsningar från en bärbar sensor, lyssnar på en beacon eller talar med en följeslagar-mikrokontroller är en central.
Centralmönstret i aioble går igenom fyra steg: skanna, ansluta, upptäcka, operera.
11.10.1. Skanning¶
aioble.scan() returnerar en async-kontexthanterare som även fungerar som en async-iterator över upptäckta enheter. Den typiska användningen är att skanna tills en intressant enhet dyker upp och sedan bryta sig ut ur iterationen:
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 begränsar hur länge skanningen körs; duration_ms=0 skannar för evigt (tills kontexthanteraren avslutas). active=True begär skanningssvar, vilket fördubblar nyttolaststorleken per enhet till priset av en liten ytterligare överföring från båda sidor. De återstående nyckelordsargumenten interval_us / window_us justerar skannerns egen radio-arbetscykel och ändras sällan från standardvärdena.
Varje aioble.ScanResult exponerar enhetens adress, den senaste RSSI, de råa annonserings- och skanningssvarsbytena, samt hjälpfunktioner som tolkar standardfälten:
result.device– enaioble.Deviceredo att anropaconnect()på.result.rssi– indikator för mottagen signalstyrka i dBm, användbar för logik som ”välj den närmaste”.result.name()– strängen med det lokala namnet, ellerNoneom det inte annonseras.result.services()– en generator avbluetooth.UUIDför varje tjänst som enheten annonserar.result.manufacturer()– en generator av tupler(company_id, data)för de tillverkarspecifika fälten.result.connectable– huruvida den senaste annonseringen var anslutningsbar.
Samma ScanResult återlämnas allteftersom nya annonseringsdata anländer för samma enhet, så en passiv lyssnare som bara vill spåra enheter på obestämd tid kan köra async-iteratorn för evigt och dispatcha på varje händelse.
11.10.2. Anslutning¶
När en målenhet väl är identifierad är det att öppna en anslutning ett enda await
async def talk_to(device):
connection = await device.connect() # 10 s timeout
async with connection:
# ... do GATT work ...
pass
aioble.Device.connect() tar timeout_ms (hur länge man ska vänta på att anslutningen kommer upp; standard 10 s), och min_conn_interval_us / max_conn_interval_us (det begärda intervallet för anslutningsintervall från Anslutningar).
11.10.2.1. Återanslutning till en känd peer utan skanning¶
När en bindning väl finns med en peer är adressen redan känd och en ny skanna-och-välj-runda är bortkastad radiotid. Konstruera en aioble.Device direkt med den sparade adressen och hoppa direkt till 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
Det första argumentet är ett av aioble.ADDR_PUBLIC (en styrenhets fabriksadress) eller aioble.ADDR_RANDOM (en genererad statisk eller upplösbar privat adress); det andra är antingen ett sex byte stort bytes-värde eller en kolonseparerad hex-sträng. Attributen addr_type och addr för vilken Device som helst (t.ex. en som erhållits tidigare från ett ScanResult) kan sparas och matas tillbaka in här.
Den returnerade aioble.DeviceConnection är det som resten av centralens arbete hänger på. async with säkerställer att anslutningen stängs när blocket avslutas – vid framgång, vid avbrott, eller vid vilket undantag som helst inklusive aioble.DeviceDisconnectedError från att peeren försvinner.
Om centralen behöver ett större karaktäristikvärde än den förvalda MTU:n på 23 byte tillåter, är det här man förhandlar om det:
await connection.exchange_mtu(512)
(exchange_mtu() returnerar den faktiskt förhandlade MTU:n, vilket är minimum av det begärda värdet och vad peeren stöder.)
11.10.3. Upptäckt¶
Upptäckt går igenom den fjärranslutna GATT-databasen för att hitta tjänsterna och karaktäristikerna via deras UUID:er. Det finns två varianter: riktad (du känner till UUID:n och vill ha en specifik sak) och uttömmande (du vill ha allt).
Riktad – det vanliga fallet:
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() och aioble.ClientService.characteristic() tar var och en en bluetooth.UUID och returnerar det matchande objektet (eller None). Båda har ett timeout_ms-nyckelord per upptäckt som har standardvärdet 2 s.
Uttömmande:
async for service in connection.services():
print("service:", service.uuid)
async for char in service.characteristics():
print(" characteristic:", char.uuid, "properties:", hex(char.properties))
Det här är vad generiska Bluetooth-utforskar-appar gör – användbart för utveckling, mindre så för produktionskod som vet vilka UUID:er den förväntar sig.
11.10.3.1. Granska vad en karaktäristik stöder¶
Upptäckt returnerar den GATT-egenskapsbitmask som peeren annonserade för varje karaktäristik som properties. Bitarna är de GATT-definierade – läs (0x02), skriv-utan-svar (0x04), skriv (0x08), notify (0x10), indicate (0x20) och vänner. Att granska bitmasken innan en operation utfärdas låter en generisk klient anpassa sig till karaktäristiker vars förmågor den inte känner till i förväg:
_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
Produktionskod som redan känner till peerens GATT-profil behöver vanligtvis inte detta – UUID:erna dokumenterades i förväg. Generiska / utforskande klienter (en inställningssida som går igenom en okänd enhet, en plugin-värd) lutar sig mot det.
11.10.4. Operering¶
När centralen väl håller en ClientCharacteristic är varje GATT-operation ett enda coroutineanrop:
Läs. Utfärda en GATT-läsning och få tillbaka värdet:
value = await char.read() print("value:", value)
Långa läsningar (värden större än MTU:n) hanteras transparent.
Skriv. Skicka ett nytt värde till servern:
await char.write(b"\\x01")response=Trueväntar på ett skrivsvar och kastaraioble.GattErrorom servern avvisar skrivningen.response=Falseär skriv-utan-svar: fire-and-forget.response=None(standardvärdet) väljer automatiskt baserat på vad peeren annonserade.Prenumerera. Aktivera notifieringar eller indikeringar genom att skriva till karaktäristikens CCCD:
await char.subscribe(notify=True)Efter att detta returnerar kan centralen vänta på inkommande pushar.
Notifierad / indikerad. Vänta på nästa push från servern:
while True: data = await char.notified() print("push:", data)
timeout_ms=None(standardvärdet) väntar för evigt; skicka ett heltal i millisekunder för att ge upp efter ett tag.
Att sätta ihop de fyra ger det kanoniska centralprogrammet ”anslut, prenumerera, strömma”:
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())
Det hela är ungefär ett dussin rader och täcker flödet från ”ingen Bluetooth körs” till ”livedata strömmas”. Skanningsiteratorn matchar broadcaster/observer-mönstret, connect öppnar GAP-anslutningen, service / characteristic går igenom GATT-trädet, subscribe skriver CCCD:n, och notified väntar på pushar.
11.10.5. Frånkopplingar och återanslutning¶
Allt som händer med radiolänken dyker upp i den coroutine som väntade på den. aioble.DeviceDisconnectedError är signalen att peeren försvann eller att övervakningstimeouten utlöstes; undantaget avslutar vilket read()-, write()- eller notified()-anrop som var pågående, och vilket async with connection-block som helst avslutas rent.
En central som ska återansluta vid förlust slår in arbetet i sin egen yttre 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. Att rama in en sekvens med timeout()¶
När flera GATT-operationer i rad alla bör slutföras inom en budget – inte var och en individuellt på sin egen timeout_ms – använd aioble.DeviceConnection.timeout() för att slå in dem. Den returnerade kontexthanteraren avbryter sin kropp om budgeten löper ut (kastar asyncio.TimeoutError) eller om peeren kopplar från (kastar aioble.DeviceDisconnectedError):
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")
Detta är det renare alternativet till att slå in varje anrop individuellt i asyncio.wait_for() och undviker falska framgångar där varje anrop möter sin egen tidsfrist men sekvensen som helhet drar över. Att skicka timeout_ms=None till timeout() inaktiverar tidsfristen och lämnar endast frånkopplingsskyddet aktivt.