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

357 lines
11 KiB
Markdown

# 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
<uses-feature
android:name="android.hardware.usb.host"
android:required="true" />
```
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<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` 中的常量(`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<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` 即可:
```dart
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 时附上以上信息可以大幅加快排查速度。