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
This commit is contained in:
2026-05-23 11:53:50 -04:00
parent d4d12caac7
commit c946d2df6d
6 changed files with 969 additions and 5 deletions
+167
View File
@@ -0,0 +1,167 @@
// =============================================================================
// 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();
}
}