10 KiB
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.
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— MVPUdpTransport— späterLoopbackTransport— 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
dependencies:
flutter_classic_bluetooth: ^<aktuell> # RFCOMM/SPP, gepflegt
permission_handler: ^<aktuell>
SPP-UUID: 00001101-0000-1000-8000-00805F9B34FB
Verbindungsablauf beim Tippen auf „Fly"
-
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. -
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 mitmLRSbeginnt c) sonst: Auswahlliste anzeigenNicht scannen, wenn ein gekoppeltes Gerät gefunden wurde. Ein Scan kostet Sekunden, braucht eine zusätzliche Berechtigung und ist unnötig.
-
Laufende Suche abbrechen, falls doch eine läuft. Andernfalls schlägt der Verbindungsaufbau fehl oder wird sehr langsam — klassischer Android-Fallstrick.
-
Socket öffnen,
LinkState.connectedmelden, 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.errorden 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
- Rahmen kodieren/dekodieren + Tests gegen aufgezeichnete Bytefolgen
- Telemetrie lesen und in der bestehenden Fly-Anzeige darstellen
(
LIVE-Zeilen bei Höhe und Geschwindigkeit sind bereits vorhanden) - 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)auswifi_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:
LoopbackTransportplus 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.