Files
AR-Autopilot/display/lib/data/autopilot_state.dart
T
alro65 abe9b764c7 feat(display): Sprint 7 — USB serial link to AR-Concentrador via NMEA $PARP
- Add flutter_libserialport dependency (pubspec.yaml)
- New ParpCodec: XOR-checksum NMEA parser + command builders for all PARP sentences
- New ConcentradorService: manages two independent COM ports (RX-OUT broadcast,
  TX-IN commands) at 115200/8N1; auto-fires onConnectionChanged on link drop
- AutopilotState: dual-mode operation (demo timer OR live serial); connectToSerial /
  disconnectSerial; command methods (engage/disengage/adjustSetpoint) forward to
  ConcentradorService when connected; falls back to demo on disconnect
- New PortSettingsScreen (/settings/ports): RX+TX dropdowns populated from
  SerialPort.availablePorts, persisted in SharedPreferences; Connect/Disconnect
  buttons with error display and snackbar feedback
- main.dart: auto-connect to saved ports on startup (silent fail → demo mode);
  registers /settings/ports route
- CockpitScreen: gear icon replaced with PopupMenuButton (Puertos COM / Apariencia)

AR_electronics — AR-Autopilot Project
2026-05-24 01:28:04 -04:00

206 lines
7.0 KiB
Dart

// =============================================================================
// data/autopilot_state.dart — Live autopilot data model
// =============================================================================
//
// Dual-mode: demo simulation (Sprint 4) or live USB serial (Sprint 7).
//
// Default constructor starts in demo mode (animated vessel simulation).
// Call [connectToSerial] to switch to live data from the AR-Concentrador.
// If the serial link drops, the state automatically falls back to demo.
//
// The public API (fields + methods) is identical in both modes — the UI
// never needs to know which mode is active; it reads [isConnected] for
// the status indicator only.
// =============================================================================
import 'dart:async';
import 'dart:math' as math;
import 'package:flutter/foundation.dart';
import '../services/concentrador_service.dart';
import '../services/parp_codec.dart';
import '../widgets/themed/mode_selector.dart';
class AutopilotState extends ChangeNotifier {
// ── Navigation data ──────────────────────────────────────────────────────────
double headingDeg = 125.0;
double setpointDeg = 125.0;
double rudderDeg = 0.0;
double sogKn = 6.2;
double cogDeg = 127.0;
double rotDpm = 0.0;
double depthM = 42.5;
// ── Autopilot state ──────────────────────────────────────────────────────────
AutopilotMode mode = AutopilotMode.standby;
/// True when the USB serial link to the concentrador is active.
bool isConnected = false;
// ── Serial service ───────────────────────────────────────────────────────────
ConcentradorService? _service;
// ── Demo simulation ──────────────────────────────────────────────────────────
Timer? _demoTimer;
final _rng = math.Random();
AutopilotState() {
_startDemo();
}
// ---------------------------------------------------------------------------
// Serial connection
// ---------------------------------------------------------------------------
/// Connect to the AR-Concentrador via USB serial.
///
/// Stops the demo timer and switches to live data.
/// Falls back to demo automatically if the link drops.
///
/// Throws if the ports cannot be opened (caller should catch and show error).
Future<void> connectToSerial({
required String rxPort,
required String txPort,
int stationId = 2,
}) async {
_demoTimer?.cancel();
_demoTimer = null;
_service?.disconnect();
_service = ConcentradorService(
rxPort: rxPort,
txPort: txPort,
stationId: stationId,
);
_service!.onStatus = _onSerialStatus;
_service!.onConnectionChanged = _onConnectionChanged;
await _service!.connect(); // may throw — caller handles
}
/// Disconnect the serial link and return to demo mode.
Future<void> disconnectSerial() async {
await _service?.disconnect();
_service = null;
_startDemo();
}
void _onConnectionChanged(bool connected) {
isConnected = connected;
if (!connected) {
// Link dropped — fall back to animated demo so the UI stays alive.
_startDemo();
}
notifyListeners();
}
void _onSerialStatus(ParpStatus status) {
isConnected = true;
headingDeg = status.headingDeg;
setpointDeg = status.setpointDeg;
rudderDeg = status.rudderDeg;
mode = status.mode;
notifyListeners();
}
// ---------------------------------------------------------------------------
// Demo simulation
// ---------------------------------------------------------------------------
void _startDemo() {
_demoTimer?.cancel();
_demoTimer = Timer.periodic(const Duration(milliseconds: 500), (_) => _tick());
}
void _tick() {
switch (mode) {
case AutopilotMode.standby:
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:
case AutopilotMode.trackKeep:
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;
}
cogDeg = (headingDeg + rudderDeg * 0.15 + (_rng.nextDouble() - 0.5) * 0.3) % 360;
if (cogDeg < 0) cogDeg += 360;
notifyListeners();
}
double _angleDiff(double target, double current) {
double d = (target - current) % 360;
if (d > 180) d -= 360;
if (d < -180) d += 360;
return d;
}
// ---------------------------------------------------------------------------
// Commands — work in both demo and serial modes
// ---------------------------------------------------------------------------
void engage() {
setpointDeg = headingDeg;
mode = AutopilotMode.headingHold;
_service?.sendEngage(headingDeg);
notifyListeners();
}
void disengage() {
mode = AutopilotMode.standby;
_service?.sendDisengage();
notifyListeners();
}
void adjustSetpoint(double deltaDeg) {
if (mode != AutopilotMode.headingHold) return;
setpointDeg = (setpointDeg + deltaDeg) % 360;
if (setpointDeg < 0) setpointDeg += 360;
// Route to the appropriate serial command
if (_service != null && isConnected) {
if (deltaDeg == -10) _service!.sendPortTen(setpointDeg);
else if (deltaDeg == -1) _service!.sendPortOne(setpointDeg);
else if (deltaDeg == 1) _service!.sendStbdOne(setpointDeg);
else if (deltaDeg == 10) _service!.sendStbdTen(setpointDeg);
else _service!.sendSetHeading(setpointDeg);
}
notifyListeners();
}
void selectMode(AutopilotMode newMode) {
switch (newMode) {
case AutopilotMode.standby:
disengage();
case AutopilotMode.headingHold:
engage();
case AutopilotMode.trackKeep:
mode = AutopilotMode.trackKeep;
setpointDeg = headingDeg;
notifyListeners();
}
}
// ---------------------------------------------------------------------------
// Port discovery (delegate to service layer)
// ---------------------------------------------------------------------------
static List<String> availablePorts() => ConcentradorService.availablePorts();
@override
void dispose() {
_demoTimer?.cancel();
_service?.disconnect();
super.dispose();
}
}