- 添加同设备内串口切换功能说明 - 新增主动申请 USB 权限功能描述 - 简化 Android 权限配置流程,移除手动声明 USB Host 能力要求 - 修正 getSerialPortList 方法使用说明,强调需先 openPort - 更新底层 SDK 接口映射关系 - 完善异常回调类型定义和说明 - 增加平台接口与单元测试相关内容 - 更新常见问题解答和接口总览
15 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) 读取
- 主动申请 USB 权限
- 设备拔出、Modem 错误等异常事件回调
- 底层依赖: 沁恒官方
CH934XLib.jar(插件随包发布,位于android/libs/CH934XLib.jar),通过原生CH934XManager单例直接调用, 无需在调用方业务代码中额外处理反射。 - 平台通道名称:
ch934x_serial - Dart 包名:
ch934x_serial(主类Ch934xSerial) - Android 包名:
com.xiarui.ch934x_serial
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 不支持 更低版本。 - 无需在
AndroidManifest.xml中声明 USB Host 能力——插件自带android/src/main/AndroidManifest.xml已声明<uses-feature android:name="android.hardware.usb.host" required="true" />, 应用侧合并后即可生效。 - USB 权限处理可省略——
openPort在底层若检测到未授权,会自动 调用UsbManager.requestPermission弹出系统对话框并等待用户回应; 也可以在业务侧通过requestUsbPermission(deviceId)提前主动申请。 插件内部BroadcastReceiver会在需要时动态注册/反注册,无需在AndroidManifest.xml中额外声明。
如果你仍想自己接管权限流程(例如在主界面"刷新设备列表"之后 立刻弹一次授权请求),直接调用
Ch934xSerial.requestUsbPermission即可,内部会等待用户的回应并以bool返回结果。
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}');
}
}
注意:
getSerialPortList在设备尚未openPort时会返回空 列表,因为 SDK 的getSerialCount必须先openDevice才能返回 真实值。完整流程是:扫描 → 选设备 →openPort→getSerialPortList拿真实串口数,详见 §3.2。
3. API 接口说明
所有 API 都挂在 Ch934xSerial 单例上,命名风格与原 SDK 文档保持一致,
参数使用 int 表示芯片/引脚编号,数据以 Uint8List 形式传递。
3.1 设备查找
| Dart 方法 | 原 SDK 接口 | 说明 |
|---|---|---|
getDeviceList() |
CH934XManager.enumDevice + getChipType |
获取全部 CH934X 设备 |
getSerialNumber(deviceId) |
UsbDevice.getSerialNumber |
获取指定设备的序列号 |
getDeviceType(deviceId) |
CH934XManager.getChipType |
获取设备类型常量 |
getSerialPortList(deviceId, interfaceNumber: n) |
CH934XManager.getSerialCount |
获取设备串口列表(需先 openPort) |
返回类型:
getDeviceList()→List<Ch934xDeviceInfo>getSerialPortList(...)→List<Ch934xSerialPortInfo>Ch934xDeviceInfo包含deviceId / vendorId / productId / deviceType / serialNumber / productName / manufacturerName / interfaceCount / serialPorts,可通过isCh934x判定是否被识别为 CH934X 设备。isCh934x: 当deviceType落在ch9344..ch934xOther范围内 时返回true(unknown = -1不算)。
Ch934xSerialPortInfo包含portIndex / devicePath / driverName。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()调试其字段,也可 通过Ch934xPortTarget.fromMap从 MethodChannel 回包中还原。 -
openPort内部会自动处理 USB 权限申请:若 SDK 抛NoPermissionException,会主动调起系统对话框并等待用户回应 (最多 60 秒),授权成功后会自动重试openDevice。 -
同设备内串口切换(
setActivePort):CH934X 设备一次openPort之后所有串口都已连接,后续读写只需切换serialPortIndex, 无需重新open。setActivePort(idx)仅切换当前活跃串口, 不重复打开设备:// 串口 #0 已打开,切换到 #2 await plugin.setActivePort(2); -
主动申请 USB 权限(
requestUsbPermission):final granted = await plugin.requestUsbPermission(device.deviceId); if (granted) { // 用户已授权,可继续 openPort } -
同一会话仅保留最近一次打开的串口对象,与原 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,无数据或失败时为空。length <= 0时不会调用原生层,直接返回空数组。- SDK 的
readData会返回内部缓冲的所有数据,插件按min(data.length, length)截断后回传。
write(data)返回实际写入字节数,失败时为 0。dataStream默认chunkSize = 1024、interval = 20ms,内部 持续以 [interval] 为周期反复调用read;当底层无数据时返回 空缓冲区,消费者可据此判定。chunkSize <= 0会抛ArgumentError。- 业务方在 widget dispose 时记得
cancel订阅并closePort。 read/write都针对当前活跃串口;切换串口用setActivePort。
3.4 GPIO 接口
final ok = await plugin.setGpioOutput(gpioNumber: 0, level: 1);
final value = await plugin.getGpioInput(0); // 负值表示失败
gpioNumber与level与原文档保持一致(0/1)。getGpioInput失败时返回 -1,业务方需要自行处理。- GPIO 操作同样作用于当前活跃串口。
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(0x01) / dsr(0x02) / ri(0x04) / dcd(0x08)与工具方法isSet(status, mask),取值与文档表格一致。getModemStatus()返回最近一次由 SDK 回调推送的位掩码; 若 SDK 尚未推送过任何状态,返回 0。业务方注意判空。
3.6 异常回调
final sub = await plugin.setExceptionCallback((event) {
debugPrint('设备异常: ${event.type} ${event.message}');
// 业务方应主动关闭串口、刷新设备列表或提示用户重新插拔
});
// 主动取消监听
await sub.cancel();
- 异常类型见
Ch934xExceptionType:deviceDetached = 1:设备被拔出(由 SDK 的usbDeviceDetach触发)。ioError = 2:Modem overrun / parity / frame 等错误。sdk = 3:原生 SDK 主动抛出的其他异常(目前未触发,保留语义)。unknown = 0:未识别(当前未触发,保留语义)。
Ch934xException包含type / message / cause,toString()会把type翻译为对应的常量名,方便日志/UI 显示。- 返回的
StreamSubscription必须在合适时机cancel,以便 释放底层StreamController与MethodCallHandler。
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);
}
/// 切换到同一设备的另一个串口,无需重新 open。
Future<void> switchToPort(int index) async {
await _plugin.setActivePort(index);
}
Future<void> dispose() async {
await _dataSub?.cancel();
await _exceptionSub?.cancel();
await _plugin.closePort();
}
}
5. 平台接口与单元测试
插件使用 plugin_platform_interface 暴露抽象层 Ch934xSerialPlatform,
默认实现是 MethodChannelCh934xSerial(绑定到 ch934x_serial 通道)。
注入自定义平台实现即可在宿主测试中验证上层逻辑,无需启动 Android:
import 'package:ch934x_serial/ch934x_serial.dart';
import 'package:ch934x_serial/ch934x_serial_method_channel.dart';
import 'package:ch934x_serial/ch934x_serial_platform_interface.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
class FakePlatform extends Ch934xSerialPlatform with MockPlatformInterfaceMixin {
@override
Future<List<Ch934xDeviceInfo>> getDeviceList() async => const [];
// ... 其它方法按需返回
}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
test('替换为 mock 后 Ch934xSerial 委托给 fake', () async {
Ch934xSerialPlatform.instance = FakePlatform();
final plugin = Ch934xSerial();
expect(await plugin.getDeviceList(), isEmpty);
});
}
也可以直接用 Ch934xSerial.withPlatform(fake) 局部注入,避免污染
Ch934xSerialPlatform.instance。完整 mock 示例见
test/ch934x_serial_test.dart 与 test/ch934x_serial_method_channel_test.dart。
6. 常见问题(FAQ)
Q1. getDeviceList() 返回空数组。
- 确认 OTG 数据线连接稳固。
- 部分设备需要先在系统设置中开启"USB 调试"或插入 OTG 后等几秒。
- 确认
minSdk >= 24。
Q2. openPort() 返回 false。
- 多数情况是 USB 权限未授予:确认
requestUsbPermission/openPort内部弹出的授权框被用户点击了"允许"。 - 如果目标设备具有多个接口,请尝试修改
interfaceNumber。 - 确认设备中至少有一个
Ch934xSerialPortInfo(getSerialPortList返回),否则原 SDK 也无法打开。注意: 该方法必须先openPort才返回真实数据,枚举阶段调用只会得到空列表。
Q3. read() 一直返回空数组。
- 确认对端设备正在发送数据,且波特率/校验位等参数与原 SDK 默认值
一致(
CH934XLib提供独立的setConfig接口,本插件当前未做 封装,需要时可扩展原 SDK 直接调用)。 - 检查线序:RX/TX 是否接反,以及硬件流控是否正确。
- 确认
setActivePort已切换到正确的串口索引。
Q4. setExceptionCallback 没有触发。
- 本插件仅在原生层主动推送时才会触发回调,目前沁恒 SDK 在以下
场景会自动调用:设备热拔插(
usbDeviceDetach)、USB 权限被 拒绝(usbDevicePermission(result=false))、Modem overrun / parity / frame 错误。其他异常(超时、CRC 错误等)目前不会 触发,可在原生层Ch934xSerialPlugin.java中扩展MethodChannel.invokeMethod("onException", payload)上报。
Q5. 是否支持 iOS / 桌面 / Web?
- 当前仅在 Android 端验证通过;
CH934XLib.jar由沁恒官方提供 Android 端 SDK,其他平台需要厂商另行提供或自行实现。
Q6. 如何做单元测试?
- 注入自定义的
Ch934xSerialPlatform即可,详见 §5。
Q7. 同一设备如何在多个串口之间切换?
- 使用
setActivePort(serialPortIndex),无需重新openPort。openPort之后该设备的所有串口都已连接,后续读写/GPIO/Modem 都作用于"当前活跃串口"。
Q8. Android 侧需要哪些初始化?
- 插件自带
<uses-feature android:name="android.hardware.usb.host" />。 - 业务侧不需要在
AndroidManifest.xml中声明 USB 权限BroadcastReceiver——插件会在需要时动态注册(NOT_EXPORTED)。 - 若希望应用启动即初始化 SDK,可在
Application.onCreate中 调用CH934XManager.getInstance().init(this);不调用也不影响 插件工作,插件会在onAttachedToEngine兜底初始化。
7. 接口总览
下表汇总插件中暴露的全部 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 | 设备关闭 |
setActivePort |
5.1 增强 | 串口切换(插件新增) |
requestUsbPermission |
2.2 增强 | 主动授权(插件新增) |
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 增强 | 串口流(插件新增) |
8. 反馈与贡献
遇到问题请提供:
- 复现步骤(设备型号、Android 版本、是否开启 USB 调试)
- 完整日志(建议使用
adb logcat过滤ch934x_serial标签) - 期望结果 vs 实际结果
提交 Issue 时附上以上信息可以大幅加快排查速度。