273 lines
10 KiB
Markdown
273 lines
10 KiB
Markdown
# DMC — Kommunikationsschicht
|
||
|
||
Erweiterung der bestehenden Flutter-App um die Verbindung zum Fluggerät.
|
||
Diese Datei beschreibt **nur** Transport, MSP und die zugehörige Bedienung.
|
||
Planung, Karte, Wind und Gelände sind bereits vorhanden und werden nicht berührt.
|
||
|
||
---
|
||
|
||
## 1. Aufgabe
|
||
|
||
Die App soll im Fly-Modus per Funk mit dem Flight Controller sprechen:
|
||
Telemetrie lesen, später Missionen hochladen. Der Weg führt über die
|
||
Fernsteuerung, nicht über eine eigene Funkstrecke.
|
||
|
||
```
|
||
Handy ──Bluetooth SPP──► RadioMaster Pocket (mLRS Tx + Backpack)
|
||
│ 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 funktioniert
|
||
parallel unverändert weiter.
|
||
|
||
**MVP:** Bluetooth Classic (SPP), nur Android.
|
||
**Später:** WLAN (UDP) als in den Einstellungen wählbare Alternative.
|
||
|
||
---
|
||
|
||
## 2. Transportschnittstelle
|
||
|
||
Zuerst bauen. Alles Weitere wird ausschließlich gegen diese Schnittstelle
|
||
programmiert, damit WLAN später eine zweite Implementierung ist und kein Umbau.
|
||
|
||
```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:
|
||
- `BluetoothClassicTransport` — MVP
|
||
- `UdpTransport` — später
|
||
- `LoopbackTransport` — für Tests und Entwicklung ohne Hardware
|
||
|
||
Der MSP-Code darüber kennt ausschließlich `LinkTransport`. Kein
|
||
`import` von Bluetooth- oder Netzwerkpaketen außerhalb von `transport/`.
|
||
|
||
---
|
||
|
||
## 3. Bluetooth Classic (MVP)
|
||
|
||
### Warum Classic und nicht BLE
|
||
|
||
MSP ist ein Byte-Strom, und SPP liefert genau das. BLE kennt nur GATT mit kurzen
|
||
Attributen; ein serieller Kanal müsste dort über einen UART-Dienst nachgebaut
|
||
werden, inklusive eigener Zerlegung und Wiederzusammensetzung der MSP-Nachrichten.
|
||
|
||
Zweiter Grund: Bluetooth berührt den Netzwerk-Stack nicht. WLAN und Mobilfunk
|
||
bleiben im Flug frei für Kartenkacheln und Wetterabruf. Bei WLAN ist genau das
|
||
das Problem (siehe 6).
|
||
|
||
BLE bleibt relevant, falls iOS dazukommt — dort gibt es kein SPP.
|
||
|
||
### Paket und Konstanten
|
||
|
||
```yaml
|
||
dependencies:
|
||
flutter_classic_bluetooth: ^<aktuell> # RFCOMM/SPP, gepflegt
|
||
permission_handler: ^<aktuell>
|
||
```
|
||
|
||
SPP-UUID: `00001101-0000-1000-8000-00805F9B34FB`
|
||
|
||
### Verbindungsablauf beim Tippen auf „Fly"
|
||
|
||
1. **Bluetooth an?**
|
||
Wenn nicht: Hinweis mit Knopf in die Systemeinstellungen.
|
||
`BluetoothAdapter.enable()` ist ab API 31 nicht mehr nutzbar — die App kann
|
||
Bluetooth nicht selbst einschalten.
|
||
|
||
2. **Gerät bestimmen — ohne zu suchen.**
|
||
`getBondedDevices()` liefert die gekoppelten Geräte sofort. Auswahl in dieser
|
||
Reihenfolge:
|
||
a) gemerkte MAC-Adresse aus den Einstellungen
|
||
b) erstes gekoppeltes Gerät, dessen Name mit `mLRS` beginnt
|
||
c) sonst: Auswahlliste anzeigen
|
||
|
||
**Nicht scannen**, wenn ein gekoppeltes Gerät gefunden wurde. Ein Scan kostet
|
||
Sekunden, braucht eine zusätzliche Berechtigung und ist unnötig.
|
||
|
||
3. **Laufende Suche abbrechen**, falls doch eine läuft. Andernfalls schlägt der
|
||
Verbindungsaufbau fehl oder wird sehr langsam — klassischer Android-Fallstrick.
|
||
|
||
4. **Socket öffnen**, `LinkState.connected` melden, MSP-Polling starten.
|
||
|
||
Zielzeit: unter 3 Sekunden, ohne einen einzigen Dialog.
|
||
|
||
### Kopplung
|
||
|
||
Bleibt einmalig manuell über den Systemdialog, das ist nicht umgehbar. Danach
|
||
bleiben die Geräte gekoppelt und verbinden sich bei künftigen Sitzungen
|
||
automatisch, solange die Kopplung nicht gelöscht wird.
|
||
|
||
In den Einstellungen ein Knopf „Gerät koppeln", der die Bluetooth-Systemeinstellungen
|
||
öffnet, plus ein kurzer Hinweistext.
|
||
|
||
### Berechtigungen
|
||
|
||
| Ab Android 12 (API 31) | Darunter |
|
||
|---|---|
|
||
| `BLUETOOTH_CONNECT` (Laufzeit) | `BLUETOOTH`, `BLUETOOTH_ADMIN` |
|
||
| `BLUETOOTH_SCAN` nur falls gescannt wird | `ACCESS_FINE_LOCATION` |
|
||
|
||
`BLUETOOTH_CONNECT` erscheint dem Nutzer als **„Geräte in der Nähe"**.
|
||
|
||
Wichtig: Wird sie beim ersten Mal abgelehnt, fragt Android **kein zweites Mal**.
|
||
Diesen Fall abfangen und einen erklärenden Hinweis mit Knopf in die
|
||
App-Einstellungen zeigen — nicht bloß „Verbindung fehlgeschlagen".
|
||
|
||
### Robustheit
|
||
|
||
- Wiederverbinden mit ansteigender Wartezeit (1 s, 2 s, 4 s, … max. 30 s)
|
||
- Bei `LinkState.error` den Grund mitführen und in der Oberfläche anzeigen
|
||
- **Alter des letzten empfangenen MSP-Pakets** dauerhaft sichtbar machen.
|
||
Im Feld ist das der einzige verlässliche Hinweis, ob die Werte noch aktuell
|
||
sind. Vorschlag: grün < 2 s, bernstein < 5 s, rot darüber.
|
||
- Verbindung beim Verlassen des Fly-Modus sauber schließen
|
||
|
||
---
|
||
|
||
## 4. MSP
|
||
|
||
### Rahmenformat
|
||
|
||
iNAV erwartet **MSPv2**. MSPv1 reicht nicht — der iNAV-Configurator sendet
|
||
ebenfalls v2, und die MSP-über-CRSF-Umsetzung scheitert genau daran.
|
||
|
||
```
|
||
'$' '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 implementieren: Kodierer, Streaming-Dekodierer (der Byte-Strom kommt
|
||
fragmentiert an, also Zustandsautomat statt „ein Paket pro Lesevorgang"),
|
||
CRC-Prüfung, Verwerfen fehlerhafter Rahmen.
|
||
|
||
### Polling
|
||
|
||
MSP ist ein Frage-Antwort-Protokoll: die App fragt, der FC antwortet. Es kommt
|
||
nichts von selbst. Also ein eigener Takt:
|
||
|
||
| Zweck | Rate |
|
||
|---|---|
|
||
| Position, Lage, Höhe | 5–10 Hz |
|
||
| Spannung, Strom | 1 Hz |
|
||
| Status, Flugmodus | 2 Hz |
|
||
|
||
Anfragen serialisieren: immer nur eine offene Anfrage, mit Zeitgrenze
|
||
(z. B. 500 ms) und Wiederholung. Sonst laufen bei schwacher Strecke die
|
||
Antworten durcheinander.
|
||
|
||
**Befehlsnummern nicht aus dem Gedächtnis übernehmen.** Gegen
|
||
`msp_protocol.h` beziehungsweise `msp_protocol_v2_inav.h` im iNAV-Repository
|
||
prüfen und als Konstanten an einer Stelle sammeln.
|
||
|
||
### Reihenfolge der Umsetzung
|
||
|
||
1. Rahmen kodieren/dekodieren + Tests gegen aufgezeichnete Bytefolgen
|
||
2. Telemetrie lesen und in der bestehenden Fly-Anzeige darstellen
|
||
(`LIVE`-Zeilen bei Höhe und Geschwindigkeit sind bereits vorhanden)
|
||
3. Erst danach Missionsupload
|
||
|
||
---
|
||
|
||
## 5. Gegenstelle (keine App-Arbeit, aber Voraussetzung)
|
||
|
||
Zur Einordnung, damit beim Testen klar ist, woran es liegt, wenn nichts kommt.
|
||
|
||
**mLRS-Empfänger:** `Rx Ser Link Mode` = `mspX`, `Rx Ser Baudrate` = 115200,
|
||
`Rx Snd RcChannel` = `rc override` oder `rc channels`.
|
||
|
||
**mLRS-Sender:** `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 autonom fliegende Fixed-Wing unkritisch.
|
||
|
||
---
|
||
|
||
## 6. WLAN (später)
|
||
|
||
Vorbereiten, aber nicht im MVP umsetzen. Die Fallstricke hier festhalten, damit
|
||
sie nicht neu entdeckt werden müssen.
|
||
|
||
Die Bridge erzeugt `mLRS-xxxx AP UDP`, ohne Passwort, UDP-Port **14550**.
|
||
|
||
**Präfix-Verbindung.** Die SSID enthält eine Zufallszahl, der genaue Name ist
|
||
also unbekannt. `WifiNetworkSpecifier` mit `setSsidPattern(PatternMatcher("mLRS-",
|
||
PATTERN_PREFIX))` — der Systemdialog zeigt dann nur die Bridge, der Nutzer
|
||
bestätigt bloß.
|
||
|
||
**Kein Internet auf diesem Netz.** Ohne Bindung routet Android weiter über
|
||
Mobilfunk, und die UDP-Pakete verschwinden, obwohl das WLAN verbunden aussieht.
|
||
Zwei Wege:
|
||
|
||
- *Einfach:* `forceWifiUsage(true)` aus `wifi_iot`. Bindet aber **den gesamten
|
||
Prozess** — solange es aktiv ist, sind Kartenkacheln und Wetter nicht mehr
|
||
erreichbar. Nur im Fly-Modus setzen und beim Verlassen sofort lösen. Es gibt
|
||
Berichte über gerätespezifische Aussetzer.
|
||
- *Sauber:* UDP-Socket nativ in Kotlin öffnen, per `Network.bindSocket()`
|
||
**einzeln** binden, Daten über einen EventChannel nach Dart streamen. Dann
|
||
läuft MSP über die Bridge und alles andere weiter über Mobilfunk.
|
||
|
||
**Zieladresse nicht fest eincodieren.** Gateway-Adresse des Netzes verwenden
|
||
(das ist der ESP) oder die Absenderadresse des ersten eingehenden Pakets merken.
|
||
|
||
---
|
||
|
||
## 7. Einstellungen
|
||
|
||
Neuer Abschnitt „Verbindung":
|
||
|
||
- **Art:** Bluetooth · WLAN (ausgegraut bis umgesetzt) · USB (später)
|
||
- **Gerät:** Auswahlliste der gekoppelten Geräte, Standard
|
||
„automatisch (Name beginnt mit mLRS)"
|
||
- **Beim Wechsel in Fly automatisch verbinden:** an/aus, Standard an
|
||
- **Gerät koppeln** → öffnet die Bluetooth-Systemeinstellungen
|
||
- **Verbindungsprotokoll** → einfache Textansicht der letzten Ereignisse
|
||
(verbunden, getrennt, Fehler, Paketzähler). Spart im Feld viel Ratearbeit.
|
||
|
||
---
|
||
|
||
## 8. Abnahmekriterien
|
||
|
||
- [ ] Nach einmaliger Kopplung verbindet ein Tipp auf „Fly" in unter 3 Sekunden
|
||
ohne Dialog
|
||
- [ ] Bluetooth aus → verständlicher Hinweis mit Knopf in die Einstellungen,
|
||
kein stiller Fehlschlag
|
||
- [ ] Berechtigung abgelehnt → erklärender Hinweis mit Weg in die App-Einstellungen
|
||
- [ ] Verbindung wird während des Betriebs getrennt → automatischer Neuaufbau,
|
||
Zustand jederzeit in der Oberfläche sichtbar
|
||
- [ ] Telemetriealter ist immer sichtbar und färbt sich bei Veralten um
|
||
- [ ] 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
|
||
- [ ] Verlassen des Fly-Modus schließt die Verbindung
|
||
- [ ] Kein Bluetooth-Import außerhalb von `transport/`
|
||
|
||
---
|
||
|
||
## 9. Hinweise
|
||
|
||
- Ohne Hardware entwickelbar: `LoopbackTransport` plus aufgezeichnete
|
||
MSP-Antworten. Erst danach am echten Gerät gegenprüfen.
|
||
- Sicherheitsrelevante Anzeigen nie mit Platzhaltern füllen. Kommt keine
|
||
Telemetrie, muss das sichtbar sein — nicht ein eingefrorener alter Wert.
|
||
- Die App steuert das Fluggerät nicht. RC bleibt vollständig bei der
|
||
Fernsteuerung; MSP dient dem Lesen und dem Übertragen von Missionen.
|