meshcore-open/lib/services/usb_serial_service_native.dart
2026-03-05 02:25:05 -05:00

247 lines
7.1 KiB
Dart

import 'dart:async';
import 'package:flserial/flserial.dart';
import 'package:flserial/flserial_exception.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import '../utils/usb_port_labels.dart';
import 'usb_serial_frame_codec.dart';
/// Wraps the native flserial plugin to expose a stream of raw bytes for the
/// MeshCore connector to consume.
class UsbSerialService {
UsbSerialService();
static const MethodChannel _androidMethodChannel = MethodChannel(
'meshcore_open/android_usb_serial',
);
static const EventChannel _androidEventChannel = EventChannel(
'meshcore_open/android_usb_serial_events',
);
final StreamController<Uint8List> _frameController =
StreamController<Uint8List>.broadcast();
final FlSerial _serial = FlSerial();
final UsbSerialFrameDecoder _frameDecoder = UsbSerialFrameDecoder();
StreamSubscription<dynamic>? _androidDataSubscription;
StreamSubscription<FlSerialEventArgs>? _dataSubscription;
UsbSerialStatus _status = UsbSerialStatus.disconnected;
String? _connectedPortName;
UsbSerialStatus get status => _status;
String? get activePortName => _connectedPortName;
Stream<Uint8List> get frameStream => _frameController.stream;
bool get _useAndroidUsbHost =>
!kIsWeb && defaultTargetPlatform == TargetPlatform.android;
bool get isConnected {
if (_useAndroidUsbHost) {
return _status == UsbSerialStatus.connected;
}
return _status == UsbSerialStatus.connected &&
_serial.isOpen() == FlOpenStatus.open;
}
Future<List<String>> listPorts() async {
if (_useAndroidUsbHost) {
final ports = await _androidMethodChannel.invokeListMethod<String>(
'listPorts',
);
return ports ?? <String>[];
}
return Future.value(FlSerial.listPorts());
}
Future<void> connect({
required String portName,
int baudRate = 115200,
}) async {
if (_status == UsbSerialStatus.connected ||
_status == UsbSerialStatus.connecting) {
throw StateError('USB serial transport is already active');
}
_status = UsbSerialStatus.connecting;
final normalizedPortName = normalizeUsbPortName(portName);
if (_useAndroidUsbHost) {
try {
await _androidMethodChannel.invokeMethod<void>('connect', {
'portName': normalizedPortName,
'baudRate': baudRate,
});
debugPrint(
'USB serial opened port=$normalizedPortName on Android via USB host bridge',
);
} on PlatformException catch (error) {
_status = UsbSerialStatus.disconnected;
throw StateError(error.message ?? error.code);
}
} else {
_serial.init();
try {
final status = _serial.openPort(normalizedPortName, baudRate);
if (status != FlOpenStatus.open) {
throw StateError(
'Failed to open USB port $normalizedPortName ($status)',
);
}
_serial.setByteSize8();
_serial.setBitParityNone();
_serial.setStopBits1();
_serial.setFlowControlNone();
_serial.setRTS(false);
_serial.setDTR(true);
debugPrint(
'USB serial opened port=$normalizedPortName cts=${_serial.getCTS()} dsr=${_serial.getDSR()} dtr=true rts=false',
);
} on FlSerialException catch (error) {
_serial.free();
_status = UsbSerialStatus.disconnected;
throw StateError(
'Failed to open USB port $normalizedPortName: ${error.msg} (${error.error})',
);
} catch (error) {
_serial.free();
_status = UsbSerialStatus.disconnected;
rethrow;
}
}
_connectedPortName = normalizedPortName;
if (_useAndroidUsbHost) {
_androidDataSubscription = _androidEventChannel
.receiveBroadcastStream()
.listen(
_handleAndroidData,
onError: _handleSerialError,
onDone: _handleSerialDone,
);
} else {
_dataSubscription = _serial.onSerialData.stream.listen(
_handleSerialData,
onError: _handleSerialError,
onDone: _handleSerialDone,
);
}
_status = UsbSerialStatus.connected;
}
Future<void> write(Uint8List data) async {
if (!isConnected) {
throw StateError('USB serial port is not open');
}
final packet = wrapUsbSerialTxFrame(data);
_logFrameSummary('USB TX frame', data);
if (_useAndroidUsbHost) {
try {
await _androidMethodChannel.invokeMethod<void>('write', {
'data': packet,
});
} on PlatformException catch (error) {
throw StateError(error.message ?? error.code);
}
} else {
_serial.write(packet);
}
}
Future<void> disconnect() async {
if (_status == UsbSerialStatus.disconnected) return;
_status = UsbSerialStatus.disconnecting;
_connectedPortName = null;
await _androidDataSubscription?.cancel();
_androidDataSubscription = null;
await _dataSubscription?.cancel();
_dataSubscription = null;
if (_useAndroidUsbHost) {
try {
await _androidMethodChannel.invokeMethod<void>('disconnect');
} catch (_) {
// Ignore errors while closing.
}
} else {
try {
if (_serial.isOpen() == FlOpenStatus.open) {
_serial.closePort();
}
} catch (_) {
// Ignore errors while closing.
}
_serial.free();
}
_status = UsbSerialStatus.disconnected;
}
void updateConnectedLabel(String label) {
final trimmed = label.trim();
if (trimmed.isEmpty) {
return;
}
_connectedPortName = trimmed;
}
void dispose() {
unawaited(disconnect());
unawaited(_frameController.close());
}
void _handleSerialData(FlSerialEventArgs event) {
try {
final bytes = event.serial.readList();
if (bytes.isNotEmpty) {
_ingestRawBytes(Uint8List.fromList(bytes));
}
} catch (error, stack) {
_frameController.addError(error, stack);
}
}
void _handleAndroidData(dynamic data) {
if (data is Uint8List) {
_ingestRawBytes(data);
return;
}
if (data is ByteData) {
_ingestRawBytes(data.buffer.asUint8List());
return;
}
_frameController.addError(
StateError('Unexpected Android USB event payload: ${data.runtimeType}'),
);
}
void _handleSerialError(Object error, [StackTrace? stackTrace]) {
_frameController.addError(error, stackTrace);
}
void _handleSerialDone() {
unawaited(disconnect());
}
void _ingestRawBytes(Uint8List bytes) {
for (final packet in _frameDecoder.ingest(bytes)) {
if (!packet.isRxFrame) {
debugPrint(
'USB ignored packet start=0x${packet.frameStart.toRadixString(16).padLeft(2, '0')} len=${packet.payload.length}',
);
continue;
}
_frameController.add(packet.payload);
}
}
void _logFrameSummary(String prefix, Uint8List bytes) {
if (bytes.isEmpty) {
debugPrint('$prefix len=0');
return;
}
debugPrint('$prefix code=${bytes[0]} len=${bytes.length}');
}
}
enum UsbSerialStatus { disconnected, connecting, connected, disconnecting }