11.11. L2CAP-Kanäle¶
GATT ist ein Schlüssel/Wert-Modell. Die Operationen, die es anbietet (read, write, notify, indicate), bewegen jeweils einen kurzen Wert, und die größte einzelne Nutzlast, die sie tragen können, ist das, was die ausgehandelte MTU erlaubt – bestenfalls ein paar hundert Bytes. Das funktioniert gut für Sensorwerte, Befehlsregister und Statusflags. Bei Kilobytes oder Megabytes fällt es auseinander: Einen langen Blob in Hunderte kleiner Writes aufzuteilen, kostet Roundtrips, gegenüber denen das Funkmodul viel schneller ist.
Für Massendatenflüsse – ein erfasstes Einzelbild, das die Kamera an ein Telefon streamt, ein Over-the-Air-Update-Image, ein gebündelter Export von Messungen – bietet BLE einen alternativen Weg: das Logical Link Control and Adaptation Protocol, L2CAP. L2CAP liegt zwischen der Link-Schicht und GATT und erlaubt es einer Anwendung, einen eigenen verbindungsorientierten Kanal auf derselben Funkverbindung zu beanspruchen. Der Kanal ist ein kreditflussgesteuerter Bytepfad mit einer viel größeren MTU pro Paket und ohne GATT-Framing dazwischen.
11.11.1. Wann L2CAP zu verwenden ist¶
L2CAP-Kanäle sind das richtige Werkzeug, wenn:
Die Übertragung mehr als ein paar hundert Bytes umfasst.
Beide Enden wissen, dass ein L2CAP-Kanal verwendet wird (er wird nicht in der Advertising-Nutzlast offengelegt; der Client muss die Protocol/Service Multiplexer-Nummer (PSM) des Kanals außerhalb der Verbindung kennen).
Die Anwendung bereit ist, auf die GATT-Annehmlichkeiten zu verzichten: keine eingebaute Adressierbarkeit per UUID, keine Client-Auffindbarkeit über Standard-Apps, keine Benachrichtigungen.
Der häufigste Fall in aioble-basierten Anwendungen ist das Bewegen eines binären Blobs zwischen zwei Softwareteilen, die beide die PSM-Konvention kennen – ein eigenes Kamera-zu-Telefon-Protokoll, ein Paar openmv-Kameras, die miteinander sprechen, ein interner Firmware-Update-Pfad unter dem GATT-Dienst eines Peripherals.
Für alles andere bleibe bei GATT. Ein kurzer Status, ein Steuerregister, ein Sensorwert – all das gehört in eine Charakteristik.
11.11.2. Einen Kanal aufbauen¶
L2CAP läuft auf einer bestehenden aioble.DeviceConnection, sodass der GAP-seitige Ablauf von Discovery / Advertising / Connecting genau derselbe ist wie bei GATT. Sobald beide Seiten eine Verbindung halten, lauscht eine Seite auf einer PSM, die andere Seite verbindet sich damit.
Die PSM ist nur eine kleine Ganzzahl. Die Bluetooth SIG reserviert den unteren Bereich für standardisierte Nutzung (0x0001-0x007F); für anwendungsspezifische Kanäle verwende eine Nummer aus dem dynamischen Bereich (0x0080-0x00FF für feste PSMs, ab 0x0040 typischerweise frei für eigene Nutzung). Beide Seiten müssen sich vorher auf den Wert einigen.
Die MTU auf einem L2CAP-Kanal ist die größte einzelne SDU (Service Data Unit), die eine der beiden Seiten in einem send() liefert – nicht die MTU der BLE-Verbindung. Aioble fragmentiert größere Nutzlasten automatisch. Der BLE-Host der Kamera begrenzt die L2CAP-MTU auf 1017 Bytes; 512 ist ein sinnvoller Standardwert, der auf beiden Seiten Spielraum lässt, ohne RAM zu verbrennen.
Auf der Listener-Seite (z. B. die Kamera 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()
Auf der Connector-Seite (z. B. ein Telefon oder 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() blockiert, bis der Peer sich verbindet (oder timeout_ms auslöst); l2cap_connect() blockiert, bis der Listener akzeptiert (oder fehlschlägt). Beide liefern ein aioble.L2CAPChannel zurück – selbst ein asynchroner Kontextmanager, der den Kanal beim Verlassen schließt.
11.11.3. Senden und Empfangen¶
Die zwei Hauptoperationen auf einem Kanal sind send() (schreibt Bytes an den Peer) und recvinto() (liest in einen vorab allokierten Puffer). Beide sind Coroutinen.
send()fragmentiert den Puffer in MTU-große Stücke und wartet zwischen ihnen auf Flusssteuerungs-Credits der Link-Schicht. Ein langer Send ist aus Sicht der Anwendung ein einzigesawait; intern kann er viele Pakete in die Warteschlange stellen und pausieren, wann immer die Empfangs-Credits des Peers erschöpft sind.recvinto()füllt den übergebenen Puffer mit allem, was verfügbar ist (bis zur MTU des Kanals), und liefert die Byteanzahl zurück. Wartet, wenn nichts verfügbar ist.available()liefert synchronTrue, wenn gepufferte Daten bereitstehen – nützlich zum Pollen, ohne zu suspendieren.flush()wartet, bis jeder ausstehende Send vollständig an den Controller übertragen wurde.
L2CAP-Kanäle sind insofern stromartig, als die Bytes der Reihe nach und ohne Verlust ankommen, aber die Grenzen eines einzelnen send bleiben erhalten – jede SDU kommt aus einem einzigen recvinto heraus. Das ist anders als bei TCP, wo die Grenzen eines send() über mehrere recv()-Aufrufe verschmiert sein können.
11.11.4. Behandlung von Verbindungsabbrüchen¶
Der Kanal verschwindet unter drei Bedingungen: Eine der Seiten ruft disconnect() auf, die zugrunde liegende GAP-Verbindung bricht ab, oder der L2CAP-seitige Disconnect trifft ein. Aktive Operationen lösen aioble.L2CAPDisconnectedError aus. Wie auf der GATT-Seite zeigt sich das als Exception in der Coroutine, die gewartet hat, und der Block async with channel wird sauber verlassen.
Wird ein Kanal durch einen GAP-seitigen Disconnect unerreichbar, kehrt die Anwendung zum Advertising oder Scannen zurück, genauso wie sie es bei einem GATT-Disconnect täte.
11.11.5. Speicherkosten¶
Größere MTUs und längere Warteschlangen verbrauchen auf beiden Seiten mehr RAM. Eine 512-Byte-MTU plus ein Empfangspuffer pro Kanal ergibt etwa 1 KB pro Kanal – nicht umsonst auf einer kleinen Kamera, wenn mehrere Kanäle gleichzeitig offen sind. Bleibe bei einem Kanal pro Verbindung und wähle eine MTU, die zur erwarteten Nachrichtengröße passt; der Standard von einem L2CAPChannel pro DeviceConnection reicht für die meisten Anwendungen.
L2CAP ist BLEs Sicherheitsventil. GATT ist das, wonach fast jede Anwendung zuerst greift, und die übrigen Central-/Peripheral-Beispiele dieses Abschnitts bleiben bei GATT. Die kanalbasierte API ist die Antwort, wenn eine Anwendung dem Schlüssel/Wert-Modell entwächst.