feat(android): 添加 CH934X USB 转串口芯片支持
- 在 AndroidManifest.xml 中添加 USB Host 权限声明 - 集成 CH934X Android SDK 并通过反射调用原生功能 - 实现设备查找、串口读写、GPIO 和 Modem 控制功能 - 添加异常回调机制处理设备拔出等情况 - 提供 Stream 数据流支持实时串口数据监听 - 完善单元测试覆盖所有核心功能模块
This commit is contained in:
@@ -0,0 +1,356 @@
|
||||
# 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 时附上以上信息可以大幅加快排查速度。
|
||||
Reference in New Issue
Block a user