Files
AR-Autopilot/display/lib/data/autopilot_state.dart
T
alro65 c946d2df6d feat(display): Sprint 4 — pantalla principal del autopilot
Archivos nuevos:
  display/lib/data/autopilot_state.dart
    - ChangeNotifier con todos los datos del autopilot
    - Simulación de velero en mar (heading drift / P-controller)
    - API pública estable: engage(), disengage(), adjustSetpoint(), selectMode()
    - Sprint 7: los internos se reemplazan por Modbus RTU, la API no cambia

  display/lib/screens/cockpit/cockpit_screen.dart
    - Pantalla principal: TopBar, ModeSelector, CompassRose, DataStrip,
      HeadingAdjustBar, RudderIndicator, ENGAGE/DISENGAGE
    - Logo con triple-tap para ciclar temas (StateWidget con Timer)
    - Indicador DEMO visible cuando isConnected == false
    - Engranaje → AppearanceSettingsScreen

  display/lib/widgets/themed/engage_button.dart
    - Botón verde con glow; dimmed cuando ya está engaged

  display/lib/widgets/themed/heading_adjust_bar.dart
    - Botones << < [SET 048.0°] > >>
    - Deshabilitado cuando mode == STANDBY

  display/lib/widgets/themed/status_chip.dart
    - Indicador de punto + label para NMEA / GPS (ok / warn / off)

Modificado:
  display/lib/main.dart
    - MultiProvider: agrega AutopilotState al árbol de providers
    - Ruta inicial: CockpitScreen.routeName ('/') en lugar de Appearance

AR_electronics — AR-Autopilot Project
2026-05-23 11:53:50 -04:00

168 lines
6.0 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// =============================================================================
// 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<AutopilotState>()`.
///
/// ## 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, 0359.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();
}
}