Files
dmc/DMC_Kommunikationsschicht_neu.md

306 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.
---
## 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.
**MVP: WLAN über UDP.** Bluetooth ist auf dieser Hardware **nicht** möglich —
die mLRS Wireless Bridge läuft auf dem Backpack-Chip, und laut mLRS-Dokumentation
bieten ESP8266/ESP8285 sowie die ESP32-Cx-Reihe 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 mLRS-Bridge.
**Vor Beginn prüfen:** Im mLRS Web Flasher zeigt der Punkt „Flash Wireless Bridge"
das Backpack-Target. ESP8285 → nur WLAN. ESP32-C3 → WLAN und BLE, kein Classic.
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<Uint8List> get incoming;
Stream<LinkState> get state;
Future<void> send(Uint8List data);
Future<void> connect();
Future<void> disconnect();
void dispose();
}
```
Implementierungen: `UdpTransport` (MVP), `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`.
Diese Zweiteilung — rohe Bytes unten, Protokoll darüber — entspricht dem Aufbau
von Kite GC (`ByteTransport` + Protokollschicht) und hat sich dort bewährt.
---
## 3. UDP-Transport
Die Bridge erzeugt `mLRS-xxxx AP UDP`, ohne Passwort, Port **14550**.
### Das entscheidende 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,
auf einen flüchtigen ausweichen, aber das 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, die gesendet
haben. 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
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.
### Berechtigungen
Als Ausgangspunkt die Liste, die Mission Planner tatsächlich verwendet:
```xml
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE"/>
```
`WAKE_LOCK` ist im Flug nötig, sonst schläft das Gerät während der Telemetrie ein.
`CHANGE_WIFI_MULTICAST_STATE` erst anfordern, wenn sich zeigt, dass die Bridge
per Broadcast sendet — dann wird auch ein MulticastLock gebraucht.
`android:usesCleartextTraffic="true"` setzen, falls die Bridge je über HTTP
angesprochen wird. Für UDP nicht erforderlich.
---
## 4. Die Routing-Frage — Leiter statt Festlegung
Das mLRS-Netz hat kein Internet. Die Sorge ist, dass Android den Verkehr weiter
über Mobilfunk leitet und die UDP-Pakete verschwinden.
**Wie gravierend das ist, ist offen.** Weder Mission Planner noch Kite GC bindet
das Netz — im gesamten Mission-Planner-Quelltext gibt es kein
`bindProcessToNetwork` und kein `WifiNetworkSpecifier`, und die dafür nötige
Berechtigung `CHANGE_NETWORK_STATE` wird nicht einmal angefordert. Bei
`targetSdkVersion 35`. Entweder tritt das Problem seltener auf als beschrieben,
oder beide Projekte überlassen die Lösung dem Nutzer (mobile Daten abschalten).
**Deshalb: von unten nach oben durchprobieren, beim ersten Erfolg aufhören.**
### Stufe 0 — Messung (zuerst, vor allem anderen)
Kleiner Versuch auf echter Hardware, **mit aktiven mobilen Daten** — ohne
zweites Netz misst man nichts.
1. UDP-Socket auf 14550 binden, Paket an die Bridge senden
2. Prüfen, ob eine Antwort kommt
3. Parallel eine HTTP-Anfrage absetzen und prüfen, ob sie durchgeht
Ergebnis schriftlich festhalten, es entscheidet über alles Weitere.
### Stufe 1 — ohne Bindung
Wenn Stufe 0 erfolgreich war: fertig. Kein Sonderfall, keine Plattform-Kanäle.
### Stufe 2 — Bindungsfenster
Falls UDP nicht ankommt. Android bindet laut Dokumentation **alle künftig
erzeugten Sockets** an das gesetzte Netz, und die Bindung haftet am Socket:
1. Prozess an das WLAN binden
2. UDP-Socket in Dart erzeugen
3. Prozessbindung sofort wieder lösen
Der Socket bleibt am WLAN, spätere Verbindungen (Karten, Wetter) laufen wieder
über die Standardroute. Kostet einen Plattform-Kanal mit zwei Methoden.
**Unbestätigt:** ob die Prozessmarkierung auch für Sockets greift, die Dart auf
seinen eigenen I/O-Threads erzeugt. Der Mechanismus spricht dafür, dokumentiert
ist es nicht. Deshalb steht Stufe 0 am Anfang.
Achtung: Trennt sich das Netz, hören so gebundene Sockets nach Dokumentation
auf zu funktionieren — bewusst so gestaltet. Die Wiederverbindung muss den
Socket verwerfen und neu erzeugen, und dabei das Fenster wiederholen.
### Stufe 3 — nativer Socket
Falls Stufe 2 scheitert. UDP-Socket in Kotlin öffnen, per
`Network.bindSocket(DatagramSocket)` einzeln binden, Daten über einen
EventChannel nach Dart. Android empfiehlt Socket-genaue Bindung ausdrücklich
gegenüber der Prozessbindung; aus Dart ist sie nur nicht erreichbar, weil weder
Socket-Objekt noch Deskriptor herausgegeben werden.
### Verbindungsaufbau mit dem WLAN
Unabhängig von der Stufe. Zwei Wege:
- **Einfach:** Nutzer verbindet sich über die Systemeinstellungen. Die App prüft
nur, ob ein Netz mit Präfix `mLRS-` aktiv ist, und weist sonst darauf hin.
- **Komfortabel:** `WifiNetworkSpecifier` mit **Präfix-Muster**
(`setSsidPattern("mLRS-", PATTERN_PREFIX)`), weil die SSID eine Zufallszahl
enthält. Der Systemdialog zeigt dann nur die Bridge, der Nutzer bestätigt.
Braucht `CHANGE_NETWORK_STATE`. Verbindung ist an die App gebunden und wird
beim Beenden getrennt — für eine Bodenstation eher praktisch.
Für den MVP reicht der einfache Weg.
---
## 5. MSP
### Rahmenformat
iNAV erwartet **MSPv2**. MSPv1 reicht nicht.
```
'$' 'X' <dir> <flag:1> <function:2 LE> <payloadSize:2 LE> <payload> <crc:1>
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
---
## 6. 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.
---
## 7. Bedienung
### Fly-Modus
Beim Wechsel: Verbindung aufbauen, sobald verbunden Polling starten. Beim
Verlassen sauber schließen und — falls Stufe 2 oder 3 — die Bindung lösen.
**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 Ausbleiben der Telemetrie kennzeichnen, **nicht**
den letzten Wert stehenlassen.
Wiederverbinden mit ansteigender Wartezeit (1, 2, 4 … max. 30 s).
### Einstellungen, Abschnitt „Verbindung"
- **Art:** WLAN · USB (später)
- **Host/Port:** Vorgabe automatisch (Gateway) / 14550, überschreibbar
- **Beim Wechsel in Fly automatisch verbinden:** an/aus, Standard an
- **Verbindungsprotokoll:** einfache Textansicht der letzten Ereignisse
(verbunden, getrennt, Fehler, gelernte Gegenstelle, Paketzähler). Spart im
Feld viel Ratearbeit.
---
## 8. Abnahmekriterien
- [ ] Stufe-0-Messung durchgeführt, Ergebnis dokumentiert, Stufe begründet gewählt
- [ ] 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 → automatischer Neuaufbau, Zustand sichtbar
- [ ] Verlassen des Fly-Modus schließt Verbindung und löst eine etwaige Bindung
- [ ] Kein Netzwerk-Import außerhalb von `transport/`
- [ ] Karten und Wetter funktionieren im Fly-Modus weiterhin (bei Stufe 2/3 der
eigentliche Prüfstein)
---
## 9. 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`
(Kommentar erklärt das Portbindungs- und Peer-Lernen-Problem ausführlich),
Mission Planner `ExtLibs/Comms/CommsUdpSerial.cs` (dieselbe Lösung in C#).