Die simulierte Telemetrie war fest auf 6S verdrahtet (18.0-24.0V), obwohl das Drohnenprofil laengst eine konfigurierbare Zellenzahl hat - beim T1-Ranger-Standardprofil (4S) zeigte die Statusanzeige dadurch bis zu 18V, obwohl 16.8V (4 * 4.2V Ladeschluss) der realistische Maximalwert ist. MockFlightControllerLink nimmt jetzt batteryCellCount entgegen und skaliert den simulierten Spannungsverlauf (4.2V/Zelle voll bis 3.0V/Zelle nahezu leer) damit. telemetry_provider.dart uebergibt dafuer die Zellenzahl des aktuell aktiven Drohnenprofils (ref.read, analog zu den initialWaypoints - einmalig bei der Verbindungserzeugung, kein Reset des laufenden Akkustands bei spaeteren Profilwechseln). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
196 lines
7.9 KiB
Dart
196 lines
7.9 KiB
Dart
import 'dart:async';
|
|
import 'dart:math' as math;
|
|
|
|
import '../../domain/waypoint/flat_waypoint_list.dart';
|
|
import '../flight_controller_link.dart';
|
|
|
|
/// Synthetische Telemetrie fuer UI-Entwicklung/Tests ohne Hardware oder SITL
|
|
/// (Architektur-Doku 3.1).
|
|
///
|
|
/// Verankert am ersten Wegpunkt der uebergebenen Mission (Doku: "solange die
|
|
/// Drohne nicht verbunden ist, Mock-Daten fuer die neue Live-Details-
|
|
/// Ansicht nutzbar machen") statt an einem fest verdrahteten Ort (frueher
|
|
/// Berlin) - dadurch liegt die simulierte Position tatsaechlich auf der
|
|
/// gerade bearbeiteten Route, und activeWaypointIndex ist von Anfang an
|
|
/// gesetzt (0, "erster Wegpunkt aktiv"), auch ohne den noch nicht
|
|
/// umgesetzten Missions-Upload-Flow. Fehlt eine Mission (keine Wegpunkte),
|
|
/// bleibt der alte Berlin-Fallback bestehen, damit die Klasse auch ganz
|
|
/// ohne Mission nutzbar ist.
|
|
class MockFlightControllerLink implements FlightControllerLink {
|
|
MockFlightControllerLink({
|
|
List<Waypoint> initialWaypoints = const [],
|
|
int batteryCellCount = 4,
|
|
}) : _anchorLat = initialWaypoints.isEmpty ? 52.5 : initialWaypoints.first.lat,
|
|
_anchorLon = initialWaypoints.isEmpty ? 13.4 : initialWaypoints.first.lon,
|
|
_activeWaypointIndex = initialWaypoints.isEmpty ? null : 0,
|
|
_batteryCellCount = batteryCellCount;
|
|
|
|
double _anchorLat;
|
|
double _anchorLon;
|
|
|
|
/// Zellenzahl des simulierten Akkus (Doku DroneProfile.batteryCellCount) -
|
|
/// bestimmt den Spannungsbereich der Mock-Telemetrie (siehe
|
|
/// batteryVoltage unten), damit die Statusanzeige mit den Ampel-
|
|
/// Schwellwerten des aktiven Profils zusammenpasst statt fest auf 6S
|
|
/// verdrahtet zu sein.
|
|
final int _batteryCellCount;
|
|
final _random = math.Random();
|
|
|
|
bool _armed = false;
|
|
int? _activeWaypointIndex;
|
|
StreamController<TelemetryFrame>? _telemetryController;
|
|
Timer? _ticker;
|
|
double _heading = 0;
|
|
double _batteryPercent = 100;
|
|
|
|
/// Aktuelle Drift vom Ankerpunkt in Metern (Nord/Ost) - Zufalls-Trudeln
|
|
/// innerhalb eines 10-m-Radius statt eines fest stehenden Punkts, damit
|
|
/// auch die Distanz/ETA-Anzeige der neuen Details-Pille sichtbar auf
|
|
/// Werteaenderungen reagiert (Doku 9: UI-Entwicklung ohne Hardware).
|
|
double _driftNorthM = 0;
|
|
double _driftEastM = 0;
|
|
static const _driftRadiusM = 10.0;
|
|
static const _driftStepM = 1.5;
|
|
static const _metersPerDegreeLat = 111320.0;
|
|
|
|
@override
|
|
FcCapabilities get capabilities => const FcCapabilities(
|
|
supportsMultiMission: true,
|
|
supportsInFlightUpload: false,
|
|
maxWaypoints: 60,
|
|
);
|
|
|
|
@override
|
|
Future<void> connect() async {
|
|
// Idempotent (Doku flightControllerLinkProvider: "Verbindung bleibt
|
|
// ueber Moduswechsel hinweg aktiv") - sonst wuerde jeder erneute
|
|
// Eintritt in den Fly-Modus einen zweiten Timer/Controller anlegen und
|
|
// den vorherigen verwaist weiterlaufen lassen.
|
|
if (_telemetryController != null) return;
|
|
_telemetryController = StreamController<TelemetryFrame>.broadcast();
|
|
_ticker = Timer.periodic(const Duration(seconds: 1), (_) {
|
|
// Kurs dreht sich langsam weiter statt fest zu stehen, damit die
|
|
// Rotation des Drohnen-Markers (DroneMarkerIcon) auch ohne Hardware
|
|
// sichtbar ueberprueft werden kann (Doku 9: UI-Entwicklung ohne
|
|
// Hardware).
|
|
_heading = (_heading + 5) % 360;
|
|
// Langsam sinkender Akkustand, damit die Anzeige auch ohne Hardware
|
|
// sichtbar auf Werteaenderungen reagiert (Doku 9).
|
|
_batteryPercent = (_batteryPercent - 0.1).clamp(0, 100);
|
|
|
|
// Zufallsschritt, danach auf den 10-m-Radius um den Ankerpunkt
|
|
// zurueckgeklemmt statt frei wegzulaufen.
|
|
_driftNorthM += (_random.nextDouble() - 0.5) * 2 * _driftStepM;
|
|
_driftEastM += (_random.nextDouble() - 0.5) * 2 * _driftStepM;
|
|
final driftMagnitude = math.sqrt(
|
|
_driftNorthM * _driftNorthM + _driftEastM * _driftEastM,
|
|
);
|
|
if (driftMagnitude > _driftRadiusM) {
|
|
final scale = _driftRadiusM / driftMagnitude;
|
|
_driftNorthM *= scale;
|
|
_driftEastM *= scale;
|
|
}
|
|
final metersPerDegreeLon =
|
|
_metersPerDegreeLat * math.cos(_anchorLat * math.pi / 180);
|
|
final lat = _anchorLat + _driftNorthM / _metersPerDegreeLat;
|
|
final lon = _anchorLon + _driftEastM / metersPerDegreeLon;
|
|
|
|
_telemetryController?.add(TelemetryFrame(
|
|
lat: lat,
|
|
lon: lon,
|
|
hasFix: true,
|
|
numSat: 12,
|
|
altitudeM: 120,
|
|
speedMs: 18,
|
|
headingDeg: _heading,
|
|
armed: _armed,
|
|
batteryPercent: _batteryPercent.round(),
|
|
// Spannung sinkt mit dem Mock-Ladestand von voll (4.2 V/Zelle,
|
|
// LiPo-Ladeschluss) auf nahezu leer (3.0 V/Zelle) mit, skaliert mit
|
|
// der Zellenzahl des aktiven Drohnenprofils statt fest auf 6S
|
|
// verdrahtet zu sein (Doku: "bei 4s ist 16.8 Volt aber der max
|
|
// Wert") - damit reagiert auch die neue Statusanzeige (Doku: "Drone
|
|
// battery Voltage") auf Werteaenderungen und durchlaeuft dabei
|
|
// realistisch die Ampel-Schwellwerte des Profils.
|
|
batteryVoltage:
|
|
_batteryCellCount * (3.0 + (_batteryPercent / 100) * 1.2),
|
|
currentA: 12.5,
|
|
linkQuality: 95,
|
|
snrDb: 9,
|
|
hdop: 0.9,
|
|
// MW_GPS_MODE_NAV (3), sobald armed und eine Mission aktiv ist -
|
|
// analog zu flightMode unten, damit computeDroneStatus (Doku:
|
|
// "drone state ... waypoint") auch mit Mock-Daten testbar ist.
|
|
navMode: _armed && _activeWaypointIndex != null ? 3 : 0,
|
|
flightMode: _activeWaypointIndex != null
|
|
? 'Waypoint mission (en route)'
|
|
: 'Idle',
|
|
activeWaypointIndex: _activeWaypointIndex,
|
|
));
|
|
});
|
|
}
|
|
|
|
@override
|
|
Future<void> disconnect() async {
|
|
_ticker?.cancel();
|
|
await _telemetryController?.close();
|
|
_telemetryController = null;
|
|
}
|
|
|
|
/// Setzt die simulierte Position auf den ersten Wegpunkt von [waypoints]
|
|
/// zurueck und loescht die bisherige Drift (Doku: "beim Laden einer
|
|
/// Mission Mock-Drohnenposition auf ersten Wegpunkt setzen, solange die
|
|
/// Drohne nicht verbunden ist") - der Aufrufer (CurrentMissionMetaNotifier)
|
|
/// ruft dies nur auf, wenn [FlightControllerLink] tatsaechlich dieser Mock
|
|
/// ist, also noch keine echte Verbindung (WLAN/MSP) aktiv ist. Ohne
|
|
/// Wegpunkte faellt die Position auf den alten Berlin-Fallback zurueck,
|
|
/// analog zum Konstruktor.
|
|
void resetPosition(List<Waypoint> waypoints) {
|
|
_anchorLat = waypoints.isEmpty ? 52.5 : waypoints.first.lat;
|
|
_anchorLon = waypoints.isEmpty ? 13.4 : waypoints.first.lon;
|
|
_driftNorthM = 0;
|
|
_driftEastM = 0;
|
|
_activeWaypointIndex = waypoints.isEmpty ? null : 0;
|
|
}
|
|
|
|
@override
|
|
Future<void> uploadMission(FlatWaypointList mission) async {
|
|
// iNAV/MAVLink erlauben Mission-Upload nur im unarmed-Zustand (Doku 4.5).
|
|
if (_armed) {
|
|
throw StateError('Mission-Upload verweigert: Drohne ist armed.');
|
|
}
|
|
_uploadedWaypointCount = mission.waypoints.length;
|
|
_activeWaypointIndex = mission.waypoints.isEmpty ? null : 0;
|
|
}
|
|
|
|
/// Kein separates FC-seitiges "gueltig"-Flag zu simulieren (anders als
|
|
/// MspFlightControllerLink, das echte MSP_WP_GETINFO-Antworten abfragt) -
|
|
/// der Mock haelt die zuletzt hochgeladene Wegpunktzahl und vergleicht sie
|
|
/// hier direkt, das reicht fuer UI-Entwicklung ohne Hardware.
|
|
int? _uploadedWaypointCount;
|
|
|
|
@override
|
|
Future<bool> verifyMission(FlatWaypointList mission) async =>
|
|
_uploadedWaypointCount == mission.waypoints.length;
|
|
|
|
@override
|
|
Future<void> setFlightMode(FlightMode mode) async {}
|
|
|
|
@override
|
|
Future<void> arm() async {
|
|
_armed = true;
|
|
}
|
|
|
|
@override
|
|
Stream<TelemetryFrame> subscribeTelemetry() {
|
|
final controller = _telemetryController;
|
|
if (controller == null) {
|
|
throw StateError('subscribeTelemetry() vor connect() aufgerufen.');
|
|
}
|
|
return controller.stream;
|
|
}
|
|
|
|
@override
|
|
Future<int?> readActiveWaypointIndex() async => _activeWaypointIndex;
|
|
}
|