bluetooth connectivity layer added
This commit is contained in:
@@ -0,0 +1,272 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user