Android BLE: Gerät schaltet, aber Statusabfrage bleibt stumm – requestMtu fehlt
Meine Android-App sprach ein BLE-Gerät über den Nordic-UART-Dienst an: Licht an und aus klappte, die Statusabfrage blieb ohne Antwort. Im Browser lief dieselbe Abfrage sofort. Der Unterschied: Chrome handelt eine größere MTU aus, Android-Apps müssen requestMtu() selbst aufrufen.
$ git log --graph Der Weg zur Lösung
-
Problem
Statusabfrage bleibt stumm
Schaltbefehle gehen, die Abfrage 0x0E bekommt in der eigenen Android-App keine Antwort.
-
Irrweg
Watchdog
Notifications neu abonnieren und neu verbinden änderte nichts.
-
Erkenntnis
Im Browser geht es
Dort kommt die Antwort mit rund 160 Byte als ein Paket, Chrome hat die MTU selbst ausgehandelt.
-
Lösung
requestMtu aufrufen
requestMtu(247) nach dem Verbinden, discoverServices erst in onMtuChanged. Ausgehandelt wurden 498, der Status kommt.
Ich steuere die Lichtanlage meines Fahrrads (Canyon ECLIPS) über eine eigene Android-App. Das Gerät nutzt den Nordic-UART-Dienst: Service 6E400001-…, Schreiben auf 6E400002, Antworten als Notification auf 6E400003. Schaltbefehle (ein Byte, zum Beispiel 0x03 für „vorne an“) funktionierten, das Gerät bestätigte mit der kurzen Notification „Event received“. Die Statusabfrage 0x0E blieb dagegen einfach unbeantwortet. Keine Notification, kein Fehler, kein Hinweis im Log.
Das Problem
Dieselbe Abfrage funktionierte im Browser per Web Bluetooth sofort. Dort sah man auch den Grund: Die Statusantwort ist rund 160 Byte lang und kommt als ein Paket. Bei der Standard-MTU von 23 Byte bleiben 20 Byte Nutzdaten, und da passt sie nicht hinein. Das Gerät schickt dann offenbar gar nichts. Die Bestätigung auf Schaltbefehle hat nur 19 Byte und passt gerade noch. Deshalb sah alles andere gesund aus, und ich suchte zuerst an der falschen Stelle.
Die Lösung
Nach dem Verbinden zuerst die MTU anfordern und erst im Callback die Dienste suchen:
override fun onConnectionStateChange(g: BluetoothGatt, status: Int, newState: Int) {
if (newState == BluetoothProfile.STATE_CONNECTED) {
// Die Statusantwort passt nicht in 20-Byte-Pakete: ohne groessere MTU schweigt das Geraet
if (!g.requestMtu(247)) g.discoverServices()
}
}
override fun onMtuChanged(g: BluetoothGatt, mtu: Int, status: Int) {
note("MTU $mtu (Status $status)")
g.discoverServices()
}
Im Protokoll der App steht danach:
MTU 498 (Status 0)
ECLIPS verbunden
Und die Statusantwort kommt vollständig an: Ladestand, Spannung, Strom, Licht, Laden, Tempo, Firmware.
Warum das funktioniert
Chrome handelt bei Web Bluetooth die MTU selbst aus. Android-Apps bleiben bei 23 Byte, bis sie requestMtu() aufrufen. Das ist im Android-Handbuch dokumentiert, fällt aber nicht auf, solange alle Antworten kurz sind. Dieses Gerät zerlegt lange Antworten nicht in mehrere Notifications, sondern lässt sie weg. 247 ist der übliche Wunschwert, ausgehandelt wurden hier laut Callback 498.
Stolperfallen
- Erst
requestMtu, danndiscoverServices. Android arbeitet GATT-Operationen nacheinander ab, zwei gleichzeitig gehen schief. - Gibt
requestMtufalsezurück, trotzdem die Dienste suchen, sonst bleibt die Verbindung hängen. - Ein Watchdog, der bei fehlenden Antworten die Notifications neu abonniert oder neu verbindet, half nicht. Das Problem war nie die Verbindung.
- Antworten können auch bei großer MTU zusammengeklebt ankommen, also zwei Meldungen ohne Zeilenumbruch hintereinander. Ein kurzer Puffer von 300 ms und ein robustes Aufteilen beim Lesen helfen.