Files
ch934x_serial/docs/CH934X_Plugin_使用说明.md
T
Developer 71fd9f52ec docs(ch934x): 更新插件使用说明文档
- 添加同设备内串口切换功能说明
- 新增主动申请 USB 权限功能描述
- 简化 Android 权限配置流程,移除手动声明 USB Host 能力要求
- 修正 getSerialPortList 方法使用说明,强调需先 openPort
- 更新底层 SDK 接口映射关系
- 完善异常回调类型定义和说明
- 增加平台接口与单元测试相关内容
- 更新常见问题解答和接口总览
2026-07-06 17:04:42 +08:00

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 工程准备

  1. 确认 android/app/build.gradleminSdk >= 24,CH934X SDK 不支持 更低版本。
  2. 无需在 AndroidManifest.xml 中声明 USB Host 能力——插件自带 android/src/main/AndroidManifest.xml 已声明 <uses-feature android:name="android.hardware.usb.host" required="true" />, 应用侧合并后即可生效。
  3. 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 才能返回 真实值。完整流程是:扫描 → 选设备 → openPortgetSerialPortList 拿真实串口数,详见 §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 中的常量(ch9344ch9344Lch9350ch9348Qch9342ch934xOther,未识别为 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, 无需重新 opensetActivePort(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 = 1024interval = 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); // 负值表示失败
  • gpioNumberlevel 与原文档保持一致(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,以便 释放底层 StreamControllerMethodCallHandler

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.darttest/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),无需重新 openPortopenPort 之后该设备的所有串口都已连接,后续读写/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 时附上以上信息可以大幅加快排查速度。