docs(ch934x): 更新插件使用说明文档

- 添加同设备内串口切换功能说明
- 新增主动申请 USB 权限功能描述
- 简化 Android 权限配置流程,移除手动声明 USB Host 能力要求
- 修正 getSerialPortList 方法使用说明,强调需先 openPort
- 更新底层 SDK 接口映射关系
- 完善异常回调类型定义和说明
- 增加平台接口与单元测试相关内容
- 更新常见问题解答和接口总览
This commit is contained in:
Developer
2026-07-06 17:04:42 +08:00
parent 76377e52f2
commit 71fd9f52ec
+153 -91
View File
@@ -14,14 +14,18 @@ Android SDK 封装的 Flutter 插件,文档面向其他 Flutter 开发者,介绍
- **支持的平台:** Android(已实现,iOS / Web / Desktop 暂不支持)。 - **支持的平台:** Android(已实现,iOS / Web / Desktop 暂不支持)。
- **主要能力:** - **主要能力:**
- CH934X 设备枚举、序列号读取、芯片类型识别 - CH934X 设备枚举、序列号读取、芯片类型识别
- 多串口打开 / 关闭 - 多串口打开 / 关闭 / **同设备内串口切换**
- 字节级串口读写 - 字节级串口读写
- GPIO 输出 / 输入 - GPIO 输出 / 输入
- Modem 控制 (DTR/RTS) 与状态 (CTS/DSR/RI/DCD) 读取 - Modem 控制 (DTR/RTS) 与状态 (CTS/DSR/RI/DCD) 读取
- 设备拔出等异常事件回调 - **主动申请 USB 权限**
- 设备拔出、Modem 错误等异常事件回调
- **底层依赖:** 沁恒官方 `CH934XLib.jar`(插件随包发布,位于 - **底层依赖:** 沁恒官方 `CH934XLib.jar`(插件随包发布,位于
`android/libs/CH934XLib.jar`),通过反射方式桥接,无需在调用 `android/libs/CH934XLib.jar`),通过原生 `CH934XManager` 单例直接调用,
业务代码中额外处理。 无需在调用方业务代码中额外处理反射
- **平台通道名称:** `ch934x_serial`
- **Dart 包名:** `ch934x_serial`(主类 `Ch934xSerial`)
- **Android 包名:** `com.xiarui.ch934x_serial`
--- ---
@@ -42,48 +46,19 @@ dependencies:
1. 确认 `android/app/build.gradle``minSdk >= 24`,CH934X SDK 不支持 1. 确认 `android/app/build.gradle``minSdk >= 24`,CH934X SDK 不支持
更低版本。 更低版本。
2. `android/app/src/main/AndroidManifest.xml` 中声明 USB Host 能力: 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` 中额外声明。
```xml > 如果你仍想自己接管权限流程(例如在主界面"刷新设备列表"之后
<uses-feature > 立刻弹一次授权请求),直接调用 `Ch934xSerial.requestUsbPermission`
android:name="android.hardware.usb.host" > 即可,内部会等待用户的回应并以 `bool` 返回结果。
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 第一次调用 ### 2.3 第一次调用
@@ -100,7 +75,10 @@ Future<void> scan() async {
} }
``` ```
> 如果调用后列表为空,请先确认已经完成第 2.2 步的 USB 权限申请。 > **注意:** `getSerialPortList` 在设备尚未 `openPort` 时会返回空
> 列表,因为 SDK 的 `getSerialCount` 必须先 `openDevice` 才能返回
> 真实值。完整流程是:扫描 → 选设备 → `openPort` →
> `getSerialPortList` 拿真实串口数,详见 §3.2。
--- ---
@@ -113,10 +91,10 @@ Future<void> scan() async {
| Dart 方法 | 原 SDK 接口 | 说明 | | Dart 方法 | 原 SDK 接口 | 说明 |
| --------- | ----------- | ---- | | --------- | ----------- | ---- |
| `getDeviceList()` | `UsbHelper.getCH934XDeviceList` | 获取全部 CH934X 设备 | | `getDeviceList()` | `CH934XManager.enumDevice` + `getChipType` | 获取全部 CH934X 设备 |
| `getSerialNumber(deviceId)` | `UsbHelper.CH934XSerialNum` | 获取指定设备的序列号 | | `getSerialNumber(deviceId)` | `UsbDevice.getSerialNumber` | 获取指定设备的序列号 |
| `getDeviceType(deviceId)` | `UsbHelper.CH934XDeviceType` | 获取设备类型常量 | | `getDeviceType(deviceId)` | `CH934XManager.getChipType` | 获取设备类型常量 |
| `getSerialPortList(deviceId, interfaceNumber: n)` | `UsbHelper.getCH934XSerialPortList` | 获取设备串口列表 | | `getSerialPortList(deviceId, interfaceNumber: n)` | `CH934XManager.getSerialCount` | 获取设备串口列表(需先 `openPort`) |
返回类型: 返回类型:
@@ -125,10 +103,13 @@ Future<void> scan() async {
- `Ch934xDeviceInfo` 包含 `deviceId / vendorId / productId / deviceType / - `Ch934xDeviceInfo` 包含 `deviceId / vendorId / productId / deviceType /
serialNumber / productName / manufacturerName / interfaceCount / serialNumber / productName / manufacturerName / interfaceCount /
serialPorts`,可通过 `isCh934x` 判定是否被识别为 CH934X 设备。 serialPorts`,可通过 `isCh934x` 判定是否被识别为 CH934X 设备。
- **`isCh934x`:** 当 `deviceType` 落在 `ch9344..ch934xOther` 范围内
时返回 `true`(`unknown = -1` 不算)。
- `Ch934xSerialPortInfo` 包含 `portIndex / devicePath / driverName`。
- `deviceType` 取值为 `Ch934xDeviceType` 中的常量(`ch9344`、`ch9344L`、 - `deviceType` 取值为 `Ch934xDeviceType` 中的常量(`ch9344`、`ch9344L`、
`ch9350`、`ch9348Q`、`ch9342`、`ch934xOther`,未识别为 `unknown = -1`)。 `ch9350`、`ch9348Q`、`ch9342`、`ch934xOther`,未识别为 `unknown = -1`)。
### 3.2 设备打开关闭 ### 3.2 设备打开关闭与串口切换
```dart ```dart
final target = Ch934xPortTarget( final target = Ch934xPortTarget(
@@ -146,9 +127,32 @@ await plugin.closePort();
``` ```
- `Ch934xPortTarget` 封装了 `deviceId` / `interfaceNumber` / - `Ch934xPortTarget` 封装了 `deviceId` / `interfaceNumber` /
`serialPortIndex` 三个参数,可通过 `toMap()` 调试其字段 `serialPortIndex` 三个参数,可通过 `toMap()` 调试其字段,也可
通过 `Ch934xPortTarget.fromMap` 从 MethodChannel 回包中还原。
- **`openPort` 内部会自动处理 USB 权限申请**:若 SDK 抛
`NoPermissionException`,会主动调起系统对话框并等待用户回应
(最多 60 秒),授权成功后会自动重试 `openDevice`。
- **同设备内串口切换(`setActivePort`)**:CH934X 设备一次 `openPort`
之后所有串口都已连接,后续读写只需切换 `serialPortIndex`,
无需重新 `open`。`setActivePort(idx)` 仅切换当前活跃串口,
不重复打开设备:
```dart
// 串口 #0 已打开,切换到 #2
await plugin.setActivePort(2);
```
- **主动申请 USB 权限**(`requestUsbPermission`):
```dart
final granted = await plugin.requestUsbPermission(device.deviceId);
if (granted) {
// 用户已授权,可继续 openPort
}
```
- 同一会话仅保留最近一次打开的串口对象,与原 SDK 行为一致;打开新 - 同一会话仅保留最近一次打开的串口对象,与原 SDK 行为一致;打开新
串口前请先 `closePort()` 或在 finally 块中清理。 串口前请先 `closePort()` 或在 `finally` 块中清理。
### 3.3 串口读写 ### 3.3 串口读写
@@ -171,9 +175,16 @@ await sub.cancel();
``` ```
- `read(length)` 返回 `Uint8List`,无数据或失败时为空。 - `read(length)` 返回 `Uint8List`,无数据或失败时为空。
- `length <= 0` 时**不会调用原生层**,直接返回空数组。
- SDK 的 `readData` 会返回内部缓冲的所有数据,插件按
`min(data.length, length)` 截断后回传。
- `write(data)` 返回实际写入字节数,失败时为 0。 - `write(data)` 返回实际写入字节数,失败时为 0。
- `dataStream` 默认每 20ms 轮询一次,可调整 `interval` 与 `chunkSize`。 - `dataStream` 默认 `chunkSize = 1024`、`interval = 20ms`,内部
业务方在 widget dispose 时记得 `cancel` 订阅并 `closePort`。 持续以 [interval] 为周期反复调用 `read`;当底层无数据时返回
空缓冲区,消费者可据此判定。`chunkSize <= 0` 会抛
`ArgumentError`。
- 业务方在 widget dispose 时记得 `cancel` 订阅并 `closePort`。
- `read`/`write` 都针对**当前活跃串口**;切换串口用 `setActivePort`。
### 3.4 GPIO 接口 ### 3.4 GPIO 接口
@@ -184,6 +195,7 @@ final value = await plugin.getGpioInput(0); // 负值表示失败
- `gpioNumber` 与 `level` 与原文档保持一致(0/1)。 - `gpioNumber` 与 `level` 与原文档保持一致(0/1)。
- `getGpioInput` 失败时返回 -1,业务方需要自行处理。 - `getGpioInput` 失败时返回 -1,业务方需要自行处理。
- GPIO 操作同样作用于**当前活跃串口**。
### 3.5 Modem 控制接口 ### 3.5 Modem 控制接口
@@ -195,9 +207,10 @@ if (ModemStatus.isSet(status, ModemStatus.cts)) {
} }
``` ```
- `ModemStatus` 暴露位掩码常量 `cts / dsr / ri / dcd` 与工具方法 - `ModemStatus` 暴露位掩码常量 `cts(0x01) / dsr(0x02) / ri(0x04) /
`isSet(status, mask)`,取值与文档表格一致。 dcd(0x08)` 与工具方法 `isSet(status, mask)`,取值与文档表格一致。
- `getModemStatus()` 返回 0 时表示无有效状态,业务方注意判空。 - `getModemStatus()` 返回**最近一次由 SDK 回调推送的位掩码**;
若 SDK 尚未推送过任何状态,返回 0。业务方注意判空。
### 3.6 异常回调 ### 3.6 异常回调
@@ -211,10 +224,15 @@ final sub = await plugin.setExceptionCallback((event) {
await sub.cancel(); await sub.cancel();
``` ```
- 异常类型见 `Ch934xExceptionType`(`deviceDetached / ioError / sdk / - 异常类型见 `Ch934xExceptionType`:
unknown`)。 - `deviceDetached = 1`:设备被拔出(由 SDK 的 `usbDeviceDetach` 触发)。
- 返回的 `StreamSubscription` 需要在合适时机 cancel,以便释放监听 - `ioError = 2`:Modem overrun / parity / frame 等错误。
与平台资源 - `sdk = 3`:原生 SDK 主动抛出的其他异常(目前未触发,保留语义)
- `unknown = 0`:未识别(当前未触发,保留语义)。
- `Ch934xException` 包含 `type / message / cause`,`toString()` 会把
`type` 翻译为对应的常量名,方便日志/UI 显示。
- 返回的 `StreamSubscription` **必须在合适时机 `cancel`**,以便
释放底层 `StreamController` 与 `MethodCallHandler`。
--- ---
@@ -261,6 +279,11 @@ class SerialBridge {
await _plugin.write(bytes); await _plugin.write(bytes);
} }
/// 切换到同一设备的另一个串口,无需重新 open。
Future<void> switchToPort(int index) async {
await _plugin.setActivePort(index);
}
Future<void> dispose() async { Future<void> dispose() async {
await _dataSub?.cancel(); await _dataSub?.cancel();
await _exceptionSub?.cancel(); await _exceptionSub?.cancel();
@@ -271,32 +294,70 @@ class SerialBridge {
--- ---
## 5. 常见问题(FAQ) ## 5. 平台接口与单元测试
插件使用 `plugin_platform_interface` 暴露抽象层 `Ch934xSerialPlatform`,
默认实现是 `MethodChannelCh934xSerial`(绑定到 `ch934x_serial` 通道)。
注入自定义平台实现即可在宿主测试中验证上层逻辑,无需启动 Android:
```dart
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.dart` 与 `test/ch934x_serial_method_channel_test.dart`。
---
## 6. 常见问题(FAQ)
**Q1. `getDeviceList()` 返回空数组。** **Q1. `getDeviceList()` 返回空数组。**
- 确认已声明 `<uses-feature android:name="android.hardware.usb.host" />` - 确认 OTG 数据线连接稳固
- 确认 Android `UsbManager` 已对目标设备授权(系统会弹出对话框,需要 - 部分设备需要先在系统设置中开启"USB 调试"或插入 OTG 后等几秒。
用户点击"允许") - 确认 `minSdk >= 24`
- 确认 OTG 数据线连接稳固,并尝试调用 `setExceptionCallback` 监听
`deviceDetached` 事件,排查设备是否被系统频繁弹出。
**Q2. `openPort()` 返回 false。** **Q2. `openPort()` 返回 false。**
- 多数情况是 USB 权限未授予;请在 `UsbManager.requestPermission` - 多数情况是 USB 权限未授予:确认 `requestUsbPermission` /
返回 true 后再调用 `openPort`。 `openPort` 内部弹出的授权框被用户点击了"允许"
- 如果目标设备具有多个接口,请尝试修改 `interfaceNumber`。 - 如果目标设备具有多个接口,请尝试修改 `interfaceNumber`。
- 确认设备中至少有一个 `Ch934xSerialPortInfo`(`getSerialPortList` - 确认设备中至少有一个 `Ch934xSerialPortInfo`(`getSerialPortList`
返回),否则原 SDK 也无法打开。 返回),否则原 SDK 也无法打开。**注意:** 该方法必须先 `openPort`
才返回真实数据,枚举阶段调用只会得到空列表。
**Q3. `read()` 一直返回空数组。** **Q3. `read()` 一直返回空数组。**
- 确认对端设备正在发送数据,且波特率/校验位等参数与原 SDK 默认值 - 确认对端设备正在发送数据,且波特率/校验位等参数与原 SDK 默认值
一致(`CH934XLib` 提供独立的 `setConfig` 接口,本插件当前未做 一致(`CH934XLib` 提供独立的 `setConfig` 接口,本插件当前未做
封装,需要时可扩展原 SDK 反射调用)。 封装,需要时可扩展原 SDK 直接调用)。
- 检查线序:RX/TX 是否接反,以及硬件流控是否正确。 - 检查线序:RX/TX 是否接反,以及硬件流控是否正确。
- 确认 `setActivePort` 已切换到正确的串口索引。
**Q4. `setExceptionCallback` 没有触发。** **Q4. `setExceptionCallback` 没有触发。**
- 本插件仅在原生层主动推送时才会触发回调,目前沁恒 SDK 在设备热拔 - 本插件仅在原生层主动推送时才会触发回调,目前沁恒 SDK 在以下
场景会自动调用,其他异常(超时、CRC 错误等)可能不会触发。 场景会自动调用:设备热拔插(`usbDeviceDetach`)、USB 权限被
如需丰富事件类型,可在原生层 `Ch934xSerialPlugin.java` 中扩展 拒绝(`usbDevicePermission(result=false)`)、Modem overrun /
parity / frame 错误。其他异常(超时、CRC 错误等)目前不会
触发,可在原生层 `Ch934xSerialPlugin.java` 中扩展
`MethodChannel.invokeMethod("onException", payload)` 上报。 `MethodChannel.invokeMethod("onException", payload)` 上报。
**Q5. 是否支持 iOS / 桌面 / Web?** **Q5. 是否支持 iOS / 桌面 / Web?**
@@ -304,27 +365,26 @@ class SerialBridge {
Android 端 SDK,其他平台需要厂商另行提供或自行实现。 Android 端 SDK,其他平台需要厂商另行提供或自行实现。
**Q6. 如何做单元测试?** **Q6. 如何做单元测试?**
- 注入自定义的 `Ch934xSerialPlatform` 即可: - 注入自定义的 `Ch934xSerialPlatform` 即可,详见 §5。
```dart **Q7. 同一设备如何在多个串口之间切换?**
class FakePlatform extends Ch934xSerialPlatform - 使用 `setActivePort(serialPortIndex)`,无需重新 `openPort`。
with MockPlatformInterfaceMixin { `openPort` 之后该设备的所有串口都已连接,后续读写/GPIO/Modem
@override 都作用于"当前活跃串口"。
Future<List<Ch934xDeviceInfo>> getDeviceList() async => const [];
// ... 其它方法按需返回
}
Ch934xSerialPlatform.instance = FakePlatform(); **Q8. Android 侧需要哪些初始化?**
final plugin = Ch934xSerial(); - 插件自带 `<uses-feature android:name="android.hardware.usb.host" />`。
``` - 业务侧不需要在 `AndroidManifest.xml` 中声明 USB 权限
`BroadcastReceiver`——插件会在需要时动态注册(`NOT_EXPORTED`)。
插件仓库的 `test/ch934x_serial_test.dart` 给出了完整 mock 示例。 - 若希望应用启动即初始化 SDK,可在 `Application.onCreate` 中
调用 `CH934XManager.getInstance().init(this)`;不调用也不影响
插件工作,插件会在 `onAttachedToEngine` 兜底初始化。
--- ---
## 6. 接口总览 ## 7. 接口总览
下表汇总插件中暴露的全部 API,具体调用示例见上文第 3 节。 下表汇总插件中暴露的全部 API,具体调用示例见上文第 3 节。
| Dart 方法 | 文档编号 | 分类 | | Dart 方法 | 文档编号 | 分类 |
| --------- | -------- | ---- | | --------- | -------- | ---- |
@@ -334,6 +394,8 @@ class SerialBridge {
| `getSerialPortList` | 4.1.3 | 设备查找 | | `getSerialPortList` | 4.1.3 | 设备查找 |
| `openPort` | 5.1.1 | 设备打开 | | `openPort` | 5.1.1 | 设备打开 |
| `closePort` | 5.2.1 | 设备关闭 | | `closePort` | 5.2.1 | 设备关闭 |
| `setActivePort` | 5.1 增强 | 串口切换(插件新增) |
| `requestUsbPermission` | 2.2 增强 | 主动授权(插件新增) |
| `read` | 6.1.1 | 串口读写 | | `read` | 6.1.1 | 串口读写 |
| `write` | 6.2.1 | 串口读写 | | `write` | 6.2.1 | 串口读写 |
| `setGpioOutput` | 7.1.1 | GPIO | | `setGpioOutput` | 7.1.1 | GPIO |
@@ -345,7 +407,7 @@ class SerialBridge {
--- ---
## 7. 反馈与贡献 ## 8. 反馈与贡献
遇到问题请提供: 遇到问题请提供: