Adds MSP_WP (118) support to query the flightcontroller's stored home point (WP#0 is a special case for this in iNAV's getWaypoint(), per navigation.c) - the FC reports (0,0,0) rather than an error before one is set, so parseMspHomePoint() treats that pair as "unset". Rendered with a small pentagon house icon (design/homepoint-pentagon.svg, ported to a CustomPainter like the other map icons). Deliberately not polled continuously: a new home_point_provider.dart refreshes it only at the three moments the FC's home point can actually change - connect/reconnect, GPS fix acquired, and arming (iNAV's default reset_home_type=FIRST_ARM only freezes it at the first arm; before that it continuously follows the aircraft while disarmed). Mock's implementation offsets the point 25m from the anchor so it doesn't sit exactly under the drone marker during UI testing.
163 lines
5.8 KiB
Dart
163 lines
5.8 KiB
Dart
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<void> 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<void> disconnect() async {
|
|
await _poller?.dispose();
|
|
_poller = null;
|
|
_client?.dispose();
|
|
_client = null;
|
|
await transport.disconnect();
|
|
}
|
|
|
|
@override
|
|
Future<void> 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<bool> 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<void> setFlightMode(FlightMode mode) => throw UnimplementedError(
|
|
'Flugmodus-Wechsel ist noch nicht umgesetzt.',
|
|
);
|
|
|
|
@override
|
|
Future<void> arm() => throw UnimplementedError(
|
|
'Armen ueber MSP ist noch nicht umgesetzt.',
|
|
);
|
|
|
|
@override
|
|
Stream<TelemetryFrame> subscribeTelemetry() {
|
|
final poller = _poller;
|
|
if (poller == null) {
|
|
throw StateError('subscribeTelemetry() vor connect() aufgerufen.');
|
|
}
|
|
return poller.frames;
|
|
}
|
|
|
|
@override
|
|
Future<int?> readActiveWaypointIndex() async {
|
|
final client = _client;
|
|
if (client == null) {
|
|
throw StateError('readActiveWaypointIndex() vor connect() aufgerufen.');
|
|
}
|
|
return parseMspNavStatusActiveWaypoint(
|
|
await client.request(MspCommands.navStatus),
|
|
);
|
|
}
|
|
|
|
/// Fragt WP#0 (Homepoint, siehe `MspCommands.getWp`-Doku) gezielt per
|
|
/// `MSP_WP` ab - bewusst kein Teil von [MspTelemetryPoller]s Zyklus (Doku:
|
|
/// "Homepoint muss nicht kontinuierlich abgefragt werden"), Aufrufer ist
|
|
/// home_point_provider.dart.
|
|
@override
|
|
Future<HomePoint?> readHomePoint() async {
|
|
final client = _client;
|
|
if (client == null) {
|
|
throw StateError('readHomePoint() vor connect() aufgerufen.');
|
|
}
|
|
final payload = await client.request(
|
|
MspCommands.getWp,
|
|
payload: encodeMspGetWaypointRequest(mspWpNumberHome),
|
|
);
|
|
return parseMspHomePoint(payload);
|
|
}
|
|
}
|