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
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
// =============================================================================
|
||||
// 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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user