Files
dmc/DMC_Kommunikationsschicht v2.md

334 lines
13 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.
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.
```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` (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.
```kotlin
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
```xml
<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.