feat(device): TX/RX 日志附加完整 JSON 字符串
This commit is contained in:
225
lib/features/device/services/serial_port_service.dart
Normal file
225
lib/features/device/services/serial_port_service.dart
Normal file
@@ -0,0 +1,225 @@
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:usb_serial/usb_serial.dart';
|
||||
|
||||
import '../models/serial_config.dart';
|
||||
import 'device_log.dart';
|
||||
|
||||
/// 串口连接状态
|
||||
enum SerialConnectionState { disconnected, connecting, connected, error }
|
||||
|
||||
/// 串口服务
|
||||
///
|
||||
/// 封装 [UsbSerial] 库,对外暴露稳定的连接/读写接口,
|
||||
/// 屏蔽底层 USB 设备的插拔、权限请求、参数配置等细节。
|
||||
class SerialPortService {
|
||||
UsbPort? _port;
|
||||
UsbDevice? _device;
|
||||
StreamSubscription<Uint8List>? _readSub;
|
||||
final _connectionCtrl = StreamController<SerialConnectionState>.broadcast();
|
||||
final _dataCtrl = StreamController<Uint8List>.broadcast();
|
||||
final _errorCtrl = StreamController<String>.broadcast();
|
||||
SerialConnectionState _state = SerialConnectionState.disconnected;
|
||||
String? _lastError;
|
||||
|
||||
/// 当前连接状态
|
||||
SerialConnectionState get state => _state;
|
||||
|
||||
/// 最后一次错误信息
|
||||
String? get lastError => _lastError;
|
||||
|
||||
/// 是否已连接
|
||||
bool get isConnected => _state == SerialConnectionState.connected;
|
||||
|
||||
/// 当前连接设备(仅在已连接时可用)
|
||||
UsbDevice? get currentDevice => _device;
|
||||
|
||||
/// 连接状态变更流
|
||||
Stream<SerialConnectionState> get connectionStateChanges =>
|
||||
_connectionCtrl.stream;
|
||||
|
||||
/// 接收到的数据流
|
||||
Stream<Uint8List> get onData => _dataCtrl.stream;
|
||||
|
||||
/// 错误信息流
|
||||
Stream<String> get onError => _errorCtrl.stream;
|
||||
|
||||
/// 列出当前连接的 USB 串口设备
|
||||
Future<List<UsbDevice>> listDevices() async {
|
||||
return await UsbSerial.listDevices();
|
||||
}
|
||||
|
||||
/// 设备名(vendor/product 拼接)
|
||||
static String deviceLabel(UsbDevice d) {
|
||||
final v = _hex(d.vid);
|
||||
final p = _hex(d.pid);
|
||||
final name = d.productName ?? d.manufacturerName ?? 'USB Serial';
|
||||
return '$name [$v:$p]';
|
||||
}
|
||||
|
||||
static String _hex(int? v) =>
|
||||
(v ?? 0).toRadixString(16).padLeft(4, '0').toUpperCase();
|
||||
|
||||
/// 打开指定设备并按 [config] 配置参数
|
||||
Future<bool> connect(UsbDevice device, SerialConfig config) async {
|
||||
if (_state == SerialConnectionState.connected ||
|
||||
_state == SerialConnectionState.connecting) {
|
||||
await disconnect();
|
||||
}
|
||||
|
||||
DeviceLog.info('连接串口设备: ${deviceLabel(device)}, '
|
||||
'baud=${config.baudRate} dataBits=${config.dataBits} '
|
||||
'stopBits=${config.stopBits} parity=${config.parity.name}');
|
||||
_setState(SerialConnectionState.connecting);
|
||||
try {
|
||||
// UsbSerial.create 内部会触发 Android USB 权限弹窗
|
||||
final port = await device.create();
|
||||
if (port == null) {
|
||||
_fail('创建设备端口失败(可能未授予 USB 权限)');
|
||||
return false;
|
||||
}
|
||||
|
||||
final opened = await port.open();
|
||||
if (!opened) {
|
||||
await port.close();
|
||||
_fail('打开端口失败');
|
||||
return false;
|
||||
}
|
||||
|
||||
await _applyConfig(port, config);
|
||||
|
||||
_port = port;
|
||||
_device = device;
|
||||
_subscribeStream(port);
|
||||
_setState(SerialConnectionState.connected);
|
||||
DeviceLog.info('串口已连接: ${deviceLabel(device)}');
|
||||
return true;
|
||||
} catch (e) {
|
||||
_fail('连接异常: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 应用串口参数
|
||||
Future<void> _applyConfig(UsbPort port, SerialConfig config) async {
|
||||
await port.setPortParameters(
|
||||
config.baudRate,
|
||||
config.dataBits,
|
||||
config.stopBits,
|
||||
_parityToCode(config.parity),
|
||||
);
|
||||
await port.setFlowControl(_flowControlToCode(config.flowControl));
|
||||
}
|
||||
|
||||
/// 断开当前连接
|
||||
Future<void> disconnect() async {
|
||||
if (_state == SerialConnectionState.disconnected) return;
|
||||
await _readSub?.cancel();
|
||||
_readSub = null;
|
||||
try {
|
||||
await _port?.close();
|
||||
} catch (_) {}
|
||||
_port = null;
|
||||
_device = null;
|
||||
_setState(SerialConnectionState.disconnected);
|
||||
DeviceLog.info('串口已断开');
|
||||
}
|
||||
|
||||
/// 写入数据,返回成功写入的字节数(设备库本身不返回字节数,连接失败时返回 0)
|
||||
Future<int> write(Uint8List data) async {
|
||||
final port = _port;
|
||||
if (port == null || !isConnected) {
|
||||
_emitError('串口未连接,写入失败');
|
||||
return 0;
|
||||
}
|
||||
try {
|
||||
await port.write(data);
|
||||
DeviceLog.info('串口 TX: ${data.length} 字节');
|
||||
return data.length;
|
||||
} catch (e) {
|
||||
_emitError('写入异常: $e', error: e);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// 释放资源(应用退出时调用)
|
||||
Future<void> dispose() async {
|
||||
await disconnect();
|
||||
await _connectionCtrl.close();
|
||||
await _dataCtrl.close();
|
||||
await _errorCtrl.close();
|
||||
}
|
||||
|
||||
void _subscribeStream(UsbPort port) {
|
||||
_readSub?.cancel();
|
||||
final stream = port.inputStream;
|
||||
if (stream == null) {
|
||||
_emitError('当前端口无输入流');
|
||||
return;
|
||||
}
|
||||
_readSub = stream.listen(
|
||||
(data) {
|
||||
if (data.isNotEmpty) {
|
||||
DeviceLog.info('串口 RX: ${data.length} 字节');
|
||||
_dataCtrl.add(data);
|
||||
}
|
||||
},
|
||||
onError: (Object e) {
|
||||
_emitError('读取异常: $e', error: e);
|
||||
},
|
||||
onDone: () {
|
||||
DeviceLog.info('串口输入流关闭');
|
||||
_setState(SerialConnectionState.disconnected);
|
||||
},
|
||||
cancelOnError: false,
|
||||
);
|
||||
}
|
||||
|
||||
void _setState(SerialConnectionState s) {
|
||||
_state = s;
|
||||
if (s != SerialConnectionState.error) {
|
||||
_lastError = null;
|
||||
}
|
||||
if (!_connectionCtrl.isClosed) {
|
||||
_connectionCtrl.add(s);
|
||||
}
|
||||
}
|
||||
|
||||
void _fail(String message) {
|
||||
_lastError = message;
|
||||
_setState(SerialConnectionState.error);
|
||||
_emitError(message);
|
||||
}
|
||||
|
||||
void _emitError(String message, {Object? error}) {
|
||||
_lastError = message;
|
||||
if (error != null) {
|
||||
DeviceLog.severe(message, error: error);
|
||||
} else {
|
||||
DeviceLog.warn(message);
|
||||
}
|
||||
if (!_errorCtrl.isClosed) {
|
||||
_errorCtrl.add(message);
|
||||
}
|
||||
}
|
||||
|
||||
static int _parityToCode(SerialParity p) {
|
||||
return switch (p) {
|
||||
SerialParity.none => UsbPort.PARITY_NONE,
|
||||
SerialParity.odd => UsbPort.PARITY_ODD,
|
||||
SerialParity.even => UsbPort.PARITY_EVEN,
|
||||
SerialParity.mark => UsbPort.PARITY_MARK,
|
||||
SerialParity.space => UsbPort.PARITY_SPACE,
|
||||
};
|
||||
}
|
||||
|
||||
static int _flowControlToCode(SerialFlowControl f) {
|
||||
return switch (f) {
|
||||
SerialFlowControl.none => UsbPort.FLOW_CONTROL_OFF,
|
||||
SerialFlowControl.rtsCts => UsbPort.FLOW_CONTROL_RTS_CTS,
|
||||
SerialFlowControl.xonXoff => UsbPort.FLOW_CONTROL_XON_XOFF,
|
||||
SerialFlowControl.dtrDsr => UsbPort.FLOW_CONTROL_DSR_DTR,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user