Files
dmc/DMC_Kommunikationsschicht v2.md
T

13 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.

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.

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 (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.

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

<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>

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> <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

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.