c946d2df6d
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
168 lines
6.0 KiB
Dart
168 lines
6.0 KiB
Dart
// =============================================================================
|
||
// 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, 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();
|
||
}
|
||
}
|