mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-04-20 22:13:48 +00:00
247 lines
7.1 KiB
Dart
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 }
|