# 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` 中加入依赖 ```yaml dependencies: flutter: sdk: flutter ch934x_serial: ^1.0.0 ``` 执行 `flutter pub get` 完成依赖拉取。 ### 2.2 Android 工程准备 1. 确认 `android/app/build.gradle` 中 `minSdk >= 24`,CH934X SDK 不支持 更低版本。 2. 在 `android/app/src/main/AndroidManifest.xml` 中声明 USB Host 能力: ```xml ``` 3. **运行时申请 USB 权限**。本插件不主动申请权限,需业务方调用 Android `UsbManager` 申请并接收广播,例如在 `MainActivity.onCreate` 中: ```kotlin 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 第一次调用 ```dart import 'package:ch934x_serial/ch934x_serial.dart'; Future 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` - `getSerialPortList(...)` → `List` - `Ch934xDeviceInfo` 包含 `deviceId / vendorId / productId / deviceType / serialNumber / productName / manufacturerName / interfaceCount / serialPorts`,可通过 `isCh934x` 判定是否被识别为 CH934X 设备。 - `deviceType` 取值为 `Ch934xDeviceType` 中的常量(`ch9344`、`ch9344L`、 `ch9350`、`ch9348Q`、`ch9342`、`ch934xOther`,未识别为 `unknown = -1`)。 ### 3.2 设备打开与关闭 ```dart 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 串口读写 ```dart // 写入 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 接口 ```dart final ok = await plugin.setGpioOutput(gpioNumber: 0, level: 1); final value = await plugin.getGpioInput(0); // 负值表示失败 ``` - `gpioNumber` 与 `level` 与原文档保持一致(0/1)。 - `getGpioInput` 失败时返回 -1,业务方需要自行处理。 ### 3.5 Modem 控制接口 ```dart 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 异常回调 ```dart 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`。 ```dart import 'dart:async'; import 'dart:typed_data'; import 'package:ch934x_serial/ch934x_serial.dart'; class SerialBridge { SerialBridge() : _plugin = Ch934xSerial(); final Ch934xSerial _plugin; StreamSubscription? _exceptionSub; StreamSubscription? _dataSub; Future 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 sendString(String s) async { final bytes = Uint8List.fromList(s.codeUnits); await _plugin.write(bytes); } Future dispose() async { await _dataSub?.cancel(); await _exceptionSub?.cancel(); await _plugin.closePort(); } } ``` --- ## 5. 常见问题(FAQ) **Q1. `getDeviceList()` 返回空数组。** - 确认已声明 ``。 - 确认 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` 即可: ```dart class FakePlatform extends Ch934xSerialPlatform with MockPlatformInterfaceMixin { @override Future> 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 时附上以上信息可以大幅加快排查速度。