306 lines
12 KiB
Markdown
306 lines
12 KiB
Markdown
# 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#).
|