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
145 lines
4.8 KiB
Dart
145 lines
4.8 KiB
Dart
// =============================================================================
|
|
// services/parp_codec.dart — $PARP sentence parser and builder
|
|
// =============================================================================
|
|
//
|
|
// Protocol reference: docs/concentrador_protocol.md
|
|
//
|
|
// Inbound ($PARP,STATUS — broadcast by concentrador at 2 Hz):
|
|
// $PARP,STATUS,<mode>,<setpoint>,<heading>,<rudder>,<commander>*XX\r\n
|
|
// Example: $PARP,STATUS,HEADING_HOLD,125.0,125.3,2.5,01*3A
|
|
//
|
|
// Outbound ($PARP commands — sent by display to concentrador):
|
|
// $PARP,<CMD>,<value>,<station_id>*XX\r\n
|
|
// Example: $PARP,ENGAGE,0.0,02*XX
|
|
// =============================================================================
|
|
|
|
import '../widgets/themed/mode_selector.dart';
|
|
|
|
/// Parsed content of a $PARP,STATUS sentence.
|
|
class ParpStatus {
|
|
const ParpStatus({
|
|
required this.mode,
|
|
required this.setpointDeg,
|
|
required this.headingDeg,
|
|
required this.rudderDeg,
|
|
required this.commander,
|
|
});
|
|
|
|
final AutopilotMode mode;
|
|
final double setpointDeg;
|
|
final double headingDeg;
|
|
final double rudderDeg;
|
|
final int commander; // station ID of the current commander (0 = none)
|
|
}
|
|
|
|
/// Stateless NMEA $PARP sentence codec.
|
|
abstract final class ParpCodec {
|
|
// ---------------------------------------------------------------------------
|
|
// Inbound parser
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// Parse a complete NMEA sentence string (with or without leading $).
|
|
///
|
|
/// Returns [ParpStatus] if the sentence is a valid $PARP,STATUS with correct
|
|
/// checksum. Returns null for any other sentence type or if CRC fails.
|
|
static ParpStatus? parseStatus(String sentence) {
|
|
// Strip whitespace / CRLF
|
|
final s = sentence.trim();
|
|
|
|
// Locate checksum delimiter
|
|
final starIdx = s.lastIndexOf('*');
|
|
if (starIdx < 0) return null;
|
|
|
|
final body = s.startsWith('\$') ? s.substring(1, starIdx) : s.substring(0, starIdx);
|
|
final crcHex = s.substring(starIdx + 1);
|
|
|
|
// Verify checksum
|
|
if (!_crcOk(body, crcHex)) return null;
|
|
|
|
// Tokenise
|
|
final parts = body.split(',');
|
|
if (parts.length < 7) return null;
|
|
if (parts[0] != 'PARP' || parts[1] != 'STATUS') return null;
|
|
|
|
final mode = _parseMode(parts[2]);
|
|
final setpoint = double.tryParse(parts[3]);
|
|
final heading = double.tryParse(parts[4]);
|
|
final rudder = double.tryParse(parts[5]);
|
|
final commander = int.tryParse(parts[6]);
|
|
|
|
if (setpoint == null || heading == null || rudder == null || commander == null) {
|
|
return null;
|
|
}
|
|
|
|
return ParpStatus(
|
|
mode: mode,
|
|
setpointDeg: setpoint,
|
|
headingDeg: heading,
|
|
rudderDeg: rudder,
|
|
commander: commander,
|
|
);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Outbound builders
|
|
// ---------------------------------------------------------------------------
|
|
|
|
static String engage(int stationId, double currentHeadingDeg) =>
|
|
_build('ENGAGE', currentHeadingDeg, stationId);
|
|
|
|
static String disengage(int stationId) =>
|
|
_build('DISENGAGE', 0.0, stationId);
|
|
|
|
static String setHeading(int stationId, double headingDeg) =>
|
|
_build('SETHEADING', headingDeg, stationId);
|
|
|
|
static String portOne(int stationId, double currentSetpoint) =>
|
|
_build('PORTONE', currentSetpoint, stationId);
|
|
|
|
static String stbdOne(int stationId, double currentSetpoint) =>
|
|
_build('STBDONE', currentSetpoint, stationId);
|
|
|
|
static String portTen(int stationId, double currentSetpoint) =>
|
|
_build('PORTTEN', currentSetpoint, stationId);
|
|
|
|
static String stbdTen(int stationId, double currentSetpoint) =>
|
|
_build('STBDTEN', currentSetpoint, stationId);
|
|
|
|
static String reqCmd(int stationId) =>
|
|
_build('REQCMD', 0.0, stationId);
|
|
|
|
static String relCmd(int stationId) =>
|
|
_build('RELCMD', 0.0, stationId);
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Private helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
static String _build(String cmd, double value, int stationId) {
|
|
final body = 'PARP,$cmd,${value.toStringAsFixed(1)},'
|
|
'${stationId.toString().padLeft(2, '0')}';
|
|
final crc = _computeCrc(body);
|
|
return '\$$body*${crc.toRadixString(16).toUpperCase().padLeft(2, '0')}\r\n';
|
|
}
|
|
|
|
static int _computeCrc(String body) {
|
|
int crc = 0;
|
|
for (final ch in body.codeUnits) {
|
|
crc ^= ch;
|
|
}
|
|
return crc;
|
|
}
|
|
|
|
static bool _crcOk(String body, String crcHex) {
|
|
final expected = _computeCrc(body);
|
|
final received = int.tryParse(crcHex, radix: 16);
|
|
return received != null && received == expected;
|
|
}
|
|
|
|
static AutopilotMode _parseMode(String raw) => switch (raw) {
|
|
'HEADING_HOLD' => AutopilotMode.headingHold,
|
|
'TRACK' => AutopilotMode.trackKeep,
|
|
_ => AutopilotMode.standby,
|
|
};
|
|
}
|