// ============================================================================= // 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? _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 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 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 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(); } }