11.11. L2CAP-kanalen¶
GATT is een sleutel/waarde-model. De bewerkingen die het biedt (read, write, notify, indicate) verplaatsen telkens één korte waarde, en de grootste enkele payload die ze kunnen dragen is wat de onderhandelde MTU toestaat – hooguit een paar honderd bytes. Dat werkt prima voor sensoraflezingen, commandoregisters en statusvlaggen. Het loopt vast op kilobytes of megabytes: een lange blob opsplitsen in honderden kleine writes kost round trips waar de radio veel sneller in is.
Voor bulkdatastromen – een vastgelegd frame dat de camera naar een telefoon streamt, een over-the-air-update-image, een gebundelde export van metingen – biedt BLE een alternatief pad: het Logical Link Control and Adaptation Protocol, L2CAP. L2CAP zit tussen de linklaag en GATT en laat een toepassing zijn eigen verbindingsgericht kanaal claimen bovenop dezelfde radioverbinding. Het kanaal is een door creditflow gecontroleerd bytepad met een veel grotere MTU per pakket en geen GATT-framing daartussen.
11.11.1. Wanneer L2CAP te gebruiken¶
L2CAP-kanalen zijn het juiste gereedschap wanneer:
De overdracht meer dan een paar honderd bytes is.
Beide uiteinden weten dat er een L2CAP-kanaal gebruikt zal worden (het wordt niet blootgesteld in de advertentie-payload; de client moet het protocol/service multiplexer-nummer, oftewel PSM, van het kanaal buiten de band kennen).
De toepassing bereid is de gemakken van GATT op te geven: geen ingebouwde adresseerbaarheid via UUID, geen client-ontdekbaarheid via standaard-apps, geen notificaties.
Het meest voorkomende geval in op aioble gebaseerde toepassingen is het verplaatsen van een binaire blob tussen twee stukken software die beide de PSM-conventie kennen – een aangepast camera-naar-telefoonprotocol, een paar openmv-camera’s die met elkaar praten, een intern firmware-updatepad onder de GATT-service van een peripheral.
Blijf voor al het overige bij GATT. Een korte status, een controleregister, een sensoraflezing – die horen allemaal thuis in een characteristic.
11.11.2. Een kanaal opzetten¶
L2CAP draait bovenop een bestaande aioble.DeviceConnection, dus de GAP-zijde van discovery / advertising / verbinden verloopt precies hetzelfde als bij GATT. Zodra beide zijden een verbinding vasthouden, luistert de ene zijde op een PSM en maakt de andere zijde er verbinding mee.
De PSM is gewoon een klein geheel getal. De Bluetooth SIG reserveert het onderste deel van het bereik voor gestandaardiseerd gebruik (0x0001-0x007F); gebruik voor toepassingsspecifieke kanalen een nummer uit het dynamische bereik (0x0080-0x00FF voor vaste PSM’s, vanaf 0x0040 doorgaans vrij voor aangepast gebruik). Beide zijden moeten het van tevoren eens zijn over de waarde.
De MTU op een L2CAP-kanaal is de grootste enkele SDU (Service Data Unit) die een van beide zijden in één send() zal afleveren – niet de MTU van de BLE-link. Aioble fragmenteert grotere payloads automatisch. De BLE-host van de camera beperkt de L2CAP-MTU tot 1017 bytes; 512 is een verstandige standaard die aan beide zijden ruimte laat zonder RAM te verbranden.
Aan de luisterzijde (bijv. de camera als peripheral):
async def serve_l2cap(connection, image_bytes):
channel = await connection.l2cap_accept(psm=0x80, mtu=512)
async with channel:
# image_bytes is a bytearray -- e.g. csi0.snapshot().bytearray()
# or a compressed JPEG buffer. send() fragments into MTU-sized
# chunks automatically and awaits flow-control credits between.
await channel.send(image_bytes)
await channel.flush()
Aan de verbindende zijde (bijv. een telefoon of central):
async def open_l2cap(connection, total_bytes):
channel = await connection.l2cap_connect(psm=0x80, mtu=512)
async with channel:
image_bytes = bytearray(total_bytes)
view = memoryview(image_bytes)
received = 0
while received < total_bytes:
n = await channel.recvinto(view[received:])
if n == 0:
break
received += n
return image_bytes
l2cap_accept() blokkeert totdat de peer verbinding maakt (of timeout_ms afgaat); l2cap_connect() blokkeert totdat de luisteraar accepteert (of faalt). Beide retourneren een aioble.L2CAPChannel – zelf een async-contextmanager die het kanaal bij het verlaten sluit.
11.11.3. Verzenden en ontvangen¶
De twee belangrijkste bewerkingen op een kanaal zijn send() (schrijft bytes naar de peer) en recvinto() (leest in een vooraf toegewezen buffer). Beide zijn coroutines.
send()fragmenteert de buffer in stukken ter grootte van de MTU en wacht tussen de stukken op flow-control-credits van de linklaag. Een lange send is vanuit het perspectief van de toepassing éénawait; intern kan het veel pakketten in de wachtrij zetten en pauzeren zodra de ontvangstcredits van de peer op zijn.recvinto()vult de doorgegeven buffer met wat beschikbaar is (tot aan de MTU van het kanaal) en retourneert het aantal bytes. Wacht als er niets beschikbaar is.available()retourneert synchroonTrueals er gebufferde gegevens klaarstaan – handig om te pollen zonder op te schorten.flush()wacht totdat een uitstaande send volledig naar de controller is verzonden.
L2CAP-kanalen zijn streamachtig in de zin dat de bytes in volgorde en zonder verlies aankomen, maar de grenzen van een enkele send blijven behouden – elke SDU komt uit één enkele recvinto. Dat is anders dan bij TCP, waar de grenzen van één send() kunnen uitsmeren over meerdere recv()-aanroepen.
11.11.4. Afhandeling van verbreking¶
Het kanaal verdwijnt onder drie voorwaarden: een van beide zijden roept disconnect() aan, de onderliggende GAP-verbinding valt weg, of de verbreking op L2CAP-niveau arriveert. Actieve bewerkingen werpen aioble.L2CAPDisconnectedError. Net als aan de GATT-zijde komt dit naar boven als een exception in de coroutine die stond te wachten, en het async with channel-blok wordt netjes verlaten.
Als een kanaal onbereikbaar wordt door een verbreking op GAP-niveau, keert de toepassing terug naar adverteren of scannen op dezelfde manier als bij een GATT-verbreking.
11.11.5. Geheugenkosten¶
Grotere MTU’s en langere wachtrijen gebruiken meer RAM aan beide zijden. Een MTU van 512 bytes plus een ontvangstbuffer per kanaal is ongeveer 1 KB per kanaal – niet gratis op een kleine camera als er meerdere kanalen tegelijk open zijn. Houd het bij één kanaal per verbinding en kies een MTU die past bij de verwachte berichtgrootte; de standaard van één L2CAPChannel per DeviceConnection is voldoende voor de meeste toepassingen.
L2CAP is de veiligheidsklep van BLE. GATT is waar bijna elke toepassing eerst naar grijpt, en de rest van de central- / peripheral-voorbeelden in deze sectie blijven bij GATT. De kanaalgerichte API is het antwoord wanneer een toepassing het sleutel/waarde-model ontgroeit.