Files
ch934x_serial/docs/CH934X_Plugin_使用说明.md
T
Developer 020904bd2a feat(ch934x): 添加GPIO方向控制和串口参数配置功能
- 添加Ch934xMode和GpioDirection枚举类定义
- 实现GPIO方向控制相关方法(setGpioDir/queryGpioDirFromCache)
- 添加GPIO使能控制(enableGpio)和数量/组数查询(getGpiocount/getGpiogroup)
- 实现串口参数配置(setSerialParameter)包括波特率/数据位/停止位/校验位/流控
- 添加Break信号控制(setBreak)功能
- 实现当前串口模式查询(getCurrentMode)
- 添加设备连接状态查询(isConnected)和已打开设备列表获取(getConnectedDevices)
- 更新文档说明GPIO方向控制和串口参数配置用法
- 在示例应用中添加相关功能按钮和操作逻辑
2026-07-07 16:15:11 +08:00

19 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 权限
    • Break 信号控制
    • 串口参数配置(波特率/数据位/停止位/校验/流控)
    • 查询设备连接状态与已打开设备列表
    • 查询 GPIO 数量、组信息
    • 设备拔出、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 接口

// 设置 GPIO 输出电平
final ok = await plugin.setGpioOutput(gpioNumber: 0, level: 1);

// 读取 GPIO 输入电平(负值表示失败)
final value = await plugin.getGpioInput(0);
  • gpioNumberlevel 与原文档保持一致(0/1)。
  • getGpioInput 失败时返回 -1,业务方需要自行处理。
  • GPIO 操作同样作用于当前活跃串口

3.4.1 GPIO 方向控制

// 使能 GPIO(需要先获取 chipType,可从 Ch934xDeviceInfo.deviceType 取得)
await plugin.enableGpio(chipType: device.deviceType, gpioGroup: 0, enable: 1);

// 设置 GPIO 方向
await plugin.setGpioDir(gpioGroup: 0, gpioNumber: 0, dir: GpioDirection.out);

// 从缓存查询 GPIO 方向
final dir = await plugin.queryGpioDirFromCache(gpioGroup: 0, gpioNumber: 0);
if (dir == GpioDirection.out) { /* 输出 */ }

// 查询 GPIO 数量与组数
final count = await plugin.getGpiocount(device.deviceId);
final groups = await plugin.getGpiogroup(device.deviceId);
  • GpioDirection 常量: in_ = 0(输入)、out = 1(输出)。
  • enableGpioenable 参数: CH9344 传 1/0 控制整组;CH348 使用位掩码。
  • 方向查询返回 -1 表示失败。
  • getGpiocount/getGpiogroup 失败时返回 -1,需先 openPort

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

3.7 其他接口

3.7.1 Break 信号

final ok = await plugin.setBreak(true); // 设置 Break(低电平有效)
  • 作用于当前活跃串口

3.7.2 获取当前串口模式

final mode = await plugin.getCurrentMode();
// mode 取值见 Ch934xMode 常量
  • Ch934xMode 常量: normal = 0(普通)、hardflow = 1(硬件流控)、gpio = 2(GPIO)。
  • 仅对 CH934X 型号有效,CH348 返回 -1。

3.7.3 设备连接状态

final connected = await plugin.isConnected(device.deviceId);
final ids = await plugin.getConnectedDevices();
  • isConnected: 查询指定设备是否已被打开。
  • getConnectedDevices: 返回当前已打开设备的 deviceId 列表。

3.8 串口参数配置

// 配置波特率为 9600
final ok = await plugin.setSerialParameter(
  baud: 9600,
  dataBit: 8,
  stopBit: 1,
  parityBit: 0,
  flow: false,
);
  • 作用于当前活跃串口openPort 内部默认设为 115200/8/1/N/无流控, 如需修改可在此之后调用。
  • stopBit: 0=1 停止位,1=1.5 停止位,2=2 停止位。
  • parityBit: 0=无校验,1=奇校验,2=偶校验。

4. 完整示例

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() 一直返回空数组。

  • 确认对端设备正在发送数据,且波特率/校验位等参数匹配。可通过 setSerialParameter(baud: 9600, dataBit: 8, stopBit: 1, parityBit: 0, flow: false) 配置串口参数。
  • 检查线序: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 串口读写
setSerialParameter setSerialParameter 串口参数
setBreak setBreak 其他
getCurrentMode getCurrentMode 其他
isConnected isConnected 设备状态
getConnectedDevices getConnectedDevices 设备状态
setGpioOutput setGPIOValue GPIO
getGpioInput getGPIOValue GPIO
setGpioDir setGPIODir GPIO 方向
queryGpioDirFromCache queryGPIODirFromCache GPIO 方向
enableGpio enableGPIO GPIO
getGpiocount getGPIOCount GPIO
getGpiogroup getGPIOGroup GPIO
setModemControl 7.2.1 Modem
getModemStatus 7.2.2 Modem
setExceptionCallback 7.3.1 异常
dataStream 6.1 增强 串口流(插件新增)
portDataStream 6.1 增强 多端口轮询流(插件新增)

8. 反馈与贡献

遇到问题请提供:

  • 复现步骤(设备型号、Android 版本、是否开启 USB 调试)
  • 完整日志(建议使用 adb logcat 过滤 ch934x_serial 标签)
  • 期望结果 vs 实际结果

提交 Issue 时附上以上信息可以大幅加快排查速度。