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,197 @@
|
||||
// =============================================================================
|
||||
// services/concentrador_service.dart — USB serial link to AR-Concentrador
|
||||
// =============================================================================
|
||||
//
|
||||
// The AR-Concentrador exposes two separate CH340N virtual COM ports:
|
||||
// rxPort — USB-OUT: concentrador broadcasts $PARP,STATUS + NMEA at 2 Hz
|
||||
// txPort — USB-IN : display sends $PARP commands to the concentrador
|
||||
//
|
||||
// Both ports run at 115 200 baud, 8N1, no flow control.
|
||||
//
|
||||
// Usage:
|
||||
// final svc = ConcentradorService(rxPort: 'COM3', txPort: 'COM4', stationId: 2);
|
||||
// svc.onStatus = (status) { ... };
|
||||
// svc.onConnectionChanged = (connected) { ... };
|
||||
// await svc.connect();
|
||||
// svc.sendEngage(headingDeg: 125.0);
|
||||
// svc.disconnect();
|
||||
// =============================================================================
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter_libserialport/flutter_libserialport.dart';
|
||||
|
||||
import 'parp_codec.dart';
|
||||
|
||||
/// Callback type for status updates from the concentrador.
|
||||
typedef StatusCallback = void Function(ParpStatus status);
|
||||
|
||||
/// Callback type for connection state changes.
|
||||
typedef ConnectionCallback = void Function(bool connected);
|
||||
|
||||
class ConcentradorService {
|
||||
ConcentradorService({
|
||||
required this.rxPort,
|
||||
required this.txPort,
|
||||
this.stationId = 2,
|
||||
this.baudRate = 115200,
|
||||
});
|
||||
|
||||
final String rxPort;
|
||||
final String txPort;
|
||||
final int stationId;
|
||||
final int baudRate;
|
||||
|
||||
/// Called whenever a valid $PARP,STATUS sentence is received.
|
||||
StatusCallback? onStatus;
|
||||
|
||||
/// Called when the connection state changes.
|
||||
ConnectionCallback? onConnectionChanged;
|
||||
|
||||
SerialPort? _rx;
|
||||
SerialPort? _tx;
|
||||
SerialPortReader? _reader;
|
||||
StreamSubscription<Uint8List>? _rxSub;
|
||||
|
||||
bool _connected = false;
|
||||
bool get isConnected => _connected;
|
||||
|
||||
// Accumulation buffer for partial sentences
|
||||
final StringBuffer _buf = StringBuffer();
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Connection lifecycle
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Open both serial ports and start listening for STATUS sentences.
|
||||
///
|
||||
/// Throws [SerialPortError] if either port cannot be opened.
|
||||
Future<void> connect() async {
|
||||
await disconnect(); // clean slate
|
||||
|
||||
_rx = SerialPort(rxPort);
|
||||
_tx = SerialPort(txPort);
|
||||
|
||||
_configPort(_rx!);
|
||||
_configPort(_tx!);
|
||||
|
||||
if (!_rx!.openRead()) {
|
||||
throw SerialPortError('Cannot open RX port $rxPort');
|
||||
}
|
||||
if (!_tx!.openWrite()) {
|
||||
throw SerialPortError('Cannot open TX port $txPort');
|
||||
}
|
||||
|
||||
_reader = SerialPortReader(_rx!);
|
||||
_rxSub = _reader!.stream.listen(
|
||||
_onData,
|
||||
onError: (_) => _handleDisconnect(),
|
||||
onDone: _handleDisconnect,
|
||||
);
|
||||
|
||||
_connected = true;
|
||||
onConnectionChanged?.call(true);
|
||||
}
|
||||
|
||||
/// Close both ports gracefully.
|
||||
Future<void> disconnect() async {
|
||||
_rxSub?.cancel();
|
||||
_rxSub = null;
|
||||
_reader = null;
|
||||
|
||||
_rx?.close();
|
||||
_tx?.close();
|
||||
_rx = null;
|
||||
_tx = null;
|
||||
|
||||
if (_connected) {
|
||||
_connected = false;
|
||||
onConnectionChanged?.call(false);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Command senders
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void sendEngage(double headingDeg) =>
|
||||
_send(ParpCodec.engage(stationId, headingDeg));
|
||||
|
||||
void sendDisengage() =>
|
||||
_send(ParpCodec.disengage(stationId));
|
||||
|
||||
void sendSetHeading(double headingDeg) =>
|
||||
_send(ParpCodec.setHeading(stationId, headingDeg));
|
||||
|
||||
void sendPortOne(double setpointDeg) =>
|
||||
_send(ParpCodec.portOne(stationId, setpointDeg));
|
||||
|
||||
void sendStbdOne(double setpointDeg) =>
|
||||
_send(ParpCodec.stbdOne(stationId, setpointDeg));
|
||||
|
||||
void sendPortTen(double setpointDeg) =>
|
||||
_send(ParpCodec.portTen(stationId, setpointDeg));
|
||||
|
||||
void sendStbdTen(double setpointDeg) =>
|
||||
_send(ParpCodec.stbdTen(stationId, setpointDeg));
|
||||
|
||||
void sendReqCmd() =>
|
||||
_send(ParpCodec.reqCmd(stationId));
|
||||
|
||||
void sendRelCmd() =>
|
||||
_send(ParpCodec.relCmd(stationId));
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Port listing (for settings screen)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// All serial ports currently visible to the OS.
|
||||
static List<String> availablePorts() => SerialPort.availablePorts;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Private
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void _configPort(SerialPort port) {
|
||||
final cfg = SerialPortConfig()
|
||||
..baudRate = baudRate
|
||||
..bits = 8
|
||||
..stopBits = 1
|
||||
..parity = SerialPortParity.none
|
||||
..setFlowControl(SerialPortFlowControl.none);
|
||||
port.config = cfg;
|
||||
}
|
||||
|
||||
void _onData(Uint8List data) {
|
||||
_buf.write(String.fromCharCodes(data));
|
||||
final raw = _buf.toString();
|
||||
final lines = raw.split('\n');
|
||||
|
||||
// Keep the last (possibly incomplete) chunk in the buffer.
|
||||
_buf.clear();
|
||||
_buf.write(lines.last);
|
||||
|
||||
for (int i = 0; i < lines.length - 1; i++) {
|
||||
final line = lines[i].trim();
|
||||
if (line.isEmpty) continue;
|
||||
final status = ParpCodec.parseStatus(line);
|
||||
if (status != null) {
|
||||
onStatus?.call(status);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _send(String sentence) {
|
||||
if (_tx == null || !_connected) return;
|
||||
try {
|
||||
_tx!.write(Uint8List.fromList(sentence.codeUnits));
|
||||
} catch (_) {
|
||||
_handleDisconnect();
|
||||
}
|
||||
}
|
||||
|
||||
void _handleDisconnect() {
|
||||
disconnect();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user