# DMC — Kommunikationsschicht Erweiterung der bestehenden Flutter-App um die Verbindung zum Fluggerät. Betrifft **nur** Transport, MSP und die zugehörige Bedienung. Planung, Karte, Wind und Gelände sind vorhanden und werden nicht berührt. Stand: nach dem ersten Gerätetest auf Pixel 10a. Die Erkenntnisse daraus sind in Abschnitt 4 eingearbeitet und haben die ursprüngliche Herangehensweise korrigiert. --- ## 1. Aufgabe und Aufbau ``` Handy ──WLAN (UDP)──► RadioMaster Pocket (mLRS Tx + Backpack ESP) │ 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 läuft parallel unverändert weiter. **WLAN über UDP ist der einzige Weg auf dieser Hardware.** Bluetooth scheidet aus: Die mLRS Wireless Bridge läuft auf dem Backpack-Chip, und ESP8266/ESP8285 sowie die ESP32-Cx-Reihe bieten kein klassisches Bluetooth. Bei RadioMaster-Modulen ist der Backpack ein ESP8285 — WLAN, sonst nichts. Das mit „Bluetooth" beworbene Merkmal der Funke betrifft den Haupt-Chip des Moduls (Simulator-Joystick), nicht die Bridge. Später möglich: USB-Seriell als kabelgebundene Rückfallebene. --- ## 2. Transportschnittstelle Zuerst bauen. Alles Weitere wird ausschließlich dagegen programmiert. ```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: `UdpTransport` (aktuell), `LoopbackTransport` (Tests ohne Hardware), später `UsbSerialTransport`. **Regel, als Abnahmekriterium prüfbar:** kein Netzwerk- oder Plattform-Import außerhalb von `transport/`. Der MSP-Code kennt nur `LinkTransport`. --- ## 3. UDP-Transport Die Bridge erzeugt `mLRS-xxxx AP UDP`, ohne Passwort, Port **14550**. ### Das Umsetzungsmuster Kite GC und Mission Planner kommen unabhängig voneinander zum selben Ergebnis. Beide Punkte sind nicht optional — ohne sie empfängt die App **kein einziges Byte**: **1. Lokal auf denselben Port binden, den man ansprechen will (14550).** Nicht auf einen flüchtigen Port. WLAN-Telemetriebrücken senden an einen festen Port; ein zufälliger lokaler Port verwirft das stillschweigend. Ist 14550 belegt, ausweichen, aber protokollieren. **2. Kein `connect()`. Gegenstelle aus dem Verkehr lernen.** Ziel mit der konfigurierten Adresse vorbelegen (damit das erste Paket rausgeht), danach auf die Quelladresse umstellen, von der tatsächlich Daten kommen. Deckt Lausch-, Client- und Broadcast-Betrieb gleichermaßen ab. Mission Planner führt zusätzlich eine Liste aller Gegenstellen — für uns nicht nötig, aber der Grund ist bedenkenswert: Es kann mehr als eine geben. **Zieladresse niemals fest eincodieren.** Vorbelegung aus der Gateway-Adresse des angeforderten Netzes, danach gilt die gelernte Adresse. **Lesezeitgrenze kurz halten** (etwa 50 ms). Sie begrenzt, wie lange ein ausgehender Befehl wartet, bis die laufende Leseoperation zurückkehrt. --- ## 4. Verbindungsaufbau — der kritische Teil ### Was der Gerätetest ergeben hat Getestet auf Pixel 10a: Verbindung mit dem mLRS-Netz über die **Systemeinstellungen**, danach in der App verbunden. - UDP funktionierte, empfangene Pakete zählten hoch ✓ - **Kartenkacheln und Wetterabruf fielen aus** ✗ Ursache: Bei Verbindung über die Systemeinstellungen wird das mLRS-Netz zur **Standardroute des Geräts** — für alles. Dass es kein Internet hat, hindert Android nicht daran; bestätigt der Nutzer den „kein Internet"-Hinweis, bleibt das Netz aktiv und bevorzugt. ### Die Lösung: Verbindung ausschließlich aus der App `WifiNetworkSpecifier` ist genau dafür gebaut. Aus der Android-Referenz: Diese Spezifizierer können ausschließlich für ein lokales WLAN ohne Internet-Fähigkeit verwendet werden; das Gerät wechselt seine Standardroute daher **nicht** auf WLAN, wenn andere Übertragungswege wie Mobilfunk verfügbar sind. Das ist keine Nebenwirkung, sondern der Zweck der Schnittstelle. ```kotlin val specifier = WifiNetworkSpecifier.Builder() .setSsidPattern(PatternMatcher("mLRS-", PatternMatcher.PATTERN_PREFIX)) .build() val request = NetworkRequest.Builder() .addTransportType(NetworkCapabilities.TRANSPORT_WIFI) .removeCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) .setNetworkSpecifier(specifier) .build() connectivityManager.requestNetwork(request, callback) ``` - **Präfix-Muster** statt fester SSID, weil die SSID eine Zufallszahl aus der MAC-Adresse enthält. Der Systemdialog zeigt dann nur die Bridge, der Nutzer bestätigt einmal. - **`removeCapability(NET_CAPABILITY_INTERNET)` ist zwingend** — ohne sie kommt die Anfrage nie zustande, weil das Netz diese Fähigkeit nicht besitzt. - Braucht `CHANGE_NETWORK_STATE` im Manifest. - Die Verbindung ist an die App gebunden und endet, wenn der Request freigegeben wird. Für eine Bodenstation eher praktisch. ### Bindung ist damit verpflichtend Weil das angeforderte Netz **nicht** die Standardroute ist, erreicht ein ungebundener Socket die Bridge nicht. Ablauf im `onAvailable`-Rückruf: 1. Prozess an das erhaltene `Network` binden 2. UDP-Socket in Dart erzeugen (`RawDatagramSocket.bind`) 3. Prozessbindung **sofort wieder lösen** Android bindet laut Dokumentation alle **künftig erzeugten** Sockets an das gesetzte Netz, und die Bindung haftet am Socket. Der UDP-Socket bleibt daher am WLAN, während spätere Verbindungen — Karten, Wetter — wieder über Mobilfunk laufen. Plattform-Kanal mit zwei Methoden (`bindToNetwork`, `unbind`). **Noch unbestätigt:** ob die Prozessmarkierung auch für Sockets greift, die Dart auf eigenen I/O-Threads erzeugt. Der Mechanismus spricht dafür, dokumentiert ist es nicht. Deshalb der Abnahmetest in Abschnitt 8, der genau das prüft. **Falls es nicht greift:** UDP-Socket nativ in Kotlin öffnen, per `Network.bindSocket(DatagramSocket)` einzeln binden, Daten über einen EventChannel nach Dart. Android empfiehlt die Socket-genaue Bindung ohnehin gegenüber der Prozessbindung; aus Dart ist sie nur nicht erreichbar, weil weder Socket-Objekt noch Deskriptor herausgegeben werden. ### Zwei Fallstricke beim Testen **Das mLRS-Netz muss in den Systemeinstellungen entfernt werden.** Bleibt es gespeichert, verbindet sich das Telefon automatisch wieder darauf, es wird erneut zur Standardroute, und der Fehler tritt auf, obwohl der Code stimmt. **Der Test braucht aktive mobile Daten.** Ohne zweites Netz funktioniert alles auch ohne Bindung, und man misst nichts. ### Gleichzeitige Nutzung prüfen Ab Android 12 können Geräte parallel mit einem lokalen Gerät und dem primären Netz verbunden bleiben. Abfragbar über `WifiManager.isStaConcurrencyForLocalOnlyConnectionsSupported()`. Wird das unterstützt (auf aktuellen Pixel-Geräten der Fall), bleibt sogar das heimische WLAN nutzbar. Nicht voraussetzen, aber abfragen und im Verbindungsprotokoll festhalten — es erklärt Verhaltensunterschiede zwischen Geräten. --- ## 5. Berechtigungen ```xml ``` `CHANGE_NETWORK_STATE` für `requestNetwork` — die fehlt in Mission Planner, weshalb dieser Weg dort gar nicht möglich ist. `WAKE_LOCK` ist im Flug nötig, sonst schläft das Gerät während der Telemetrie ein. `CHANGE_WIFI_MULTICAST_STATE` erst ergänzen, wenn sich zeigt, dass die Bridge per Broadcast sendet — dann wird zusätzlich ein MulticastLock gebraucht. `android:usesCleartextTraffic="true"` nur nötig, falls die Bridge je über HTTP angesprochen wird. Für UDP nicht erforderlich. --- ## 6. MSP ### Rahmenformat iNAV erwartet **MSPv2**. MSPv1 reicht nicht. ``` '$' 'X' dir: '<' Anfrage, '>' Antwort, '!' Fehler crc: CRC8 DVB-S2 über flag..payload ``` Zu bauen: Kodierer, **Streaming-Dekodierer als Zustandsautomat** (der Byte-Strom kommt fragmentiert an — mehrere Pakete in einem Lesevorgang, ein Paket über mehrere Lesevorgänge), CRC-Prüfung, Verwerfen fehlerhafter Rahmen ohne den Strom zu verlieren. ### Polling MSP ist Frage und Antwort — von selbst kommt nichts. Eigener Takt: | Zweck | Rate | |---|---| | Position, Lage, Höhe | 5–10 Hz | | Status, Flugmodus | 2 Hz | | Spannung, Strom | 1 Hz | Immer nur **eine offene Anfrage**, mit Zeitgrenze (etwa 500 ms) und Wiederholung. Sonst laufen bei schwacher Strecke die Antworten durcheinander. **Befehlsnummern nicht aus dem Gedächtnis übernehmen.** Gegen `msp_protocol.h` und `msp_protocol_v2_inav.h` im iNAV-Repository prüfen und als Konstanten an einer Stelle sammeln. ### Reihenfolge 1. Rahmen kodieren/dekodieren, Tests gegen aufgezeichnete Bytefolgen 2. Telemetrie lesen, in die vorhandenen `LIVE`-Zeilen einspeisen 3. Erst danach Missionsupload --- ## 7. Gegenstelle (keine App-Arbeit, aber Voraussetzung) Damit beim Testen klar ist, woran es liegt, wenn nichts ankommt. **mLRS Rx:** `Rx Ser Link Mode` = `mspX`, `Rx Ser Baudrate` = 115200, `Rx Snd RcChannel` = `rc override` oder `rc channels`. **mLRS Tx:** `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 autonome Fixed-Wing unkritisch. --- ## 8. Bedienung ### Fly-Modus Beim Wechsel: Netz anfordern, nach `onAvailable` Socket erzeugen und binden, Polling starten. Beim Verlassen Socket schließen und den Netz-Request freigeben. **Telemetriealter dauerhaft sichtbar**, nicht nur ein Verbindungssymbol. Im Feld ist das der einzige verlässliche Hinweis, ob die Werte noch stimmen. Vorschlag: grün < 2 s, bernstein < 5 s, rot darüber. Sicherheitsrelevante Anzeigen bei ausbleibender Telemetrie kennzeichnen, **nicht** den letzten Wert stehenlassen. Wiederverbinden mit ansteigender Wartezeit (1, 2, 4 … max. 30 s). Nach Netzabriss den Socket verwerfen und samt Bindungsfenster neu erzeugen — laut Dokumentation hören so gebundene Sockets bei Netztrennung bewusst auf zu funktionieren. ### Einstellungen, Abschnitt „Verbindung" - **Art:** WLAN · USB (später) - **SSID-Präfix:** Vorgabe `mLRS-`, überschreibbar - **Port:** Vorgabe 14550, überschreibbar - **Beim Wechsel in Fly automatisch verbinden:** an/aus, Standard an - **Verbindungsprotokoll:** Textansicht der letzten Ereignisse — Netz angefordert, verfügbar, Socket gebunden, gelernte Gegenstelle, Paketzähler, Fehler. Dazu einmalig das Ergebnis von `isStaConcurrencyForLocalOnlyConnectionsSupported()`. Spart im Feld viel Ratearbeit. - **Hinweistext:** dass das mLRS-Netz nicht in den Systemeinstellungen gespeichert sein soll. --- ## 9. Abnahmekriterien - [ ] Verbindung erfolgt ausschließlich über `WifiNetworkSpecifier` aus der App, nicht über die Systemeinstellungen - [ ] **Während aktiver Telemetrie funktionieren Kartenkacheln und Wetterabruf weiter** — das ist der eigentliche Prüfstein, mit aktiven mobilen Daten testen - [ ] Socket bindet auf 14550; ist der Port belegt, wird ausgewichen und geloggt - [ ] Gegenstelle wird aus dem ersten eingehenden Paket gelernt und im Protokoll angezeigt - [ ] Keine feste IP-Adresse im Quelltext - [ ] 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 - [ ] Nur eine offene Anfrage gleichzeitig; Zeitüberschreitung führt zu Wiederholung, nicht zum Hängen - [ ] Telemetriealter immer sichtbar und farblich abgestuft - [ ] Verbindungsabriss → Socket wird verworfen und mit Bindungsfenster neu erzeugt, Zustand sichtbar - [ ] Verlassen des Fly-Modus gibt den Netz-Request frei - [ ] Kein Netzwerk-Import außerhalb von `transport/` --- ## 10. Hinweise - **Ohne Hardware entwickelbar:** `LoopbackTransport` plus aufgezeichnete MSP-Antworten. Erst danach am echten Gerät gegenprüfen. - Die App steuert das Fluggerät nicht. RC bleibt vollständig bei der Fernsteuerung; MSP dient dem Lesen und dem Übertragen von Missionen. - Referenzen zum Nachschlagen: Kite GC `src-tauri/src/transport/udp.rs` (der Kommentar erklärt das Portbindungs- und Peer-Lernen-Problem ausführlich), Mission Planner `ExtLibs/Comms/CommsUdpSerial.cs` (dieselbe Lösung in C#). Beide lösen das Routing-Problem **nicht** — dort verbindet sich der Nutzer über die Systemeinstellungen, mit demselben Nebeneffekt, den unser Test gezeigt hat.