12 KiB
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.
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:
<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.
- UDP-Socket auf 14550 binden, Paket an die Bridge senden
- Prüfen, ob eine Antwort kommt
- 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:
- Prozess an das WLAN binden
- UDP-Socket in Dart erzeugen
- 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:
WifiNetworkSpecifiermit 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. BrauchtCHANGE_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
- Rahmen kodieren/dekodieren, Tests gegen aufgezeichnete Bytefolgen
- Telemetrie lesen, in die vorhandenen
LIVE-Zeilen einspeisen - 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:
LoopbackTransportplus 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 PlannerExtLibs/Comms/CommsUdpSerial.cs(dieselbe Lösung in C#).