abe9b764c7
- 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
206 lines
7.0 KiB
Dart
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();
|
|
}
|
|
}
|