abe9b764c7
- 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
198 lines
5.6 KiB
Dart
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();
|
|
}
|
|
}
|