wifi connection established by app, process socket binding, primary os connection untouched
This commit is contained in:
@@ -0,0 +1,333 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user