Files
AR-Autopilot/display/lib/services/concentrador_service.dart
T
alro65 abe9b764c7 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
2026-05-24 01:28:04 -04:00

198 lines
5.6 KiB
Dart

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