Files
ch934x_serial/docs/CH934X_Plugin_使用说明.md
T
Developer e0d1a1775d feat(android): 添加 CH934X USB 转串口芯片支持
- 在 AndroidManifest.xml 中添加 USB Host 权限声明
- 集成 CH934X Android SDK 并通过反射调用原生功能
- 实现设备查找、串口读写、GPIO 和 Modem 控制功能
- 添加异常回调机制处理设备拔出等情况
- 提供 Stream 数据流支持实时串口数据监听
- 完善单元测试覆盖所有核心功能模块
2026-07-06 16:06:54 +08:00

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

  1. 确认 android/app/build.gradleminSdk >= 24,CH934X SDK 不支持 更低版本。

  2. android/app/src/main/AndroidManifest.xml 中声明 USB Host 能力:

    <uses-feature
        android:name="android.hardware.usb.host"
        android:required="true" />
    
  3. 运行时申请 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 中的常量(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() 调试其字段。
  • 同一会话仅保留最近一次打开的串口对象,与原 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 轮询一次,可调整 intervalchunkSize。 业务方在 widget dispose 时记得 cancel 订阅并 closePort

3.4 GPIO 接口

final ok = await plugin.setGpioOutput(gpioNumber: 0, level: 1);
final value = await plugin.getGpioInput(0); // 负值表示失败
  • gpioNumberlevel 与原文档保持一致(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 时附上以上信息可以大幅加快排查速度。