import '../../domain/waypoint/flat_waypoint_list.dart'; import '../flight_controller_link.dart'; import '../link_transport.dart'; import 'msp_client.dart'; import 'msp_commands.dart'; import 'msp_telemetry_codec.dart'; import 'msp_telemetry_poller.dart'; import 'msp_waypoint_codec.dart'; /// iNAV-Anbindung via MSP (Architektur-Doku 3.1), umgesetzt gegen den /// tatsaechlichen iNAV-9.1.0-Quellcode (Doku Kommunikationsschicht Abschnitt /// 4 - Befehlsnummern/Byte-Layouts gegen `msp_protocol.h`, /// `msp_protocol_v2_inav.h` und `fc_msp.c` geprueft, nicht aus dem /// Gedaechtnis uebernommen). /// /// Kennt nur die protokollneutrale [LinkTransport]-Abstraktion (Doku /// Kommunikationsschicht Abschnitt 2) - ob darunter Bluetooth Classic (MVP) /// oder spaeter WLAN/UDP steckt, ist fuer diese Klasse unsichtbar. /// /// Setzt die in der Doku vorgegebene Reihenfolge um: Rahmen kodieren/ /// dekodieren, Telemetrie lesen, Missionsupload (`MSP_SET_WP`, siehe /// msp_waypoint_codec.dart fuer das Byte-Layout und die /// Aktions-P1/P2/P3-Belegung). Flugmodus-Wechsel und Armen sind weiterhin /// bewusst nicht umgesetzt - eigene, sicherheitsrelevante Schritte, die /// eigene Sorgfalt brauchen. class MspFlightControllerLink implements FlightControllerLink { MspFlightControllerLink({required this.transport}); final LinkTransport transport; MspClient? _client; MspTelemetryPoller? _poller; @override FcCapabilities get capabilities => const FcCapabilities( supportsMultiMission: true, supportsInFlightUpload: false, // Konservativer iNAV-Default (navigation.h: NAV_MAX_WAYPOINTS = 15). // Viele Boards/Targets erlauben mehr; uploadMission() fragt das nicht // live per MSP_WP_GETINFO ab (dessen maxWaypoints-Feld wird nur zur // Verifikation nach dem Upload gelesen, siehe verifyMission()) - // ein client-seitiger Vorab-Check gegen diesen Default verhindert // trotzdem, dass wir versuchen, mehr Wegpunkte zu senden, als der FC // WP-Nummern dafuer reserviert (WP#1..#15). maxWaypoints: 15, ); @override Future connect() async { // Idempotent (Doku flightControllerLinkProvider: "Verbindung bleibt // ueber Moduswechsel hinweg aktiv") - jeder erneute Eintritt in den // Fly-Modus ruft connect() erneut auf derselben, weiterhin // bestehenden Instanz auf; ohne diese Sperre wuerde das einen zweiten // MspClient/-Poller nebenher starten, der um MSP-Antworten race // (MSP kennt keine Request-IDs, siehe MspClient-Doku). if (_poller != null) return; await transport.connect(); final client = MspClient(transport); _client = client; final poller = MspTelemetryPoller(client); _poller = poller; poller.start(); } @override Future disconnect() async { await _poller?.dispose(); _poller = null; _client?.dispose(); _client = null; await transport.disconnect(); } @override Future uploadMission(FlatWaypointList mission) async { final client = _client; if (client == null) { throw StateError('uploadMission() vor connect() aufgerufen.'); } final waypoints = mission.waypoints; if (waypoints.isEmpty) { throw ArgumentError('Mission ohne Wegpunkte kann nicht hochgeladen werden.'); } if (waypoints.length > capabilities.maxWaypoints) { throw ArgumentError( 'Mission hat ${waypoints.length} Wegpunkte, iNAV erlaubt hier ' 'maximal ${capabilities.maxWaypoints} (NAV_MAX_WAYPOINTS).', ); } // MSP_SET_WP kennt kein Batch-Kommando - der FC nimmt WP-fuer-WP nur // entweder WP#1 (startet damit eine neue Mission, resetWaypointList()) // oder direkt den naechsten erwarteten WP# an (navigation.c // setWaypoint()). Erst das NAV_WP_FLAG_LAST auf dem letzten Wegpunkt // macht die Mission fuer den FC gueltig, siehe verifyMission(). for (var i = 0; i < waypoints.length; i++) { final payload = encodeMspSetWaypoint( waypoints[i], wireIndex: i + 1, isLast: i == waypoints.length - 1, ); await client.request(MspCommands.setWp, payload: payload); } } @override Future verifyMission(FlatWaypointList mission) async { final client = _client; if (client == null) { throw StateError('verifyMission() vor connect() aufgerufen.'); } final info = parseMspWpGetInfo(await client.request(MspCommands.wpGetInfo)); return info.isValid && info.waypointCount == mission.waypoints.length; } @override Future setFlightMode(FlightMode mode) => throw UnimplementedError( 'Flugmodus-Wechsel ist noch nicht umgesetzt.', ); @override Future arm() => throw UnimplementedError( 'Armen ueber MSP ist noch nicht umgesetzt.', ); @override Stream subscribeTelemetry() { final poller = _poller; if (poller == null) { throw StateError('subscribeTelemetry() vor connect() aufgerufen.'); } return poller.frames; } @override Future readActiveWaypointIndex() async { final client = _client; if (client == null) { throw StateError('readActiveWaypointIndex() vor connect() aufgerufen.'); } return parseMspNavStatusActiveWaypoint( await client.request(MspCommands.navStatus), ); } }