diff --git a/DMC_Kommunikationsschicht v2.md b/DMC_Kommunikationsschicht v2.md new file mode 100644 index 0000000..d20a9a9 --- /dev/null +++ b/DMC_Kommunikationsschicht v2.md @@ -0,0 +1,333 @@ +# 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. diff --git a/app/android/app/build.gradle.kts b/app/android/app/build.gradle.kts index 89f0886..b37b608 100644 --- a/app/android/app/build.gradle.kts +++ b/app/android/app/build.gradle.kts @@ -19,7 +19,11 @@ android { applicationId = "com.dmc.dmc_app" // You can update the following values to match your application needs. // For more information, see: https://flutter.dev/to/review-gradle-config. - minSdk = flutter.minSdkVersion + // minSdk 29 (statt Flutter-Default 24): WifiNetworkSpecifier/ + // ConnectivityManager.requestNetwork() fuer den app-initiierten + // mLRS-Verbindungsaufbau (Doku Kommunikationsschicht v2, Abschnitt 4) + // gibt es erst ab Android 10. + minSdk = 29 targetSdk = flutter.targetSdkVersion versionCode = flutter.versionCode versionName = flutter.versionName diff --git a/app/android/app/src/main/AndroidManifest.xml b/app/android/app/src/main/AndroidManifest.xml index e56bac6..89fda6a 100644 --- a/app/android/app/src/main/AndroidManifest.xml +++ b/app/android/app/src/main/AndroidManifest.xml @@ -5,6 +5,14 @@ + +