// ============================================================================= // 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,,,,,*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,,,*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, }; }