# DMC — Kommunikationsschicht Erweiterung der bestehenden Flutter-App um die Verbindung zum Fluggerät. Diese Datei beschreibt **nur** Transport, MSP und die zugehörige Bedienung. Planung, Karte, Wind und Gelände sind bereits vorhanden und werden nicht berührt. --- ## 1. Aufgabe Die App soll im Fly-Modus per Funk mit dem Flight Controller sprechen: Telemetrie lesen, später Missionen hochladen. Der Weg führt über die Fernsteuerung, nicht über eine eigene Funkstrecke. ``` Handy ──Bluetooth SPP──► RadioMaster Pocket (mLRS Tx + Backpack) │ LoRa 2,4 GHz Funke ──CRSF (intern)───────────┤ ▼ XR1 (mLRS Rx) ──UART/MspX──► FC (iNAV, MSP) ``` RC und MSP teilen sich dieselbe LoRa-Strecke. Die Fernsteuerung funktioniert parallel unverändert weiter. **MVP:** Bluetooth Classic (SPP), nur Android. **Später:** WLAN (UDP) als in den Einstellungen wählbare Alternative. --- ## 2. Transportschnittstelle Zuerst bauen. Alles Weitere wird ausschließlich gegen diese Schnittstelle programmiert, damit WLAN später eine zweite Implementierung ist und kein Umbau. ```dart enum LinkState { disconnected, connecting, connected, error } abstract class LinkTransport { Stream get incoming; Stream get state; Future send(Uint8List data); Future connect(); Future disconnect(); void dispose(); } ``` Implementierungen: - `BluetoothClassicTransport` — MVP - `UdpTransport` — später - `LoopbackTransport` — für Tests und Entwicklung ohne Hardware Der MSP-Code darüber kennt ausschließlich `LinkTransport`. Kein `import` von Bluetooth- oder Netzwerkpaketen außerhalb von `transport/`. --- ## 3. Bluetooth Classic (MVP) ### Warum Classic und nicht BLE MSP ist ein Byte-Strom, und SPP liefert genau das. BLE kennt nur GATT mit kurzen Attributen; ein serieller Kanal müsste dort über einen UART-Dienst nachgebaut werden, inklusive eigener Zerlegung und Wiederzusammensetzung der MSP-Nachrichten. Zweiter Grund: Bluetooth berührt den Netzwerk-Stack nicht. WLAN und Mobilfunk bleiben im Flug frei für Kartenkacheln und Wetterabruf. Bei WLAN ist genau das das Problem (siehe 6). BLE bleibt relevant, falls iOS dazukommt — dort gibt es kein SPP. ### Paket und Konstanten ```yaml dependencies: flutter_classic_bluetooth: ^ # RFCOMM/SPP, gepflegt permission_handler: ^ ``` SPP-UUID: `00001101-0000-1000-8000-00805F9B34FB` ### Verbindungsablauf beim Tippen auf „Fly" 1. **Bluetooth an?** Wenn nicht: Hinweis mit Knopf in die Systemeinstellungen. `BluetoothAdapter.enable()` ist ab API 31 nicht mehr nutzbar — die App kann Bluetooth nicht selbst einschalten. 2. **Gerät bestimmen — ohne zu suchen.** `getBondedDevices()` liefert die gekoppelten Geräte sofort. Auswahl in dieser Reihenfolge: a) gemerkte MAC-Adresse aus den Einstellungen b) erstes gekoppeltes Gerät, dessen Name mit `mLRS` beginnt c) sonst: Auswahlliste anzeigen **Nicht scannen**, wenn ein gekoppeltes Gerät gefunden wurde. Ein Scan kostet Sekunden, braucht eine zusätzliche Berechtigung und ist unnötig. 3. **Laufende Suche abbrechen**, falls doch eine läuft. Andernfalls schlägt der Verbindungsaufbau fehl oder wird sehr langsam — klassischer Android-Fallstrick. 4. **Socket öffnen**, `LinkState.connected` melden, MSP-Polling starten. Zielzeit: unter 3 Sekunden, ohne einen einzigen Dialog. ### Kopplung Bleibt einmalig manuell über den Systemdialog, das ist nicht umgehbar. Danach bleiben die Geräte gekoppelt und verbinden sich bei künftigen Sitzungen automatisch, solange die Kopplung nicht gelöscht wird. In den Einstellungen ein Knopf „Gerät koppeln", der die Bluetooth-Systemeinstellungen öffnet, plus ein kurzer Hinweistext. ### Berechtigungen | Ab Android 12 (API 31) | Darunter | |---|---| | `BLUETOOTH_CONNECT` (Laufzeit) | `BLUETOOTH`, `BLUETOOTH_ADMIN` | | `BLUETOOTH_SCAN` nur falls gescannt wird | `ACCESS_FINE_LOCATION` | `BLUETOOTH_CONNECT` erscheint dem Nutzer als **„Geräte in der Nähe"**. Wichtig: Wird sie beim ersten Mal abgelehnt, fragt Android **kein zweites Mal**. Diesen Fall abfangen und einen erklärenden Hinweis mit Knopf in die App-Einstellungen zeigen — nicht bloß „Verbindung fehlgeschlagen". ### Robustheit - Wiederverbinden mit ansteigender Wartezeit (1 s, 2 s, 4 s, … max. 30 s) - Bei `LinkState.error` den Grund mitführen und in der Oberfläche anzeigen - **Alter des letzten empfangenen MSP-Pakets** dauerhaft sichtbar machen. Im Feld ist das der einzige verlässliche Hinweis, ob die Werte noch aktuell sind. Vorschlag: grün < 2 s, bernstein < 5 s, rot darüber. - Verbindung beim Verlassen des Fly-Modus sauber schließen --- ## 4. MSP ### Rahmenformat iNAV erwartet **MSPv2**. MSPv1 reicht nicht — der iNAV-Configurator sendet ebenfalls v2, und die MSP-über-CRSF-Umsetzung scheitert genau daran. ``` '$' 'X' dir: '<' Anfrage, '>' Antwort, '!' Fehler crc: CRC8 DVB-S2 über flag..payload ``` Zu implementieren: Kodierer, Streaming-Dekodierer (der Byte-Strom kommt fragmentiert an, also Zustandsautomat statt „ein Paket pro Lesevorgang"), CRC-Prüfung, Verwerfen fehlerhafter Rahmen. ### Polling MSP ist ein Frage-Antwort-Protokoll: die App fragt, der FC antwortet. Es kommt nichts von selbst. Also ein eigener Takt: | Zweck | Rate | |---|---| | Position, Lage, Höhe | 5–10 Hz | | Spannung, Strom | 1 Hz | | Status, Flugmodus | 2 Hz | Anfragen serialisieren: immer nur eine offene Anfrage, mit Zeitgrenze (z. B. 500 ms) und Wiederholung. Sonst laufen bei schwacher Strecke die Antworten durcheinander. **Befehlsnummern nicht aus dem Gedächtnis übernehmen.** Gegen `msp_protocol.h` beziehungsweise `msp_protocol_v2_inav.h` im iNAV-Repository prüfen und als Konstanten an einer Stelle sammeln. ### Reihenfolge der Umsetzung 1. Rahmen kodieren/dekodieren + Tests gegen aufgezeichnete Bytefolgen 2. Telemetrie lesen und in der bestehenden Fly-Anzeige darstellen (`LIVE`-Zeilen bei Höhe und Geschwindigkeit sind bereits vorhanden) 3. Erst danach Missionsupload --- ## 5. Gegenstelle (keine App-Arbeit, aber Voraussetzung) Zur Einordnung, damit beim Testen klar ist, woran es liegt, wenn nichts kommt. **mLRS-Empfänger:** `Rx Ser Link Mode` = `mspX`, `Rx Ser Baudrate` = 115200, `Rx Snd RcChannel` = `rc override` oder `rc channels`. **mLRS-Sender:** `Tx Ch Source` = `crsf`. In EdgeTX internes Modul auf CRSF mit **400 k** Baud (nicht 5,25 M wie bei ELRS). **iNAV:** MSP auf dem UART aktivieren, an dem der Empfänger hängt (UART2 empfohlen), „Serial RX" auf diesem Port **aus**. Baudrate ≥ 115200 — darunter gehen Nachrichten verloren. Receiver-Tab: Receiver Mode `MSP`, RSSI Source `MSP`. Bekannte Grenze: Im 2,4-GHz-FLRC-Modus ist die RC-Rate über MSP auf 37 Hz begrenzt. Für eine autonom fliegende Fixed-Wing unkritisch. --- ## 6. WLAN (später) Vorbereiten, aber nicht im MVP umsetzen. Die Fallstricke hier festhalten, damit sie nicht neu entdeckt werden müssen. Die Bridge erzeugt `mLRS-xxxx AP UDP`, ohne Passwort, UDP-Port **14550**. **Präfix-Verbindung.** Die SSID enthält eine Zufallszahl, der genaue Name ist also unbekannt. `WifiNetworkSpecifier` mit `setSsidPattern(PatternMatcher("mLRS-", PATTERN_PREFIX))` — der Systemdialog zeigt dann nur die Bridge, der Nutzer bestätigt bloß. **Kein Internet auf diesem Netz.** Ohne Bindung routet Android weiter über Mobilfunk, und die UDP-Pakete verschwinden, obwohl das WLAN verbunden aussieht. Zwei Wege: - *Einfach:* `forceWifiUsage(true)` aus `wifi_iot`. Bindet aber **den gesamten Prozess** — solange es aktiv ist, sind Kartenkacheln und Wetter nicht mehr erreichbar. Nur im Fly-Modus setzen und beim Verlassen sofort lösen. Es gibt Berichte über gerätespezifische Aussetzer. - *Sauber:* UDP-Socket nativ in Kotlin öffnen, per `Network.bindSocket()` **einzeln** binden, Daten über einen EventChannel nach Dart streamen. Dann läuft MSP über die Bridge und alles andere weiter über Mobilfunk. **Zieladresse nicht fest eincodieren.** Gateway-Adresse des Netzes verwenden (das ist der ESP) oder die Absenderadresse des ersten eingehenden Pakets merken. --- ## 7. Einstellungen Neuer Abschnitt „Verbindung": - **Art:** Bluetooth · WLAN (ausgegraut bis umgesetzt) · USB (später) - **Gerät:** Auswahlliste der gekoppelten Geräte, Standard „automatisch (Name beginnt mit mLRS)" - **Beim Wechsel in Fly automatisch verbinden:** an/aus, Standard an - **Gerät koppeln** → öffnet die Bluetooth-Systemeinstellungen - **Verbindungsprotokoll** → einfache Textansicht der letzten Ereignisse (verbunden, getrennt, Fehler, Paketzähler). Spart im Feld viel Ratearbeit. --- ## 8. Abnahmekriterien - [ ] Nach einmaliger Kopplung verbindet ein Tipp auf „Fly" in unter 3 Sekunden ohne Dialog - [ ] Bluetooth aus → verständlicher Hinweis mit Knopf in die Einstellungen, kein stiller Fehlschlag - [ ] Berechtigung abgelehnt → erklärender Hinweis mit Weg in die App-Einstellungen - [ ] Verbindung wird während des Betriebs getrennt → automatischer Neuaufbau, Zustand jederzeit in der Oberfläche sichtbar - [ ] Telemetriealter ist immer sichtbar und färbt sich bei Veralten um - [ ] MSP-Dekodierer verarbeitet beliebig fragmentierte Eingaben korrekt (Test: dieselbe Bytefolge in Stücken von 1, 7 und 512 Byte) - [ ] Fehlerhafte Rahmen werden verworfen, ohne den Strom zu verlieren - [ ] Verlassen des Fly-Modus schließt die Verbindung - [ ] Kein Bluetooth-Import außerhalb von `transport/` --- ## 9. Hinweise - Ohne Hardware entwickelbar: `LoopbackTransport` plus aufgezeichnete MSP-Antworten. Erst danach am echten Gerät gegenprüfen. - Sicherheitsrelevante Anzeigen nie mit Platzhaltern füllen. Kommt keine Telemetrie, muss das sichtbar sein — nicht ein eingefrorener alter Wert. - Die App steuert das Fluggerät nicht. RC bleibt vollständig bei der Fernsteuerung; MSP dient dem Lesen und dem Übertragen von Missionen.