11.4. Advertising und Scanning¶
Zwei BLE-Geräte, die sich noch nie zuvor begegnet sind, müssen sich zunächst finden. Die Netzwerktechnik löst das, indem sie jedem Gerät eine Adresse aus einem gemeinsamen Pool zuweist und beide Seiten einander über Router erreichen lässt. BLE hat keine Router, keinen gemeinsamen Pool und – zwischen den meisten Gerätepaaren – überhaupt keine vorherige Beziehung. Das Generic Access Profile (GAP) löst die Erkennung stattdessen mit einem Broadcast-und-Lausch-Muster. Eine Seite bewirbt sich (advertising) – sie sendet in regelmäßigen Abständen ein kurzes Paket auf den drei Advertising-Kanälen, das beschreibt, wer sie ist. Die andere Seite scannt – sie durchläuft dieselben drei Kanäle und lauscht auf diese Pakete.
GAP definiert rund um dieses Muster vier Rollen, jede eine bestimmte Kombination aus Advertising und Lauschen.
11.4.1. Die vier GAP-Rollen¶
Die vier GAP-Rollen. Die vertikale Achse gibt an, ob das Gerät sich bewirbt; die horizontale Achse, ob es Verbindungen akzeptiert (oder initiiert).¶
Ein Peripheral bewirbt Pakete, die sagen „Ich bin hier und du kannst dich mit mir verbinden“. Wenn ein anderes Gerät eine Verbindung öffnet, hört das Peripheral mit dem Advertising auf und beginnt, GATT-Anfragen zu bedienen. Herzfrequenzgurte, Thermometer und die meisten Kameras-als-Sensoren agieren als Peripherals.
Ein Central scannt nach Peripherals, wählt eines aus und initiiert eine Verbindung. Nach dem Verbinden spricht es GATT als Client. Telefone, Laptops und Kameras, die als Datensammler agieren, sind Centrals.
Ein Broadcaster bewirbt sich, akzeptiert aber niemals Verbindungen. Seine Advertising-Nutzlast ist die Daten – es gibt nichts, womit man sich verbinden könnte. iBeacons und die meisten Anwesenheits-Beacons in Geschäften sind Broadcaster.
Ein Observer scannt nach diesen Aussendungen und liest die Nutzlast aus, ebenfalls ohne sich jemals zu verbinden. Eine Kamera, die auf Beacons in der Nähe lauscht und auf das reagiert, was sie hört, ist ein Observer.
Ein einzelnes Gerät kann gleichzeitig mehr als eine Rolle übernehmen – eine Kamera kann ein Peripheral sein, das seinen eigenen Zustand veröffentlicht, und ein Central, das sich mit einem Sensor in der Nähe verbindet. Das Funkmodul multiplext die Arbeit.
11.4.2. Was ein Advertising-Paket enthält¶
Ein Advertising-Paket ist klein: 31 Bytes Nutzlast, oder 62, wenn der Advertiser zusätzlich eine Scan Response veröffentlicht, die Scanner spontan anfordern können. Die Nutzlast ist eine Liste kurzer typisierter Felder:
Flags. Verbindbar oder nicht, allgemein / eingeschränkt auffindbar.
Local Name. Eine kurze, menschenfreundliche Zeichenkette – der Name, den das Betriebssystem auf einem Telefon oder Laptop in seinem Bluetooth-Menü anzeigt.
Service-UUIDs. Eine Liste der GATT-Service-Bezeichner, die das Gerät bereitstellt, sodass ein Scanner geeignete Peripherals erkennen kann, ohne sich zuerst zu verbinden. Ein Herzfrequenzgurt bewirbt
0x180D– die Standard-UUID des Heart-Rate-Service – und eine Herzfrequenz-App auf dem Telefon weiß allein daraus, dass das Gerät eine Verbindung wert ist.Appearance. Ein 16-Bit-Wert aus der Liste der von Bluetooth zugewiesenen Nummern (Sensor, generisches Medium, generische Uhr, …) – ein Hinweis für das Central, was angezeigt werden soll.
Herstellerspezifische Daten. Freiformige Bytes mit einer vorangestellten Firmen-ID. iBeacons verwenden dieses Feld, um ihre UUID, ihren Major- und ihren Minor-Wert zu transportieren; eigene Anwendungen können hier alles Beliebige unterbringen.
Advertising-Nutzlasten sind knapp bemessen. Das 31-Byte-Limit macht die Auswahl dessen, was aufgenommen wird, zu einer echten Entwurfsentscheidung – ein langer, menschenlesbarer Name kann schnell keinen Platz mehr für Service-UUIDs lassen. Die aioble.advertise()-API nimmt jedes dieser Felder als Schlüsselwortargument entgegen und setzt die Bytes für dich zusammen, wobei sie automatisch in die Scan Response überläuft, wenn das Hauptpaket voll wird.
11.4.3. Aktives und passives Scanning¶
Ein Scanner kann passiv laufen, wobei er auf Advertising-Pakete lauscht und das Eintreffende parst, oder aktiv, wobei er zusätzlich an jeden Advertiser eine Scan Request sendet und die zurückkommende Scan Response parst.
Passives Scanning sieht nur das anfängliche Advertising-Paket (bis zu 31 Bytes). Aktives Scanning verdoppelt das – die Scan Response sind weitere 31 Bytes, die das Peripheral für Felder nutzen kann, die nicht hineinpassten. Aktives Scanning kostet außerdem auf beiden Seiten Strom, da der Scanner sendet und der Advertiser ein zusätzliches Paket sendet, weshalb es eine bewusste Wahl statt einer Voreinstellung ist.
In der aioble-API schaltet active=True bei aioble.scan() den Modus um, und jedes ScanResult stellt die kombinierten Daten aus adv_data plus resp_data sowie Hilfsfunktionen wie result.name() und result.services() bereit, die das byteweise Parsen verbergen.
Bemerkung
Die Attribute adv_data und resp_data sind die rohen Advertising- und Scan-Response-Nutzlasten (bytes). Die Hilfsfunktionen – name(), services(), manufacturer() – decken die gängigen Standardfelder ab und sind in 99 % der Fälle die richtige Wahl. Greife nur dann zu den rohen Bytes, wenn du ein Herstellerfeld brauchst, das die Hilfsfunktionen nicht parsen (Eddystone-URLs, iBeacon-UUID/Major/Minor, eigene Advertising-Typen). Das Byte-Layout ist das standardmäßige TLV-Format: jedes Feld ist length, type, value....
11.4.4. Das Advertising-Intervall¶
Wie oft das Peripheral sendet, ist ein Kompromiss zwischen Leistung und Erkennungslatenz. Aussendungen, die alle 20 ms hinausgehen, werden von einem Scanner fast sofort aufgegriffen, halten aber das Funkmodul beschäftigt und entleeren die Batterie; Aussendungen jede Sekunde verbrauchen fast keinen Strom, sorgen aber dafür, dass ein durchsuchender Scanner das Gerät langsamer bemerkt.
interval_us bei aioble.advertise() legt das Intervall in Mikrosekunden fest:
20.000 bis 100.000 us (20 ms - 100 ms) – schnelles Koppeln, App erwartet eine zügige Reaktion, am Stromnetz angeschlossenes Gerät.
250.000 bis 1.000.000 us (250 ms - 1 s) – ein vernünftiger Standard für ein batteriebetriebenes Peripheral, das auffindbar sein möchte, ohne Ladung zu verbrennen.
Über 1.000.000 us – langsamer Hintergrund-Broadcast, Beacons, die alle paar Sekunden ein Positions-Update senden.
Die Scanner-Seite hat ihre eigenen Stellschrauben – aioble.scan() nimmt interval_us und window_us entgegen (wie oft der Scanner sein Funkmodul aufweckt und wie lange er jeweils lauscht). Die Voreinstellungen sind in Ordnung; die einzige übliche Änderung besteht darin, beide für einen kontinuierlichen Scan gleichzusetzen, wenn die Batterie keine Rolle spielt.
11.4.5. Verbindungslose Muster – Broadcaster und Observer¶
Die Seiten zu Als Peripheriegerät agieren und Als Central agieren arbeiten die verbindbare Form der API durch – bei der ein Peripheral eine Verbindung akzeptiert und die beiden Seiten Daten über GATT austauschen. Die andere Form ist verbindungslos: ein Broadcaster sendet die Nutzlast als Aussendung, und jeder Observer in Reichweite kann sie lesen, ohne sich jemals zu verbinden. Beacons, Anwesenheitssensoren und einseitige Telemetrie leben alle hier.
Ein Broadcaster ist aioble.advertise() mit connectable=False. Herstellerspezifische Daten transportieren die Nutzlast:
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())
Das Schlüsselwort timeout_ms beendet den advertise-Aufruf nach einer Sekunde; die äußere Schleife stößt ihn mit der nächsten Sequenznummer erneut an, sodass Lauscher frische Daten sehen. Das Flag connectable=False ist es, was die Aussendung im Broadcaster-Stil hält – die Kamera reagiert nicht auf eine Verbindungsanfrage, selbst wenn eine eintrifft.
Ein Observer ist der dazu passende, nur lesende Scanner. Er führt aioble.scan() endlos aus, parst eintreffende Aussendungen und ruft niemals connect() auf:
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 scannt, bis der Kontextmanager verlassen wird; active=False hält das eigene Funkmodul des Observers stumm (keine Scan-Response-Anfragen), für den geringsten Stromverbrauch. Das Argument filter= bei manufacturer() verwirft jede Aussendung, die nicht zur Firmen-ID passt, sodass die Schleife nur für den Verkehr des Broadcasters auslöst.
11.4.6. Von der Erkennung zu einer Verbindung¶
Sobald ein Central ein Peripheral zum Kommunizieren ausgewählt hat, hört es auf zu lauschen, sendet eine Connect Request auf dem Advertising-Kanal, den das Peripheral zuletzt benutzt hat, und beide Seiten fallen in die springenden Datenkanäle des Link Layers. Das Peripheral hört an diesem Punkt typischerweise mit dem Advertising auf. Was als Nächstes geschieht – Verbindungsparameter, GATT-Erkennung, die Lebensdauer der Verbindung – steht unter Verbindungen.