Files
dmc/DMC_Kommunikationsschicht_neu.md
T

12 KiB
Raw Blame History

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.

  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#).