- 在 AndroidManifest.xml 中添加 USB Host 权限声明 - 集成 CH934X Android SDK 并通过反射调用原生功能 - 实现设备查找、串口读写、GPIO 和 Modem 控制功能 - 添加异常回调机制处理设备拔出等情况 - 提供 Stream 数据流支持实时串口数据监听 - 完善单元测试覆盖所有核心功能模块
11 KiB
ch934x_serial 插件使用说明
ch934x_serial 是基于南京沁恒微电子 CH934X 系列 USB 转多串口芯片
Android SDK 封装的 Flutter 插件,文档面向其他 Flutter 开发者,介绍
如何把它集成到自己的应用中并使用全部对外 API。
全部接口语义与官方 Android 文档
docs/CH934X_Android_开发说明.md保持一致,本说明不再重复其原始描述,而是说明在 Flutter 中如何调用。
1. 插件概述
- 支持的平台: Android(已实现,iOS / Web / Desktop 暂不支持)。
- 主要能力:
- CH934X 设备枚举、序列号读取、芯片类型识别
- 多串口打开 / 关闭
- 字节级串口读写
- GPIO 输出 / 输入
- Modem 控制 (DTR/RTS) 与状态 (CTS/DSR/RI/DCD) 读取
- 设备拔出等异常事件回调
- 底层依赖: 沁恒官方
CH934XLib.jar(插件随包发布,位于android/libs/CH934XLib.jar),通过反射方式桥接,无需在调用方 业务代码中额外处理。
2. 集成步骤
2.1 在 pubspec.yaml 中加入依赖
dependencies:
flutter:
sdk: flutter
ch934x_serial: ^1.0.0
执行 flutter pub get 完成依赖拉取。
2.2 Android 工程准备
-
确认
android/app/build.gradle中minSdk >= 24,CH934X SDK 不支持 更低版本。 -
在
android/app/src/main/AndroidManifest.xml中声明 USB Host 能力:<uses-feature android:name="android.hardware.usb.host" android:required="true" /> -
运行时申请 USB 权限。本插件不主动申请权限,需业务方调用 Android
UsbManager申请并接收广播,例如在MainActivity.onCreate中:private val actionDevicePermission = "com.example.USB_PERMISSION" private val usbPermissionReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { if (intent.action != actionDevicePermission) return val device: UsbDevice? = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE) val granted = intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false) if (granted && device != null) { // 此处可继续打开串口 } } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val usbManager = getSystemService(Context.USB_SERVICE) as UsbManager val pendingIntent = PendingIntent.getBroadcast( this, 0, Intent(actionDevicePermission), 0 ) registerReceiver(usbPermissionReceiver, IntentFilter(actionDevicePermission)) usbManager.deviceList.values.forEach { device -> usbManager.requestPermission(device, pendingIntent) } }也可以监听
UsbManager.ACTION_USB_DEVICE_ATTACHED让系统在插入设备 时主动弹出授权框。
2.3 第一次调用
import 'package:ch934x_serial/ch934x_serial.dart';
Future<void> scan() async {
final plugin = Ch934xSerial();
final devices = await plugin.getDeviceList();
for (final d in devices) {
debugPrint('发现设备: VID=0x${d.vendorId.toRadixString(16)} '
'SN=${d.serialNumber}');
}
}
如果调用后列表为空,请先确认已经完成第 2.2 步的 USB 权限申请。
3. API 接口说明
所有 API 都挂在 Ch934xSerial 单例上,命名风格与原 SDK 文档保持一致,
参数使用 int 表示芯片/引脚编号,数据以 Uint8List 形式传递。
3.1 设备查找
| Dart 方法 | 原 SDK 接口 | 说明 |
|---|---|---|
getDeviceList() |
UsbHelper.getCH934XDeviceList |
获取全部 CH934X 设备 |
getSerialNumber(deviceId) |
UsbHelper.CH934XSerialNum |
获取指定设备的序列号 |
getDeviceType(deviceId) |
UsbHelper.CH934XDeviceType |
获取设备类型常量 |
getSerialPortList(deviceId, interfaceNumber: n) |
UsbHelper.getCH934XSerialPortList |
获取设备串口列表 |
返回类型:
getDeviceList()→List<Ch934xDeviceInfo>getSerialPortList(...)→List<Ch934xSerialPortInfo>Ch934xDeviceInfo包含deviceId / vendorId / productId / deviceType / serialNumber / productName / manufacturerName / interfaceCount / serialPorts,可通过isCh934x判定是否被识别为 CH934X 设备。deviceType取值为Ch934xDeviceType中的常量(ch9344、ch9344L、ch9350、ch9348Q、ch9342、ch934xOther,未识别为unknown = -1)。
3.2 设备打开与关闭
final target = Ch934xPortTarget(
deviceId: device.deviceId,
interfaceNumber: 0, // 多数设备只有 1 个接口
serialPortIndex: port.portIndex,
);
final ok = await plugin.openPort(target);
if (!ok) {
debugPrint('打开失败');
return;
}
// ... 进行业务通信
await plugin.closePort();
Ch934xPortTarget封装了deviceId/interfaceNumber/serialPortIndex三个参数,可通过toMap()调试其字段。- 同一会话仅保留最近一次打开的串口对象,与原 SDK 行为一致;打开新
串口前请先
closePort()或在 finally 块中清理。
3.3 串口读写
// 写入
final bytes = Uint8List.fromList([0x01, 0x02, 0x03]);
final written = await plugin.write(bytes);
debugPrint('写入字节数: $written');
// 读取(单次)
final recv = await plugin.read(1024);
if (recv.isEmpty) debugPrint('暂无数据');
// 持续接收:使用 dataStream
final sub = plugin.dataStream(chunkSize: 1024).listen((chunk) {
debugPrint('收到: $chunk');
});
// 取消订阅
await sub.cancel();
read(length)返回Uint8List,无数据或失败时为空。write(data)返回实际写入字节数,失败时为 0。dataStream默认每 20ms 轮询一次,可调整interval与chunkSize。 业务方在 widget dispose 时记得cancel订阅并closePort。
3.4 GPIO 接口
final ok = await plugin.setGpioOutput(gpioNumber: 0, level: 1);
final value = await plugin.getGpioInput(0); // 负值表示失败
gpioNumber与level与原文档保持一致(0/1)。getGpioInput失败时返回 -1,业务方需要自行处理。
3.5 Modem 控制接口
await plugin.setModemControl(dtr: 1, rts: 0);
final status = await plugin.getModemStatus();
if (ModemStatus.isSet(status, ModemStatus.cts)) {
debugPrint('CTS 高电平');
}
ModemStatus暴露位掩码常量cts / dsr / ri / dcd与工具方法isSet(status, mask),取值与文档表格一致。getModemStatus()返回 0 时表示无有效状态,业务方注意判空。
3.6 异常回调
final sub = await plugin.setExceptionCallback((event) {
debugPrint('设备异常: ${event.type} ${event.message}');
// 业务方应主动关闭串口、刷新设备列表或提示用户重新插拔
});
// 主动取消监听
await sub.cancel();
- 异常类型见
Ch934xExceptionType(deviceDetached / ioError / sdk / unknown)。 - 返回的
StreamSubscription需要在合适时机 cancel,以便释放监听 与平台资源。
4. 完整示例
下面给出一个最小可运行示例,演示"扫描 → 打开 → 收发 → 关闭"的完整
流程,完整可交互的 demo 见 example/lib/main.dart。
import 'dart:async';
import 'dart:typed_data';
import 'package:ch934x_serial/ch934x_serial.dart';
class SerialBridge {
SerialBridge() : _plugin = Ch934xSerial();
final Ch934xSerial _plugin;
StreamSubscription<Ch934xException>? _exceptionSub;
StreamSubscription<Uint8List>? _dataSub;
Future<void> connect(Ch934xDeviceInfo device, Ch934xSerialPortInfo port) async {
final opened = await _plugin.openPort(
Ch934xPortTarget(
deviceId: device.deviceId,
interfaceNumber: 0,
serialPortIndex: port.portIndex,
),
);
if (!opened) throw StateError('串口打开失败');
_exceptionSub = await _plugin.setExceptionCallback((e) {
// 设备拔出时通常会触发 deviceDetached。
print('异常: $e');
});
_dataSub = _plugin.dataStream().listen((chunk) {
print('接收: $chunk');
});
}
Future<void> sendString(String s) async {
final bytes = Uint8List.fromList(s.codeUnits);
await _plugin.write(bytes);
}
Future<void> dispose() async {
await _dataSub?.cancel();
await _exceptionSub?.cancel();
await _plugin.closePort();
}
}
5. 常见问题(FAQ)
Q1. getDeviceList() 返回空数组。
- 确认已声明
<uses-feature android:name="android.hardware.usb.host" />。 - 确认 Android
UsbManager已对目标设备授权(系统会弹出对话框,需要 用户点击"允许")。 - 确认 OTG 数据线连接稳固,并尝试调用
setExceptionCallback监听deviceDetached事件,排查设备是否被系统频繁弹出。
Q2. openPort() 返回 false。
- 多数情况是 USB 权限未授予;请在
UsbManager.requestPermission返回 true 后再调用openPort。 - 如果目标设备具有多个接口,请尝试修改
interfaceNumber。 - 确认设备中至少有一个
Ch934xSerialPortInfo(getSerialPortList返回),否则原 SDK 也无法打开。
Q3. read() 一直返回空数组。
- 确认对端设备正在发送数据,且波特率/校验位等参数与原 SDK 默认值
一致(
CH934XLib提供独立的setConfig接口,本插件当前未做 封装,需要时可扩展原 SDK 反射调用)。 - 检查线序:RX/TX 是否接反,以及硬件流控是否正确。
Q4. setExceptionCallback 没有触发。
- 本插件仅在原生层主动推送时才会触发回调,目前沁恒 SDK 在设备热拔
插场景下会自动调用,其他异常(超时、CRC 错误等)可能不会触发。
如需丰富事件类型,可在原生层
Ch934xSerialPlugin.java中扩展MethodChannel.invokeMethod("onException", payload)上报。
Q5. 是否支持 iOS / 桌面 / Web?
- 当前仅在 Android 端验证通过;
CH934XLib.jar由沁恒官方提供 Android 端 SDK,其他平台需要厂商另行提供或自行实现。
Q6. 如何做单元测试?
-
注入自定义的
Ch934xSerialPlatform即可:class FakePlatform extends Ch934xSerialPlatform with MockPlatformInterfaceMixin { @override Future<List<Ch934xDeviceInfo>> getDeviceList() async => const []; // ... 其它方法按需返回 } Ch934xSerialPlatform.instance = FakePlatform(); final plugin = Ch934xSerial();插件仓库的
test/ch934x_serial_test.dart给出了完整 mock 示例。
6. 接口总览
下表汇总了插件中暴露的全部 API,具体调用示例见上文第 3 节。
| Dart 方法 | 文档编号 | 分类 |
|---|---|---|
getDeviceList |
4.1.4 | 设备查找 |
getSerialNumber |
4.1.1 | 设备查找 |
getDeviceType |
4.1.2 | 设备查找 |
getSerialPortList |
4.1.3 | 设备查找 |
openPort |
5.1.1 | 设备打开 |
closePort |
5.2.1 | 设备关闭 |
read |
6.1.1 | 串口读写 |
write |
6.2.1 | 串口读写 |
setGpioOutput |
7.1.1 | GPIO |
getGpioInput |
7.1.2 | GPIO |
setModemControl |
7.2.1 | Modem |
getModemStatus |
7.2.2 | Modem |
setExceptionCallback |
7.3.1 | 异常 |
dataStream |
6.1 增强 | 串口流(插件新增) |
7. 反馈与贡献
遇到问题请提供:
- 复现步骤(设备型号、Android 版本、是否开启 USB 调试)
- 完整日志(建议使用
adb logcat过滤ch934x_serial标签) - 期望结果 vs 实际结果
提交 Issue 时附上以上信息可以大幅加快排查速度。