// ============================================================================= // data/autopilot_state.dart — Live autopilot data model // ============================================================================= // // Sprint 4: simulated demo data (vessel drifting / heading-hold correction). // Sprint 7: internals replaced by Modbus RTU over USB serial. // The public API (fields + methods) must not change between sprints. // ============================================================================= import 'dart:async'; import 'dart:math' as math; import 'package:flutter/foundation.dart'; import '../widgets/themed/mode_selector.dart'; /// Live state of the autopilot and navigation instruments. /// /// Consumed by [CockpitScreen] via `context.watch()`. /// /// ## Data sources (Sprint 4 → Sprint 7) /// Sprint 4: internal [Timer]-driven demo that simulates a vessel at sea. /// Sprint 7: [AutopilotStateModbus] subclass (or internal swap) feeds real /// Modbus RTU data received over USB serial from the AR-Concentrador. class AutopilotState extends ChangeNotifier { // ── Navigation data ────────────────────────────────────────────────────────── /// Current vessel heading from NMEA 2000 backbone (degrees, magnetic, 0–359.9). double headingDeg = 125.0; /// Operator-commanded heading setpoint sent to autopilot (degrees, magnetic). double setpointDeg = 125.0; /// Rudder angle in degrees. Negative = port, positive = starboard. double rudderDeg = 0.0; /// Speed over ground (knots). double sogKn = 6.2; /// Course over ground (degrees, true). double cogDeg = 127.0; /// Rate of turn (degrees per minute). Positive = turning starboard. double rotDpm = 0.0; /// Water depth below keel (metres). double depthM = 42.5; // ── Autopilot state ────────────────────────────────────────────────────────── /// Current autopilot mode as reported by the concentrador $PARP,STATUS. AutopilotMode mode = AutopilotMode.standby; /// True when the Modbus RTU link to the concentrador is active. /// Sprint 4: always false (demo mode label shown in UI). bool isConnected = false; // ── Internal ───────────────────────────────────────────────────────────────── Timer? _demoTimer; final _rng = math.Random(); AutopilotState() { _startDemo(); } // --------------------------------------------------------------------------- // Demo simulation (Sprint 4 only) // --------------------------------------------------------------------------- void _startDemo() { _demoTimer = Timer.periodic(const Duration(milliseconds: 500), (_) { _tick(); }); } void _tick() { switch (mode) { case AutopilotMode.standby: // Vessel drifts slightly — small random walk on heading and rudder. headingDeg = (headingDeg + (_rng.nextDouble() - 0.5) * 0.5) % 360; if (headingDeg < 0) headingDeg += 360; rudderDeg = (rudderDeg + (_rng.nextDouble() - 0.5) * 0.8) .clamp(-5.0, 5.0); rotDpm = rudderDeg * 0.3 + (_rng.nextDouble() - 0.5) * 0.2; case AutopilotMode.headingHold: // Simulated P-controller: autopilot drives rudder toward setpoint. final error = _angleDiff(setpointDeg, headingDeg); rudderDeg = (error * 1.2 + (_rng.nextDouble() - 0.5) * 0.5) .clamp(-35.0, 35.0); headingDeg = (headingDeg + error * 0.025 + (_rng.nextDouble() - 0.5) * 0.08) % 360; if (headingDeg < 0) headingDeg += 360; rotDpm = error * 0.4; case AutopilotMode.trackKeep: // Sprint 6: identical to headingHold for demo purposes. final error = _angleDiff(setpointDeg, headingDeg); rudderDeg = (error * 1.2).clamp(-35.0, 35.0); headingDeg = (headingDeg + error * 0.025) % 360; if (headingDeg < 0) headingDeg += 360; rotDpm = error * 0.4; } // COG tracks heading with a small lag. cogDeg = (headingDeg + rudderDeg * 0.15 + (_rng.nextDouble() - 0.5) * 0.3) % 360; if (cogDeg < 0) cogDeg += 360; notifyListeners(); } /// Signed difference in [−180, +180]: target − current. double _angleDiff(double target, double current) { double d = (target - current) % 360; if (d > 180) d -= 360; if (d < -180) d += 360; return d; } // --------------------------------------------------------------------------- // Commands — called by UI; Sprint 7 will also send Modbus frames here. // --------------------------------------------------------------------------- /// Engage heading hold on the current heading. void engage() { setpointDeg = headingDeg; mode = AutopilotMode.headingHold; notifyListeners(); } /// Return to STANDBY (manual helm). void disengage() { mode = AutopilotMode.standby; notifyListeners(); } /// Adjust the heading setpoint while in heading-hold mode. /// No-op in STANDBY or TRACK. void adjustSetpoint(double deltaDeg) { if (mode != AutopilotMode.headingHold) return; setpointDeg = (setpointDeg + deltaDeg) % 360; if (setpointDeg < 0) setpointDeg += 360; notifyListeners(); } /// Handle ModeSelector taps. void selectMode(AutopilotMode newMode) { switch (newMode) { case AutopilotMode.standby: disengage(); case AutopilotMode.headingHold: engage(); case AutopilotMode.trackKeep: // Sprint 6: engage track-keep mode (requires GPS cross-track error). mode = AutopilotMode.trackKeep; setpointDeg = headingDeg; notifyListeners(); } } @override void dispose() { _demoTimer?.cancel(); super.dispose(); } }