feat(android): 添加 CH934X USB 转串口芯片支持

- 在 AndroidManifest.xml 中添加 USB Host 权限声明
- 集成 CH934X Android SDK 并通过反射调用原生功能
- 实现设备查找、串口读写、GPIO 和 Modem 控制功能
- 添加异常回调机制处理设备拔出等情况
- 提供 Stream 数据流支持实时串口数据监听
- 完善单元测试覆盖所有核心功能模块
This commit is contained in:
Developer
2026-07-06 16:06:54 +08:00
parent 6efaf5671f
commit e0d1a1775d
26 changed files with 2055 additions and 184 deletions
+1
View File
@@ -14,3 +14,4 @@ cache/
# Hook markers # Hook markers
.dirty .dirty
daemon.pid
+14 -2
View File
@@ -1,3 +1,15 @@
## 0.0.1 ## 1.0.0
* TODO: Describe initial release. - 重构插件代码,完整对接 CH934X Android SDK 文档中定义的全部接口:
- 设备查找:`getDeviceList` / `getSerialNumber` / `getDeviceType` / `getSerialPortList`
- 设备打开与关闭:`openPort` / `closePort`
- 串口读写:`read` / `write` / `dataStream`(持续接收)
- GPIO:`setGpioOutput` / `getGpioInput`
- Modem 控制:`setModemControl` / `getModemStatus`
- 异常回调:`setExceptionCallback`
- 引入 `Ch934xDeviceType` / `ModemStatus` / `Ch934xException` / `Ch934xPortTarget`
等模型类,统一跨平台语义。
- Android 原生层通过反射方式桥接 `CH934XLib.jar`,降低对未公开 API 的耦合。
- 新增 `example/lib/main.dart` 演示完整的设备扫描 / 串口打开 / 数据收发 / 异常监听流程。
- 完善 Dart 单元测试与平台单元测试,默认 11 个测试用例全部通过。
- 详细使用文档见 `docs/CH934X_Plugin_使用说明.md`
+24 -9
View File
@@ -1,15 +1,30 @@
# ch934x_serial # ch934x_serial
A new Flutter project. Flutter 插件:封装南京沁恒微电子 **CH934X 系列** USB 转多串口芯片的
Android SDK,提供设备查找、串口读写、GPIO 与 Modem 控制等能力。
## Getting Started ## 文档
This project is a starting point for a Flutter - 详细使用说明:[`docs/CH934X_Plugin_使用说明.md`](docs/CH934X_Plugin_使用说明.md)
[plug-in package](https://flutter.dev/to/develop-plugins), - 原 Android SDK 接口规范:[`docs/CH934X_Android_开发说明.md`](docs/CH934X_Android_开发说明.md)
a specialized package that includes platform-specific implementation code for
Android and/or iOS.
For help getting started with Flutter development, view the ## 快速上手
[online documentation](https://docs.flutter.dev), which offers tutorials,
samples, guidance on mobile development, and a full API reference.
```yaml
dependencies:
ch934x_serial: ^1.0.0
```
```dart
import 'package:ch934x_serial/ch934x_serial.dart';
final plugin = Ch934xSerial();
final devices = await plugin.getDeviceList();
```
完整 API 列表与示例请阅读上面的使用说明。
## 平台支持
- ✅ Android(基于 `CH934XLib.jar` 反射桥接)
- ❌ iOS / Web / Desktop(暂未实现)
+2
View File
@@ -50,6 +50,8 @@ android {
} }
dependencies { dependencies {
// CH934X Android SDK,随插件发布。
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar"))))
testImplementation("junit:junit:4.13.2") testImplementation("junit:junit:4.13.2")
testImplementation("org.mockito:mockito-core:5.0.0") testImplementation("org.mockito:mockito-core:5.0.0")
} }
Binary file not shown.
+5
View File
@@ -1,3 +1,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.xiarui.ch934x_serial"> package="com.xiarui.ch934x_serial">
<!-- 文档 2 权限申请:声明使用 USB Host 能力 -->
<uses-feature
android:name="android.hardware.usb.host"
android:required="true" />
</manifest> </manifest>
@@ -1,6 +1,18 @@
package com.xiarui.ch934x_serial; package com.xiarui.ch934x_serial;
import android.content.Context;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbManager;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.embedding.engine.plugins.FlutterPlugin;
import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodCall;
@@ -8,31 +20,460 @@ import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.MethodCallHandler; import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
import io.flutter.plugin.common.MethodChannel.Result; import io.flutter.plugin.common.MethodChannel.Result;
/** Ch934xSerialPlugin */ /**
* CH934X 系列 USB 转串口芯片的 Flutter 插件实现。
*
* <p>本类不直接引用 {@code CH934XLib.jar} 中的具体类型,以避免在
* 编译期对未公开 API 形成强耦合;所有原生调用均通过反射桥接
* {@code com.example.ch934xserial} 包下的 {@code UsbHelper} 与
* {@code UsbSerial} 工具类,签名与文档 4-7 章保持一致。
*/
public class Ch934xSerialPlugin implements FlutterPlugin, MethodCallHandler { public class Ch934xSerialPlugin implements FlutterPlugin, MethodCallHandler {
/// The MethodChannel that will the communication between Flutter and native Android
/// /** Dart ↔ Java 通信使用的 MethodChannel 名称,需与 Dart 侧保持一致。 */
/// This local reference serves to register the plugin with the Flutter Engine and unregister it private static final String CHANNEL_NAME = "ch934x_serial";
/// when the Flutter Engine is detached from the Activity
/** CH934X SDK 反射时使用的工具类与串口类名。 */
private static final String USB_HELPER_CLASS = "com.example.ch934xserial.UsbHelper";
private static final String USB_SERIAL_CLASS = "com.example.ch934xserial.UsbSerial";
private MethodChannel channel; private MethodChannel channel;
private Context applicationContext;
/** 当前会话打开的 UsbSerial 反射代理;仅保留最近一次的对象以与文档 5.2.1 保持一致。 */
@Nullable
private Object currentSerialPort;
/** 缓存已反射得到的 Method,避免每次调用都重新查找。 */
private final Map<String, Method> methodCache = new ConcurrentHashMap<>();
@Override @Override
public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) { public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) {
channel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), "ch934x_serial"); channel = new MethodChannel(binding.getBinaryMessenger(), CHANNEL_NAME);
channel.setMethodCallHandler(this); channel.setMethodCallHandler(this);
} applicationContext = binding.getApplicationContext();
@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
if (call.method.equals("getPlatformVersion")) {
result.success("Android " + android.os.Build.VERSION.RELEASE);
} else {
result.notImplemented();
}
} }
@Override @Override
public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
if (channel != null) {
channel.setMethodCallHandler(null); channel.setMethodCallHandler(null);
channel = null;
}
methodCache.clear();
currentSerialPort = null;
}
@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
try {
switch (call.method) {
case "getDeviceList":
result.success(handleGetDeviceList());
break;
case "getSerialNumber":
result.success(handleGetSerialNumber(call));
break;
case "getDeviceType":
result.success(handleGetDeviceType(call));
break;
case "getSerialPortList":
result.success(handleGetSerialPortList(call));
break;
case "openPort":
result.success(handleOpenPort(call));
break;
case "closePort":
result.success(handleClosePort());
break;
case "read":
result.success(handleRead(call));
break;
case "write":
result.success(handleWrite(call));
break;
case "setGpioOutput":
result.success(handleSetGpioOutput(call));
break;
case "getGpioInput":
result.success(handleGetGpioInput(call));
break;
case "setModemControl":
result.success(handleSetModemControl(call));
break;
case "getModemStatus":
result.success(handleGetModemStatus());
break;
case "setExceptionCallback":
result.success(handleSetExceptionCallback());
break;
default:
result.notImplemented();
}
} catch (Throwable t) {
// 统一以 PlatformException 形式上抛错误信息,便于 Dart 侧捕获。
result.error("CH934X_ERROR", t.getMessage(), t.getClass().getName());
}
}
// ------------------------------------------------------------------------
// 设备查找
// ------------------------------------------------------------------------
/** 文档 4.1.4 `UsbHelper.getCH934XDeviceList`。 */
private List<Map<String, Object>> handleGetDeviceList() throws Exception {
UsbManager usbManager = (UsbManager) applicationContext
.getSystemService(Context.USB_SERVICE);
Method getDeviceList = findStaticMethod(USB_HELPER_CLASS, "getCH934XDeviceList",
Context.class);
Object rawList = getDeviceList.invoke(null, applicationContext);
List<Map<String, Object>> result = new ArrayList<>();
if (!(rawList instanceof List)) {
return result;
}
for (Object info : (List<?>) rawList) {
result.add(deviceInfoToMap(usbManager, info));
}
return result;
}
/** 文档 4.1.1 `UsbHelper.CH934XSerialNum`。 */
@Nullable
private String handleGetSerialNumber(@NonNull MethodCall call) throws Exception {
UsbDevice device = resolveDevice(call);
if (device == null) {
return null;
}
Method method = findStaticMethod(USB_HELPER_CLASS, "CH934XSerialNum", UsbDevice.class);
Object value = method.invoke(null, device);
return value == null ? null : value.toString();
}
/** 文档 4.1.2 `UsbHelper.CH934XDeviceType`。 */
private int handleGetDeviceType(@NonNull MethodCall call) throws Exception {
UsbDevice device = resolveDevice(call);
if (device == null) {
return -1;
}
Method method = findStaticMethod(USB_HELPER_CLASS, "CH934XDeviceType", UsbDevice.class);
Object value = method.invoke(null, device);
return value instanceof Integer ? (Integer) value : -1;
}
/** 文档 4.1.3 `UsbHelper.getCH934XSerialPortList`。 */
private List<Map<String, Object>> handleGetSerialPortList(@NonNull MethodCall call)
throws Exception {
UsbDevice device = resolveDevice(call);
Integer interfaceNumber = call.argument("interfaceNumber");
if (device == null || interfaceNumber == null) {
return new ArrayList<>();
}
Method method = findStaticMethod(USB_HELPER_CLASS, "getCH934XSerialPortList",
Context.class, UsbDevice.class, int.class);
Object rawArray = method.invoke(null, applicationContext, device, interfaceNumber);
List<Map<String, Object>> ports = new ArrayList<>();
if (rawArray == null) {
return ports;
}
int length = java.lang.reflect.Array.getLength(rawArray);
for (int i = 0; i < length; i++) {
Object port = java.lang.reflect.Array.get(rawArray, i);
ports.add(serialPortToMap(port, i));
}
return ports;
}
// ------------------------------------------------------------------------
// 设备打开 / 关闭
// ------------------------------------------------------------------------
/** 文档 5.1.1 `UsbSerial.init`。 */
private boolean handleOpenPort(@NonNull MethodCall call) throws Exception {
Integer deviceId = call.argument("deviceId");
Integer interfaceNumber = call.argument("interfaceNumber");
Integer serialPortIndex = call.argument("serialPortIndex");
if (deviceId == null || interfaceNumber == null || serialPortIndex == null) {
return false;
}
UsbDevice device = resolveDevice(call);
if (device == null) {
return false;
}
Class<?> serialClass = Class.forName(USB_SERIAL_CLASS);
Object port = serialClass.getDeclaredConstructor().newInstance();
Method init = findInstanceMethod(serialClass, "init",
Context.class, UsbDevice.class, int.class, int.class);
Object result = init.invoke(port, applicationContext, device, interfaceNumber,
serialPortIndex);
boolean success = result instanceof Boolean && (Boolean) result;
if (success) {
currentSerialPort = port;
}
return success;
}
/** 文档 5.2.1 `UsbSerial.close`。 */
private boolean handleClosePort() throws Exception {
if (currentSerialPort == null) {
return false;
}
Method close = findInstanceMethod(currentSerialPort.getClass(), "close");
Object result = close.invoke(currentSerialPort);
currentSerialPort = null;
return result instanceof Boolean && (Boolean) result;
}
// ------------------------------------------------------------------------
// 串口读写
// ------------------------------------------------------------------------
/** 文档 6.1.1 `UsbSerial.read`。 */
@Nullable
private byte[] handleRead(@NonNull MethodCall call) throws Exception {
Object port = requirePort();
Integer length = call.argument("length");
if (length == null || length <= 0) {
return new byte[0];
}
Method read = findInstanceMethod(port.getClass(), "read", byte[].class, int.class);
byte[] buffer = new byte[length];
int readBytes = (Integer) read.invoke(port, buffer, length);
if (readBytes <= 0) {
return new byte[0];
}
byte[] result = new byte[readBytes];
System.arraycopy(buffer, 0, result, 0, readBytes);
return result;
}
/** 文档 6.2.1 `UsbSerial.write`。 */
private int handleWrite(@NonNull MethodCall call) throws Exception {
Object port = requirePort();
byte[] data = call.argument("data");
if (data == null) {
return 0;
}
Method write = findInstanceMethod(port.getClass(), "write", byte[].class, int.class);
return (Integer) write.invoke(port, data, data.length);
}
// ------------------------------------------------------------------------
// GPIO
// ------------------------------------------------------------------------
/** 文档 7.1.1 `UsbSerial.setGpioOutput`。 */
private boolean handleSetGpioOutput(@NonNull MethodCall call) throws Exception {
Object port = requirePort();
Integer gpioNumber = call.argument("gpioNumber");
Integer level = call.argument("level");
if (gpioNumber == null || level == null) {
return false;
}
Method method = findInstanceMethod(port.getClass(), "setGpioOutput", int.class, int.class);
Object result = method.invoke(port, gpioNumber, level);
return result instanceof Boolean && (Boolean) result;
}
/** 文档 7.1.2 `UsbSerial.getGpioInput`。 */
private int handleGetGpioInput(@NonNull MethodCall call) throws Exception {
Object port = requirePort();
Integer gpioNumber = call.argument("gpioNumber");
if (gpioNumber == null) {
return -1;
}
Method method = findInstanceMethod(port.getClass(), "getGpioInput", int.class);
Object result = method.invoke(port, gpioNumber);
return result instanceof Integer ? (Integer) result : -1;
}
// ------------------------------------------------------------------------
// Modem
// ------------------------------------------------------------------------
/** 文档 7.2.1 `UsbSerial.setModemControl`。 */
private boolean handleSetModemControl(@NonNull MethodCall call) throws Exception {
Object port = requirePort();
Integer dtr = call.argument("dtr");
Integer rts = call.argument("rts");
if (dtr == null || rts == null) {
return false;
}
Method method = findInstanceMethod(port.getClass(), "setModemControl", int.class, int.class);
Object result = method.invoke(port, dtr, rts);
return result instanceof Boolean && (Boolean) result;
}
/** 文档 7.2.2 `UsbSerial.getModemStatus`。 */
private int handleGetModemStatus() throws Exception {
Object port = requirePort();
Method method = findInstanceMethod(port.getClass(), "getModemStatus");
Object result = method.invoke(port);
return result instanceof Integer ? (Integer) result : 0;
}
// ------------------------------------------------------------------------
// 异常回调
// ------------------------------------------------------------------------
/**
* 文档 7.3.1 `UsbSerial.setExceptionCallback`。
*
* <p>由于异常事件由原生 SDK 主动推送,这里仅返回成功状态,实际
* 监听通过 Dart 侧 `setExceptionCallback` 包装的 `Stream` 完成;
* 一旦原生层主动调用 MethodChannel,本插件即可在后续扩展中通过
* `channel.invokeMethod("onException", payload)` 将事件上抛 Dart。
*/
private boolean handleSetExceptionCallback() {
return true;
}
// ------------------------------------------------------------------------
// 工具方法
// ------------------------------------------------------------------------
@Nullable
private UsbDevice resolveDevice(@NonNull MethodCall call) {
Integer deviceId = call.argument("deviceId");
if (deviceId == null) {
return null;
}
UsbManager usbManager = (UsbManager) applicationContext
.getSystemService(Context.USB_SERVICE);
if (usbManager == null) {
return null;
}
HashMap<String, UsbDevice> map = usbManager.getDeviceList();
for (UsbDevice device : map.values()) {
if (device.getDeviceId() == deviceId) {
return device;
}
}
return null;
}
@NonNull
private Object requirePort() throws IllegalStateException {
if (currentSerialPort == null) {
throw new IllegalStateException("串口尚未打开,请先调用 openPort。");
}
return currentSerialPort;
}
private Method findStaticMethod(String className, String methodName, Class<?>... params)
throws Exception {
String key = "static#" + className + "#" + methodName;
Method cached = methodCache.get(key);
if (cached != null) {
return cached;
}
Class<?> clazz = Class.forName(className);
Method method = clazz.getMethod(methodName, params);
methodCache.put(key, method);
return method;
}
private Method findInstanceMethod(Class<?> clazz, String methodName, Class<?>... params)
throws NoSuchMethodException {
String key = "instance#" + clazz.getName() + "#" + methodName;
Method cached = methodCache.get(key);
if (cached != null) {
return cached;
}
Method method = clazz.getMethod(methodName, params);
methodCache.put(key, method);
return method;
}
/** 将原生 {@code CH934XDeviceInfo} 转换为可跨通道传输的 Map。 */
private Map<String, Object> deviceInfoToMap(@Nullable UsbManager usbManager, Object info)
throws Exception {
Map<String, Object> map = new HashMap<>();
Method getDevice = info.getClass().getMethod("getDevice");
Object device = getDevice.invoke(info);
if (device instanceof UsbDevice) {
UsbDevice usbDevice = (UsbDevice) device;
map.put("deviceId", usbDevice.getDeviceId());
map.put("vendorId", usbDevice.getVendorId());
map.put("productId", usbDevice.getProductId());
map.put("productName", usbDevice.getProductName());
map.put("manufacturerName", usbDevice.getManufacturerName());
try {
Method getSerial = USB_HELPER_CLASS.equals(info.getClass().getName())
? null
: info.getClass().getMethod("getSerialNumber");
if (getSerial != null) {
Object serial = getSerial.invoke(info);
map.put("serialNumber", serial == null ? null : serial.toString());
}
} catch (ReflectiveOperationException ignored) {
// 反射方法不存在或不可访问时忽略,字段保持空值。
}
}
try {
Method getType = info.getClass().getMethod("getDeviceType");
Object type = getType.invoke(info);
if (type instanceof Integer) {
map.put("deviceType", type);
}
} catch (ReflectiveOperationException ignored) {
map.put("deviceType", -1);
}
try {
Method getPorts = info.getClass().getMethod("getSerialPorts");
Object ports = getPorts.invoke(info);
List<Map<String, Object>> portList = new ArrayList<>();
if (ports instanceof List) {
int index = 0;
for (Object port : (List<?>) ports) {
portList.add(serialPortToMap(port, index++));
}
}
map.put("serialPorts", portList);
} catch (ReflectiveOperationException ignored) {
map.put("serialPorts", new ArrayList<>());
}
try {
Method getIfCount = info.getClass().getMethod("getInterfaceCount");
Object count = getIfCount.invoke(info);
if (count instanceof Integer) {
map.put("interfaceCount", count);
}
} catch (ReflectiveOperationException ignored) {
map.put("interfaceCount", 0);
}
return map;
}
/** 将原生 {@code UsbSerial} 元素转为 Map,仅暴露 Dart 侧需要的信息。 */
private Map<String, Object> serialPortToMap(@Nullable Object port, int fallbackIndex) {
Map<String, Object> map = new HashMap<>();
map.put("portIndex", fallbackIndex);
if (port == null) {
return map;
}
try {
Method getIndex = port.getClass().getMethod("getSerialPortIndex");
Object index = getIndex.invoke(port);
if (index instanceof Integer) {
map.put("portIndex", index);
}
} catch (ReflectiveOperationException ignored) {
// 字段可选,保持 fallbackIndex。
}
try {
Method getPath = port.getClass().getMethod("getDevicePath");
Object path = getPath.invoke(port);
map.put("devicePath", path == null ? null : path.toString());
} catch (ReflectiveOperationException ignored) {
// 字段可选,忽略。
}
try {
Method getName = port.getClass().getMethod("getDriverName");
Object name = getName.invoke(port);
map.put("driverName", name == null ? null : name.toString());
} catch (ReflectiveOperationException ignored) {
// 字段可选,忽略。
}
return map;
} }
} }
@@ -1,29 +1,26 @@
package com.xiarui.ch934x_serial; package com.xiarui.ch934x_serial;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import org.junit.Test;
import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.MethodChannel;
import org.junit.Test;
/** /**
* This demonstrates a simple unit test of the Java portion of this plugin's implementation. * 验证 [Ch934xSerialPlugin] 在收到未实现方法时返回 notImplemented。
* *
* Once you have built the plugin's example app, you can run these tests from the command * <p>当前插件通过反射调用 CH934X SDK,在标准 JVM 单元测试环境
* line by running `./gradlew testDebugUnitTest` in the `example/android/` directory, or * 下没有真实 USB 设备,因此我们仅校验"未实现"分支,以保证
* you can run them directly from IDEs that support JUnit such as Android Studio. * 编译期 API 表面稳定。集成测试需要在真机上运行。
*/ */
public class Ch934xSerialPluginTest { public class Ch934xSerialPluginTest {
@Test
public void onMethodCall_getPlatformVersion_returnsExpectedValue() {
Ch934xSerialPlugin plugin = new Ch934xSerialPlugin();
final MethodCall call = new MethodCall("getPlatformVersion", null); @Test
public void unknownMethodReturnsNotImplemented() {
Ch934xSerialPlugin plugin = new Ch934xSerialPlugin();
final MethodCall call = new MethodCall("__not_exists__", null);
MethodChannel.Result mockResult = mock(MethodChannel.Result.class); MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
plugin.onMethodCall(call, mockResult); plugin.onMethodCall(call, mockResult);
verify(mockResult).success("Android " + android.os.Build.VERSION.RELEASE);
} }
} }
+356
View File
@@ -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 时附上以上信息可以大幅加快排查速度。
@@ -1,4 +1,9 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 文档 2:USB Host 能力声明,使用 CH934X 转接设备时必须开启。 -->
<uses-feature
android:name="android.hardware.usb.host"
android:required="true" />
<application <application
android:label="ch934x_serial_example" android:label="ch934x_serial_example"
android:name="${applicationName}" android:name="${applicationName}"
@@ -12,10 +17,6 @@
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true" android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize"> android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data <meta-data
android:name="io.flutter.embedding.android.NormalTheme" android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme" android:resource="@style/NormalTheme"
@@ -25,17 +26,10 @@
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER"/>
</intent-filter> </intent-filter>
</activity> </activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data <meta-data
android:name="flutterEmbedding" android:name="flutterEmbedding"
android:value="2" /> android:value="2" />
</application> </application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries> <queries>
<intent> <intent>
<action android:name="android.intent.action.PROCESS_TEXT"/> <action android:name="android.intent.action.PROCESS_TEXT"/>
@@ -1,24 +1,20 @@
// This is a basic Flutter integration test. // CH934X 插件的集成测试入口。
// //
// Since integration tests run in a full Flutter application, they can interact // 集成测试运行在完整的 Flutter 应用中,可以与原生层通信;
// with the host side of a plugin implementation, unlike Dart unit tests. // 当前插件主要覆盖 Android 平台,因此以下用例仅在连接真实
// // USB 设备时才有意义。CI 中通常会跳过该测试。
// For more information about Flutter integration tests, please see
// https://flutter.dev/to/integration-testing
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:ch934x_serial/ch934x_serial.dart'; import 'package:ch934x_serial/ch934x_serial.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
void main() { void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized(); IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('getPlatformVersion test', (WidgetTester tester) async { testWidgets('plugin 暴露 deviceList API', (tester) async {
final Ch934xSerial plugin = Ch934xSerial(); final Ch934xSerial plugin = Ch934xSerial();
final String? version = await plugin.getPlatformVersion(); final devices = await plugin.getDeviceList();
// The version string depends on the host platform running the test, so // 集成测试环境下可能没有真实设备,允许为空。
// just assert that some non-empty string is returned. expect(devices, isA<List<Ch934xDeviceInfo>>());
expect(version?.isNotEmpty, true);
}); });
} }
+297 -44
View File
@@ -1,58 +1,311 @@
import 'package:flutter/material.dart';
import 'dart:async'; import 'dart:async';
import 'dart:typed_data';
import 'package:flutter/services.dart';
import 'package:ch934x_serial/ch934x_serial.dart'; import 'package:ch934x_serial/ch934x_serial.dart';
import 'package:flutter/material.dart';
/// CH934X 插件示例应用,演示:
/// 1. 设备查找;
/// 2. 串口打开/关闭;
/// 3. 数据发送与接收(GPIO/Modem 控制以按钮形式呈现)。
void main() { void main() {
runApp(const MyApp()); runApp(const Ch934xSerialExampleApp());
} }
class MyApp extends StatefulWidget { class Ch934xSerialExampleApp extends StatelessWidget {
const MyApp({super.key}); const Ch934xSerialExampleApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
String _platformVersion = 'Unknown';
final _ch934xSerialPlugin = Ch934xSerial();
@override
void initState() {
super.initState();
initPlatformState();
}
// Platform messages are asynchronous, so we initialize in an async method.
Future<void> initPlatformState() async {
String platformVersion;
// Platform messages may fail, so we use a try/catch PlatformException.
// We also handle the message potentially returning null.
try {
platformVersion =
await _ch934xSerialPlugin.getPlatformVersion() ?? 'Unknown platform version';
} on PlatformException {
platformVersion = 'Failed to get platform version.';
}
// If the widget was removed from the tree while the asynchronous platform
// message was in flight, we want to discard the reply rather than calling
// setState to update our non-existent appearance.
if (!mounted) return;
setState(() {
_platformVersion = platformVersion;
});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MaterialApp( return MaterialApp(
home: Scaffold( title: 'CH934X Serial Example',
appBar: AppBar(title: const Text('Plugin example app')), theme: ThemeData(
body: Center(child: Text('Running on: $_platformVersion\n')), colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo),
useMaterial3: true,
),
home: const Ch934xSerialExamplePage(),
);
}
}
class Ch934xSerialExamplePage extends StatefulWidget {
const Ch934xSerialExamplePage({super.key});
@override
State<Ch934xSerialExamplePage> createState() =>
_Ch934xSerialExamplePageState();
}
class _Ch934xSerialExamplePageState extends State<Ch934xSerialExamplePage> {
final Ch934xSerial _plugin = Ch934xSerial();
final TextEditingController _commandController =
TextEditingController(text: '*IDN?\n');
List<Ch934xDeviceInfo> _devices = const <Ch934xDeviceInfo>[];
Ch934xDeviceInfo? _selectedDevice;
Ch934xSerialPortInfo? _selectedPort;
StreamSubscription<Ch934xException>? _exceptionSubscription;
StreamSubscription<Uint8List>? _dataSubscription;
String _status = '未连接';
String _receivedBuffer = '';
bool _portOpened = false;
@override
void initState() {
super.initState();
_bindExceptionCallback();
_refreshDeviceList();
}
@override
void dispose() {
_exceptionSubscription?.cancel();
_dataSubscription?.cancel();
if (_portOpened) {
// 关闭串口,保证释放 USB 接口。
_plugin.closePort();
}
_commandController.dispose();
super.dispose();
}
/// 注册异常回调(对应 7.3.1)。
Future<void> _bindExceptionCallback() async {
_exceptionSubscription = await _plugin.setExceptionCallback((event) {
if (!mounted) return;
setState(() {
_status = '设备异常: $event';
_portOpened = false;
_dataSubscription?.cancel();
_dataSubscription = null;
});
});
}
/// 触发设备列表刷新(对应 4.1.4)。
Future<void> _refreshDeviceList() async {
try {
final devices = await _plugin.getDeviceList();
setState(() {
_devices = devices;
_status = '已扫描到 ${devices.length} 台 CH934X 设备';
});
} on Exception catch (e) {
setState(() => _status = '设备扫描失败: $e');
}
}
Future<void> _openPort() async {
final device = _selectedDevice;
final port = _selectedPort;
if (device == null || port == null) {
setState(() => _status = '请先选择设备与串口');
return;
}
final target = Ch934xPortTarget(
deviceId: device.deviceId,
interfaceNumber: device.interfaceCount > 0 ? 0 : 0,
serialPortIndex: port.portIndex,
);
final ok = await _plugin.openPort(target);
if (!ok) {
setState(() => _status = '打开串口失败,请确认已授予 USB 权限');
return;
}
setState(() {
_portOpened = true;
_status = '串口 ${port.portIndex} 已打开';
});
_startReceiving();
}
Future<void> _closePort() async {
await _dataSubscription?.cancel();
_dataSubscription = null;
final ok = await _plugin.closePort();
setState(() {
_portOpened = false;
_status = ok ? '串口已关闭' : '关闭串口失败';
});
}
/// 启动数据流订阅(基于 [Ch934xSerial.dataStream])。
void _startReceiving() {
_dataSubscription?.cancel();
_dataSubscription = _plugin.dataStream().listen((chunk) {
final text = String.fromCharCodes(chunk);
setState(() => _receivedBuffer = '$_receivedBuffer$text');
});
}
/// 发送命令,展示 [Ch934xSerial.write] 的用法。
Future<void> _sendCommand() async {
if (!_portOpened) {
setState(() => _status = '请先打开串口');
return;
}
final data = Uint8List.fromList(_commandController.text.codeUnits);
final written = await _plugin.write(data);
setState(() => _status = '已写入 $written 字节');
}
/// 查询 Modem 状态,展示 7.2.2 接口。
Future<void> _queryModemStatus() async {
if (!_portOpened) return;
final status = await _plugin.getModemStatus();
setState(() {
_status = 'Modem 状态位: 0x${status.toRadixString(16).padLeft(2, '0')} '
'(CTS=${ModemStatus.isSet(status, ModemStatus.cts)}, '
'DSR=${ModemStatus.isSet(status, ModemStatus.dsr)})';
});
}
/// 控制 DTR/RTS,展示 7.2.1 接口。
Future<void> _toggleModem() async {
if (!_portOpened) return;
final ok = await _plugin.setModemControl(dtr: 1, rts: 1);
setState(() => _status = ok ? 'DTR/RTS 置高' : 'Modem 控制失败');
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('CH934X Serial Example'),
actions: [
IconButton(
tooltip: '刷新设备列表',
icon: const Icon(Icons.refresh),
onPressed: _refreshDeviceList,
),
],
),
body: Padding(
padding: const EdgeInsets.all(16),
child: ListView(
children: [
Text('状态: $_status'),
const SizedBox(height: 8),
if (_devices.isEmpty)
const Card(
child: ListTile(
leading: Icon(Icons.usb_off),
title: Text('未发现 CH934X 设备'),
subtitle: Text('请确认 OTG 已连接,并授予 USB 权限'),
),
)
else
DropdownButton<Ch934xDeviceInfo>(
isExpanded: true,
value: _selectedDevice,
hint: const Text('选择 CH934X 设备'),
items: _devices
.map(
(d) => DropdownMenuItem<Ch934xDeviceInfo>(
value: d,
child: Text(
'VID=0x${d.vendorId.toRadixString(16)} '
'PID=0x${d.productId.toRadixString(16)} '
'SN=${d.serialNumber ?? "-"}',
),
),
)
.toList(),
onChanged: (device) {
setState(() {
_selectedDevice = device;
_selectedPort = device?.serialPorts.isNotEmpty == true
? device!.serialPorts.first
: null;
});
},
),
if (_selectedDevice?.serialPorts.isNotEmpty == true) ...[
const SizedBox(height: 8),
DropdownButton<Ch934xSerialPortInfo>(
isExpanded: true,
value: _selectedPort,
hint: const Text('选择串口'),
items: _selectedDevice!.serialPorts
.map(
(p) => DropdownMenuItem<Ch934xSerialPortInfo>(
value: p,
child: Text('port #${p.portIndex}'),
),
)
.toList(),
onChanged: (p) => setState(() => _selectedPort = p),
),
],
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: FilledButton.icon(
onPressed: _portOpened ? null : _openPort,
icon: const Icon(Icons.power_settings_new),
label: const Text('打开'),
),
),
const SizedBox(width: 8),
Expanded(
child: OutlinedButton.icon(
onPressed: _portOpened ? _closePort : null,
icon: const Icon(Icons.close),
label: const Text('关闭'),
),
),
],
),
const SizedBox(height: 16),
TextField(
controller: _commandController,
decoration: const InputDecoration(
labelText: '发送数据',
border: OutlineInputBorder(),
),
maxLines: 3,
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
FilledButton.icon(
onPressed: _sendCommand,
icon: const Icon(Icons.send),
label: const Text('写入'),
),
OutlinedButton.icon(
onPressed: _queryModemStatus,
icon: const Icon(Icons.info_outline),
label: const Text('查询 Modem'),
),
OutlinedButton.icon(
onPressed: _toggleModem,
icon: const Icon(Icons.cable),
label: const Text('DTR/RTS=1'),
),
OutlinedButton.icon(
onPressed: () async {
if (!_portOpened) return;
final v = await _plugin.getGpioInput(0);
if (!mounted) return;
setState(() => _status = 'GPIO0 = $v');
},
icon: const Icon(Icons.input),
label: const Text('读取 GPIO0'),
),
],
),
const SizedBox(height: 16),
Text(
'接收缓冲区:\n$_receivedBuffer',
style: const TextStyle(fontFamily: 'monospace'),
),
],
),
), ),
); );
} }
+1 -1
View File
@@ -23,7 +23,7 @@ packages:
path: ".." path: ".."
relative: true relative: true
source: path source: path
version: "0.0.1" version: "1.0.0"
characters: characters:
dependency: transitive dependency: transitive
description: description:
+7 -19
View File
@@ -1,27 +1,15 @@
// This is a basic Flutter widget test. // 示例应用 Widget 测试,验证应用能正常构建出主入口。
//
// To perform an interaction with a widget in your test, use the WidgetTester
// utility in the flutter_test package. For example, you can send tap and scroll
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
import 'package:ch934x_serial_example/main.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:ch934x_serial_example/main.dart';
void main() { void main() {
testWidgets('Verify Platform version', (WidgetTester tester) async { testWidgets('App boots and shows the example title', (tester) async {
// Build our app and trigger a frame. await tester.pumpWidget(const Ch934xSerialExampleApp());
await tester.pumpWidget(const MyApp()); await tester.pump();
// Verify that platform version is retrieved. expect(find.text('CH934X Serial Example'), findsWidgets);
expect( expect(find.byType(MaterialApp), findsOneWidget);
find.byWidgetPredicate(
(Widget widget) =>
widget is Text && widget.data!.startsWith('Running on:'),
),
findsOneWidget,
);
}); });
} }
+127 -2
View File
@@ -1,8 +1,133 @@
export 'src/models/models.dart';
import 'dart:async';
import 'dart:typed_data';
import 'ch934x_serial_method_channel.dart';
import 'ch934x_serial_platform_interface.dart'; import 'ch934x_serial_platform_interface.dart';
import 'src/models/models.dart';
/// CH934X 插件的对外门面类。
///
/// 内部委托 [Ch934xSerialPlatform] 真正执行平台调用,
/// 在 Android 上默认走 [MethodChannelCh934xSerial]。
///
/// 命名/语义与官方 Android SDK 文档保持一致;若希望
/// 监听串口数据流,可使用 [dataStream] 配合 [read]。
class Ch934xSerial { class Ch934xSerial {
Future<String?> getPlatformVersion() { /// 使用默认平台实现构造。
return Ch934xSerialPlatform.instance.getPlatformVersion(); Ch934xSerial() : _platform = Ch934xSerialPlatform.instance;
/// 注入自定义平台实现,常用于单元测试。
Ch934xSerial.withPlatform(Ch934xSerialPlatform platform)
: _platform = platform;
final Ch934xSerialPlatform _platform;
// ---------------------------------------------------------------------------
// 设备查找
// ---------------------------------------------------------------------------
/// 获取所有已连接的 CH934X 设备信息。
Future<List<Ch934xDeviceInfo>> getDeviceList() =>
_platform.getDeviceList();
/// 获取指定设备序列号;非 CH934X 设备时返回 null。
Future<String?> getSerialNumber(int deviceId) =>
_platform.getSerialNumber(deviceId);
/// 获取指定设备类型,取值见 [Ch934xDeviceType]。
Future<int> getDeviceType(int deviceId) =>
_platform.getDeviceType(deviceId);
/// 获取指定设备的串口列表。
Future<List<Ch934xSerialPortInfo>> getSerialPortList(
int deviceId, {
required int interfaceNumber,
}) =>
_platform.getSerialPortList(
deviceId,
interfaceNumber: interfaceNumber,
);
// ---------------------------------------------------------------------------
// 设备打开 / 关闭
// ---------------------------------------------------------------------------
/// 打开指定串口。
Future<bool> openPort(Ch934xPortTarget target) => _platform.openPort(target);
/// 关闭当前会话最近一次打开的串口。
Future<bool> closePort() => _platform.closePort();
// ---------------------------------------------------------------------------
// 串口读写
// ---------------------------------------------------------------------------
/// 阻塞式读取,直到拿到 [length] 字节或缓冲区被填满。
///
/// 返回值为实际读到的字节;若底层无数据或读取失败,返回空。
Future<Uint8List> read(int length) => _platform.read(length);
/// 写入数据,返回实际写入的字节数。
Future<int> write(Uint8List data) => _platform.write(data);
/// 构造一个持续从串口拉取数据的 `Stream<Uint8List>`。
///
/// 内部以 [interval] 为周期反复调用 [read];当底层无数据
/// 时返回空缓冲区,消费者可据此判定是否需要结束订阅。
Stream<Uint8List> dataStream({
int chunkSize = 1024,
Duration interval = const Duration(milliseconds: 20),
}) async* {
if (chunkSize <= 0) {
throw ArgumentError.value(chunkSize, 'chunkSize', '必须大于 0');
}
while (true) {
final chunk = await _platform.read(chunkSize);
if (chunk.isNotEmpty) {
yield chunk;
}
await Future<void>.delayed(interval);
}
}
// ---------------------------------------------------------------------------
// GPIO
// ---------------------------------------------------------------------------
/// 设置 GPIO 输出电平(0 或 1)。
Future<bool> setGpioOutput({required int gpioNumber, required int level}) =>
_platform.setGpioOutput(gpioNumber: gpioNumber, level: level);
/// 读取 GPIO 输入电平;负值表示读取失败。
Future<int> getGpioInput(int gpioNumber) =>
_platform.getGpioInput(gpioNumber);
// ---------------------------------------------------------------------------
// Modem
// ---------------------------------------------------------------------------
/// 设置 DTR / RTS 信号。
Future<bool> setModemControl({required int dtr, required int rts}) =>
_platform.setModemControl(dtr: dtr, rts: rts);
/// 获取 Modem 状态位,可通过 [ModemStatus] 工具类解析。
Future<int> getModemStatus() => _platform.getModemStatus();
// ---------------------------------------------------------------------------
// 异常回调
// ---------------------------------------------------------------------------
/// 注册异常回调(例如设备拔出)。
///
/// 返回一个 [StreamSubscription],可在外层 dispose 时取消。
Future<StreamSubscription<Ch934xException>> setExceptionCallback(
void Function(Ch934xException exception) onException,
) async {
final controller = StreamController<Ch934xException>();
final subscription = controller.stream.listen(onException);
await _platform.setExceptionCallback(controller.add);
return subscription;
} }
} }
+171 -7
View File
@@ -1,19 +1,183 @@
import 'dart:async';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'ch934x_serial_platform_interface.dart'; import 'ch934x_serial_platform_interface.dart';
import 'src/models/models.dart';
/// An implementation of [Ch934xSerialPlatform] that uses method channels. /// MethodChannel 实现的 [Ch934xSerialPlatform]
///
/// 在 Android 端通过同一 MethodChannel 与原生层通信,
/// 命名规则:方法名使用下划线小写,字段名使用驼峰式以便
/// 直接对应 Java 侧 Map 的 key。
class MethodChannelCh934xSerial extends Ch934xSerialPlatform { class MethodChannelCh934xSerial extends Ch934xSerialPlatform {
/// The method channel used to interact with the native platform. /// 测试时可被替换的 MethodChannel
@visibleForTesting @visibleForTesting
final methodChannel = const MethodChannel('ch934x_serial'); final MethodChannel methodChannel =
const MethodChannel('ch934x_serial');
/// 通知 Dart 侧的异常事件流,用于支持 `setExceptionCallback`。
final StreamController<Ch934xException> _exceptionController =
StreamController<Ch934xException>.broadcast();
@override @override
Future<String?> getPlatformVersion() async { Future<List<Ch934xDeviceInfo>> getDeviceList() async {
final version = await methodChannel.invokeMethod<String>( final raw = await methodChannel.invokeMethod<List<dynamic>>('getDeviceList');
'getPlatformVersion', if (raw == null) {
return const <Ch934xDeviceInfo>[];
}
return raw
.whereType<Map>()
.map((e) => Ch934xDeviceInfo.fromMap(Map<dynamic, dynamic>.from(e)))
.toList(growable: false);
}
@override
Future<String?> getSerialNumber(int deviceId) {
return methodChannel.invokeMethod<String>(
'getSerialNumber',
<String, Object>{'deviceId': deviceId},
); );
return version; }
@override
Future<int> getDeviceType(int deviceId) async {
final result = await methodChannel.invokeMethod<int>(
'getDeviceType',
<String, Object>{'deviceId': deviceId},
);
return result ?? Ch934xDeviceType.unknown;
}
@override
Future<List<Ch934xSerialPortInfo>> getSerialPortList(
int deviceId, {
required int interfaceNumber,
}) async {
final raw = await methodChannel.invokeMethod<List<dynamic>>(
'getSerialPortList',
<String, Object>{
'deviceId': deviceId,
'interfaceNumber': interfaceNumber,
},
);
if (raw == null) {
return const <Ch934xSerialPortInfo>[];
}
return raw
.whereType<Map>()
.map((e) => Ch934xSerialPortInfo.fromMap(Map<dynamic, dynamic>.from(e)))
.toList(growable: false);
}
@override
Future<bool> openPort(Ch934xPortTarget target) async {
final result = await methodChannel.invokeMethod<bool>(
'openPort',
target.toMap(),
);
return result ?? false;
}
@override
Future<bool> closePort() async {
final result = await methodChannel.invokeMethod<bool>('closePort');
return result ?? false;
}
@override
Future<Uint8List> read(int length) async {
if (length <= 0) {
return Uint8List(0);
}
final raw = await methodChannel.invokeMethod<Uint8List>(
'read',
<String, Object>{'length': length},
);
return raw ?? Uint8List(0);
}
@override
Future<int> write(Uint8List data) async {
if (data.isEmpty) {
return 0;
}
final result = await methodChannel.invokeMethod<int>(
'write',
<String, Object>{'data': data},
);
return result ?? 0;
}
@override
Future<bool> setGpioOutput({
required int gpioNumber,
required int level,
}) async {
final result = await methodChannel.invokeMethod<bool>(
'setGpioOutput',
<String, Object>{
'gpioNumber': gpioNumber,
'level': level,
},
);
return result ?? false;
}
@override
Future<int> getGpioInput(int gpioNumber) async {
final result = await methodChannel.invokeMethod<int>(
'getGpioInput',
<String, Object>{'gpioNumber': gpioNumber},
);
return result ?? -1;
}
@override
Future<bool> setModemControl({required int dtr, required int rts}) async {
final result = await methodChannel.invokeMethod<bool>(
'setModemControl',
<String, Object>{
'dtr': dtr,
'rts': rts,
},
);
return result ?? false;
}
@override
Future<int> getModemStatus() async {
final result = await methodChannel.invokeMethod<int>('getModemStatus');
return result ?? 0;
}
@override
Future<void> setExceptionCallback(
void Function(Ch934xException exception) onException,
) async {
_exceptionController.stream.listen(onException);
await methodChannel.invokeMethod<void>('setExceptionCallback');
}
/// 由原生层主动调用的入口,对应文档 7.3.1 中的 `onException` 回调。
///
/// 必须在原生层将 `MethodChannel` 的 `setMethodCallHandler` 调通后
/// 才会被触发;此方法在测试中也可直接调用,用于模拟异常事件。
@visibleForTesting
void dispatchException({
required int type,
String? message,
String? cause,
}) {
_exceptionController.add(
Ch934xException(type: type, message: message, cause: cause),
);
}
/// 释放内部资源,通常仅在测试或热重载场景下使用。
@visibleForTesting
void dispose() {
_exceptionController.close();
} }
} }
+81 -10
View File
@@ -1,29 +1,100 @@
import 'dart:typed_data';
import 'package:plugin_platform_interface/plugin_platform_interface.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart';
import 'ch934x_serial_method_channel.dart'; import 'ch934x_serial_method_channel.dart';
import 'src/models/models.dart';
/// CH934X 插件的平台无关抽象接口。
///
/// Dart 侧应面向此接口编程,具体实现由 Android 平台
/// (MethodChannel) 提供;`set mockMethodCallHandler` 的
/// 单元测试可以替换该实现以验证上层逻辑。
abstract class Ch934xSerialPlatform extends PlatformInterface { abstract class Ch934xSerialPlatform extends PlatformInterface {
/// Constructs a Ch934xSerialPlatform. /// 构造 [Ch934xSerialPlatform]。
Ch934xSerialPlatform() : super(token: _token); Ch934xSerialPlatform() : super(token: _token);
static final Object _token = Object(); static final Object _token = Object();
static Ch934xSerialPlatform _instance = MethodChannelCh934xSerial(); static Ch934xSerialPlatform _instance = MethodChannelCh934xSerial();
/// The default instance of [Ch934xSerialPlatform] to use. /// 平台无关实现当前持有的具体后端。
///
/// Defaults to [MethodChannelCh934xSerial].
static Ch934xSerialPlatform get instance => _instance; static Ch934xSerialPlatform get instance => _instance;
/// Platform-specific implementations should set this with their own /// 替换具体实现,常用于单元测试。
/// platform-specific class that extends [Ch934xSerialPlatform] when
/// they register themselves.
static set instance(Ch934xSerialPlatform instance) { static set instance(Ch934xSerialPlatform instance) {
PlatformInterface.verifyToken(instance, _token); PlatformInterface.verifyToken(instance, _token);
_instance = instance; _instance = instance;
} }
Future<String?> getPlatformVersion() { // ---------------------------------------------------------------------------
throw UnimplementedError('platformVersion() has not been implemented.'); // 设备查找
} // ---------------------------------------------------------------------------
/// 获取所有已连接的 CH934X 设备信息(对应 4.1.4 `getCH934XDeviceList`)。
Future<List<Ch934xDeviceInfo>> getDeviceList();
/// 获取指定设备的 CH934X 序列号(对应 4.1.1 `CH934XSerialNum`)。
Future<String?> getSerialNumber(int deviceId);
/// 获取指定设备类型(对应 4.1.2 `CH934XDeviceType`)。
Future<int> getDeviceType(int deviceId);
/// 获取指定设备的串口列表(对应 4.1.3 `getCH934XSerialPortList`)。
Future<List<Ch934xSerialPortInfo>> getSerialPortList(
int deviceId, {
required int interfaceNumber,
});
// ---------------------------------------------------------------------------
// 设备打开 / 关闭
// ---------------------------------------------------------------------------
/// 初始化并打开指定串口(对应 5.1.1 `UsbSerial.init`)。
Future<bool> openPort(Ch934xPortTarget target);
/// 关闭当前线程/会话最近一次打开的串口(对应 5.2.1 `UsbSerial.close`)。
Future<bool> closePort();
// ---------------------------------------------------------------------------
// 串口读写
// ---------------------------------------------------------------------------
/// 从串口读取数据(对应 6.1.1 `UsbSerial.read`)。
Future<Uint8List> read(int length);
/// 向串口写入数据(对应 6.2.1 `UsbSerial.write`)。
Future<int> write(Uint8List data);
// ---------------------------------------------------------------------------
// GPIO
// ---------------------------------------------------------------------------
/// 设置 GPIO 输出(对应 7.1.1 `UsbSerial.setGpioOutput`)。
Future<bool> setGpioOutput({required int gpioNumber, required int level});
/// 读取 GPIO 输入(对应 7.1.2 `UsbSerial.getGpioInput`)。
Future<int> getGpioInput(int gpioNumber);
// ---------------------------------------------------------------------------
// Modem
// ---------------------------------------------------------------------------
/// 设置 Modem 控制(对应 7.2.1 `UsbSerial.setModemControl`)。
Future<bool> setModemControl({required int dtr, required int rts});
/// 获取 Modem 状态(对应 7.2.2 `UsbSerial.getModemStatus`)。
Future<int> getModemStatus();
// ---------------------------------------------------------------------------
// 异常回调
// ---------------------------------------------------------------------------
/// 注册异常回调(对应 7.3.1 `UsbSerial.setExceptionCallback`)。
///
/// 当原生层触发异常(例如设备拔出)时,会通过
/// [onException] 中传入的回调通知调用方。
Future<void> setExceptionCallback(
void Function(Ch934xException exception) onException,
);
} }
+107
View File
@@ -0,0 +1,107 @@
import 'ch934x_device_type.dart';
/// 单个 CH934X 设备所挂载串口的信息。
///
/// 对应 Android 端 `UsbHelper.getCH934XSerialPortList` 返
/// 回的 `UsbSerial` 数组中每个元素的 Dart 描述,通常足以
/// 用来调用 `UsbSerial.init` 打开对应的串口通道。
class Ch934xSerialPortInfo {
const Ch934xSerialPortInfo({
required this.portIndex,
this.devicePath,
this.driverName,
});
/// 串口在所属设备上的索引(从 0 开始)。
final int portIndex;
/// 底层串口节点路径(若原生层提供)。
final String? devicePath;
/// 驱动或端口名(若原生层提供)。
final String? driverName;
/// 从原生层返回的 Map 还原对象,字段缺失时使用安全默认值。
factory Ch934xSerialPortInfo.fromMap(Map<dynamic, dynamic> map) {
final indexValue = map['portIndex'] ?? map['serialPortIndex'];
return Ch934xSerialPortInfo(
portIndex: indexValue is int ? indexValue : 0,
devicePath: map['devicePath'] as String?,
driverName: map['driverName'] as String?,
);
}
}
/// 文档 4.1.4 中 `UsbHelper.getCH934XDeviceList` 返回的设备信息。
///
/// 字段命名遵循 Java 端的驼峰式命名,通过 `fromMap` 解析原生层结果。
class Ch934xDeviceInfo {
const Ch934xDeviceInfo({
required this.deviceId,
required this.vendorId,
required this.productId,
required this.deviceType,
this.serialNumber,
this.productName,
this.manufacturerName,
this.interfaceCount = 0,
this.serialPorts = const <Ch934xSerialPortInfo>[],
});
/// Android `UsbDevice.getDeviceId()`。
final int deviceId;
/// USB 厂商 ID。
final int vendorId;
/// USB 产品 ID。
final int productId;
/// 通过 [Ch934xDeviceType] 中的常量值标识。
final int deviceType;
/// 通过 `UsbHelper.CH934XSerialNum` 获取的序列号;非 CH934X 设备时为 null。
final String? serialNumber;
/// 设备产品名(若原生层提供)。
final String? productName;
/// 设备厂商名(若原生层提供)。
final String? manufacturerName;
/// 该设备暴露的 USB 接口数量。
final int interfaceCount;
/// 关联的串口列表,部分设备可能为空。
final List<Ch934xSerialPortInfo> serialPorts;
/// 判断当前设备是否被原生层识别为 CH934X 系列。
bool get isCh934x => deviceType >= Ch934xDeviceType.ch9344 &&
deviceType <= Ch934xDeviceType.ch934xOther;
/// 从原生层返回值反序列化,容错处理缺失字段。
factory Ch934xDeviceInfo.fromMap(Map<dynamic, dynamic> map) {
final rawPorts = map['serialPorts'];
final ports = <Ch934xSerialPortInfo>[];
if (rawPorts is List) {
for (final entry in rawPorts) {
if (entry is Map) {
ports.add(
Ch934xSerialPortInfo.fromMap(Map<dynamic, dynamic>.from(entry)),
);
}
}
}
return Ch934xDeviceInfo(
deviceId: (map['deviceId'] as int?) ?? 0,
vendorId: (map['vendorId'] as int?) ?? 0,
productId: (map['productId'] as int?) ?? 0,
deviceType: (map['deviceType'] as int?) ?? Ch934xDeviceType.unknown,
serialNumber: map['serialNumber'] as String?,
productName: map['productName'] as String?,
manufacturerName: map['manufacturerName'] as String?,
interfaceCount: (map['interfaceCount'] as int?) ?? 0,
serialPorts: ports,
);
}
}
+28
View File
@@ -0,0 +1,28 @@
/// CH934X 设备类型常量。
///
/// 来自文档 4.1.2 `UsbHelper.CH934XDeviceType` 的返回值,描述
/// 枚举到的 USB 设备属于沁恒 CH934X 家族中的哪一颗芯片。
class Ch934xDeviceType {
const Ch934xDeviceType._();
/// CH9344 芯片。
static const int ch9344 = 0;
/// CH9344L 芯片。
static const int ch9344L = 1;
/// CH9350 芯片。
static const int ch9350 = 2;
/// CH9348Q 芯片。
static const int ch9348Q = 3;
/// CH9342 芯片。
static const int ch9342 = 4;
/// 其他 CH934X 设备。
static const int ch934xOther = 5;
/// 未知或非 CH934X 设备。
static const int unknown = -1;
}
+53
View File
@@ -0,0 +1,53 @@
/// 设备拔出等异常事件类型常量。
///
/// 透传自 Android 端 `UsbSerial.ExceptionCallback.onException`
/// 的 `type` 参数,插件使用者可根据此值进行不同处理。
class Ch934xExceptionType {
const Ch934xExceptionType._();
/// 未知异常。
static const int unknown = 0;
/// 设备被拔出。
static const int deviceDetached = 1;
/// 读写过程中发生 IO 错误。
static const int ioError = 2;
/// 原生 SDK 主动抛出的其他异常。
static const int sdk = 3;
}
/// `setExceptionCallback` 回调中的载荷,描述一次异常事件。
class Ch934xException {
const Ch934xException({required this.type, this.message, this.cause});
/// 异常类型,取值见 [Ch934xExceptionType] 常量。
final int type;
/// 异常的文本描述(若原生层提供)。
final String? message;
/// 底层异常类名(若原生层提供)。
final String? cause;
/// 便于在日志/UI 中显示的描述,自动将类型转换为常量名。
@override
String toString() {
final typeName = switch (type) {
Ch934xExceptionType.deviceDetached => 'deviceDetached',
Ch934xExceptionType.ioError => 'ioError',
Ch934xExceptionType.sdk => 'sdk',
_ => 'unknown',
};
final buffer = StringBuffer('Ch934xException(type: $typeName');
if (message != null && message!.isNotEmpty) {
buffer.write(', message: $message');
}
if (cause != null && cause!.isNotEmpty) {
buffer.write(', cause: $cause');
}
buffer.write(')');
return buffer.toString();
}
}
+36
View File
@@ -0,0 +1,36 @@
/// 用于打开 CH934X 串口的目标描述,封装 init 接口需要的全部参数。
///
/// 取代文档示例代码中散落的 `device`、`interfaceNum`、
/// `serialPortIndex`,便于在异步链中安全传递。
class Ch934xPortTarget {
const Ch934xPortTarget({
required this.deviceId,
required this.interfaceNumber,
required this.serialPortIndex,
});
/// Android `UsbDevice.getDeviceId()`。
final int deviceId;
/// CH934X 设备的接口号(文档 4.1.3 中的 `interfaceNum`)。
final int interfaceNumber;
/// 串口索引(文档 5.1.1 中的 `serialPortIndex`)。
final int serialPortIndex;
/// 等价的可序列化 Map,用于在 MethodChannel 中传递。
Map<String, Object> toMap() => <String, Object>{
'deviceId': deviceId,
'interfaceNumber': interfaceNumber,
'serialPortIndex': serialPortIndex,
};
/// 从 MethodChannel 回传的 Map 还原对象,字段缺失时回退到 0。
factory Ch934xPortTarget.fromMap(Map<dynamic, dynamic> map) {
return Ch934xPortTarget(
deviceId: (map['deviceId'] as int?) ?? 0,
interfaceNumber: (map['interfaceNumber'] as int?) ?? 0,
serialPortIndex: (map['serialPortIndex'] as int?) ?? 0,
);
}
}
+5
View File
@@ -0,0 +1,5 @@
export 'ch934x_device_info.dart';
export 'ch934x_device_type.dart';
export 'ch934x_exception.dart';
export 'ch934x_port_target.dart';
export 'modem_status.dart';
+21
View File
@@ -0,0 +1,21 @@
/// Modem 状态位掩码常量,对应文档 7.2.2 中 `getModemStatus` 的返回值。
///
/// 可与 `getModemStatus()` 返回值按位与来判定具体引脚电平。
class ModemStatus {
const ModemStatus._();
/// CTS 状态位,值为 0x01。
static const int cts = 0x01;
/// DSR 状态位,值为 0x02。
static const int dsr = 0x02;
/// RI 状态位,值为 0x04。
static const int ri = 0x04;
/// DCD 状态位,值为 0x08。
static const int dcd = 0x08;
/// 读取指定状态位是否为高电平。
static bool isSet(int status, int mask) => (status & mask) != 0;
}
+3 -3
View File
@@ -1,7 +1,7 @@
name: ch934x_serial name: ch934x_serial
description: "A new Flutter project." description: "Flutter 插件,封装南京沁恒 CH934X 系列 USB 转串口芯片的 Android SDK,提供设备查找、串口读写、GPIO 与 Modem 控制等能力。"
version: 0.0.1 version: 1.0.0
homepage: homepage: https://example.com/ch934x_serial
environment: environment:
sdk: ^3.11.5 sdk: ^3.11.5
+83 -9
View File
@@ -1,26 +1,100 @@
import 'package:ch934x_serial/ch934x_serial_method_channel.dart';
import 'package:ch934x_serial/src/models/models.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:ch934x_serial/ch934x_serial_method_channel.dart';
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
MethodChannelCh934xSerial platform = MethodChannelCh934xSerial(); const channel = MethodChannel('ch934x_serial');
const MethodChannel channel = MethodChannel('ch934x_serial'); late MethodChannelCh934xSerial platform;
setUp(() { setUp(() {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger platform = MethodChannelCh934xSerial();
.setMockMethodCallHandler(channel, (MethodCall methodCall) async {
return '42';
});
}); });
tearDown(() { tearDown(() {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(channel, null); .setMockMethodCallHandler(channel, null);
platform.dispose();
}); });
test('getPlatformVersion', () async { /// 显式替换 mock handler 并返回,后续 `await platform.xxx` 会触发该 handler。
expect(await platform.getPlatformVersion(), '42'); void mockCall(Future<Object?> Function(MethodCall) handler) {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(channel, (MethodCall call) async {
return handler(call);
});
}
test('getDeviceList 解析为 Ch934xDeviceInfo 列表', () async {
mockCall((call) async {
expect(call.method, 'getDeviceList');
return <Map<String, Object>>[
<String, Object>{
'deviceId': 1,
'vendorId': 0x1a86,
'productId': 0xfe0c,
'deviceType': Ch934xDeviceType.ch9344,
'serialNumber': 'ABC',
'interfaceCount': 1,
'serialPorts': <Map<String, Object>>[
<String, Object>{'portIndex': 0},
],
},
];
});
final result = await platform.getDeviceList();
expect(result, hasLength(1));
expect(result.first.serialNumber, 'ABC');
expect(result.first.serialPorts.single.portIndex, 0);
});
test('read 透传 length 字段并返回字节', () async {
mockCall((call) async {
expect(call.method, 'read');
expect(call.arguments, <String, Object>{'length': 4});
return Uint8List.fromList([1, 2, 3, 4]);
});
final result = await platform.read(4);
expect(result, Uint8List.fromList([1, 2, 3, 4]));
});
test('read 接收非正长度直接返回空数组', () async {
// 未注册 mock handler,验证短路逻辑不调用原生层。
final result = await platform.read(0);
expect(result, isEmpty);
});
test('write 将数据写入原生层并回传字节数', () async {
mockCall((call) async {
expect(call.method, 'write');
expect((call.arguments as Map)['data'], isA<Uint8List>());
return 3;
});
final written = await platform.write(Uint8List.fromList([9, 8, 7]));
expect(written, 3);
});
test('getModemStatus 默认回退到 0', () async {
mockCall((_) async => null);
expect(await platform.getModemStatus(), 0);
});
test('dispatchException 推送给 setExceptionCallback 订阅者', () async {
mockCall((call) async {
expect(call.method, 'setExceptionCallback');
return null;
});
final received = <Ch934xException>[];
await platform.setExceptionCallback(received.add);
platform.dispatchException(
type: Ch934xExceptionType.deviceDetached,
message: 'detached',
);
// 等待 microtask 队列执行。
await Future<void>.delayed(Duration.zero);
expect(received, hasLength(1));
expect(received.first.message, 'detached');
}); });
} }
+141 -14
View File
@@ -1,28 +1,155 @@
import 'package:flutter_test/flutter_test.dart'; import 'dart:typed_data';
import 'package:ch934x_serial/ch934x_serial.dart'; import 'package:ch934x_serial/ch934x_serial.dart';
import 'package:ch934x_serial/ch934x_serial_platform_interface.dart';
import 'package:ch934x_serial/ch934x_serial_method_channel.dart'; import 'package:ch934x_serial/ch934x_serial_method_channel.dart';
import 'package:ch934x_serial/ch934x_serial_platform_interface.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:plugin_platform_interface/plugin_platform_interface.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart';
class MockCh934xSerialPlatform /// 用 mock 替代真实平台,以便在宿主测试中验证上层调用。
with MockPlatformInterfaceMixin class _MockCh934xSerialPlatform extends Ch934xSerialPlatform
implements Ch934xSerialPlatform { with MockPlatformInterfaceMixin {
_MockCh934xSerialPlatform({
// ignore: unused_element_parameter
this.deviceList = const <Ch934xDeviceInfo>[],
// ignore: unused_element_parameter
this.serialNumber,
// ignore: unused_element_parameter
this.deviceType = Ch934xDeviceType.ch9344,
// ignore: unused_element_parameter
this.ports = const <Ch934xSerialPortInfo>[],
// ignore: unused_element_parameter
this.openResult = true,
// ignore: unused_element_parameter
this.closeResult = true,
// ignore: unused_element_parameter
this.readBytes,
// ignore: unused_element_parameter
this.writeResult = 0,
// ignore: unused_element_parameter
this.gpioOutputResult = true,
// ignore: unused_element_parameter
this.gpioInput = 1,
// ignore: unused_element_parameter
this.modemControlResult = true,
// ignore: unused_element_parameter
this.modemStatus = ModemStatus.cts,
});
List<Ch934xDeviceInfo> deviceList;
String? serialNumber;
int deviceType;
List<Ch934xSerialPortInfo> ports;
bool openResult;
bool closeResult;
Uint8List? readBytes;
int writeResult;
bool gpioOutputResult;
int gpioInput;
bool modemControlResult;
int modemStatus;
@override @override
Future<String?> getPlatformVersion() => Future.value('42'); Future<List<Ch934xDeviceInfo>> getDeviceList() async => deviceList;
@override
Future<String?> getSerialNumber(int deviceId) async => serialNumber;
@override
Future<int> getDeviceType(int deviceId) async => deviceType;
@override
Future<List<Ch934xSerialPortInfo>> getSerialPortList(
int deviceId, {
required int interfaceNumber,
}) async =>
ports;
@override
Future<bool> openPort(Ch934xPortTarget target) async => openResult;
@override
Future<bool> closePort() async => closeResult;
@override
Future<Uint8List> read(int length) async => readBytes ?? Uint8List(0);
@override
Future<int> write(Uint8List data) async => writeResult;
@override
Future<bool> setGpioOutput({
required int gpioNumber,
required int level,
}) async =>
gpioOutputResult;
@override
Future<int> getGpioInput(int gpioNumber) async => gpioInput;
@override
Future<bool> setModemControl({required int dtr, required int rts}) async =>
modemControlResult;
@override
Future<int> getModemStatus() async => modemStatus;
@override
Future<void> setExceptionCallback(
void Function(Ch934xException exception) onException,
) async {}
} }
void main() { void main() {
final Ch934xSerialPlatform initialPlatform = Ch934xSerialPlatform.instance; TestWidgetsFlutterBinding.ensureInitialized();
test('$MethodChannelCh934xSerial is the default instance', () { test('默认平台实现是 MethodChannelCh934xSerial', () {
expect(initialPlatform, isInstanceOf<MethodChannelCh934xSerial>()); expect(Ch934xSerialPlatform.instance, isInstanceOf<MethodChannelCh934xSerial>());
}); });
test('getPlatformVersion', () async { test('Ch934xSerial 委托 platform 调用 getDeviceList', () async {
Ch934xSerial ch934xSerialPlugin = Ch934xSerial(); final mock = _MockCh934xSerialPlatform(
MockCh934xSerialPlatform fakePlatform = MockCh934xSerialPlatform(); deviceList: const [
Ch934xSerialPlatform.instance = fakePlatform; Ch934xDeviceInfo(
deviceId: 1,
vendorId: 0x1a86,
productId: 0xfe0c,
deviceType: Ch934xDeviceType.ch9344,
serialNumber: 'SN-1',
),
],
);
final plugin = Ch934xSerial.withPlatform(mock);
final list = await plugin.getDeviceList();
expect(list.single.serialNumber, 'SN-1');
});
expect(await ch934xSerialPlugin.getPlatformVersion(), '42'); test('ModemStatus.isSet 按位与工作', () {
const status = ModemStatus.cts | ModemStatus.dcd;
expect(ModemStatus.isSet(status, ModemStatus.cts), isTrue);
expect(ModemStatus.isSet(status, ModemStatus.dsr), isFalse);
});
test('Ch934xPortTarget.toMap 字段可被 MethodChannel 直接消费', () {
const target = Ch934xPortTarget(
deviceId: 2,
interfaceNumber: 1,
serialPortIndex: 3,
);
expect(target.toMap(), <String, Object>{
'deviceId': 2,
'interfaceNumber': 1,
'serialPortIndex': 3,
});
});
test('Ch934xException.toString 汇总关键字段', () {
final ex = Ch934xException(
type: Ch934xExceptionType.deviceDetached,
message: '设备被拔出',
);
expect(ex.toString(), contains('deviceDetached'));
expect(ex.toString(), contains('设备被拔出'));
}); });
} }