feat(android): 添加 CH934X USB 转串口芯片支持
- 在 AndroidManifest.xml 中添加 USB Host 权限声明 - 集成 CH934X Android SDK 并通过反射调用原生功能 - 实现设备查找、串口读写、GPIO 和 Modem 控制功能 - 添加异常回调机制处理设备拔出等情况 - 提供 Stream 数据流支持实时串口数据监听 - 完善单元测试覆盖所有核心功能模块
This commit is contained in:
@@ -14,3 +14,4 @@ cache/
|
|||||||
|
|
||||||
# Hook markers
|
# Hook markers
|
||||||
.dirty
|
.dirty
|
||||||
|
daemon.pid
|
||||||
+14
-2
@@ -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`。
|
||||||
|
|||||||
@@ -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(暂未实现)
|
||||||
|
|||||||
@@ -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.
@@ -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
|
|
||||||
///
|
|
||||||
/// This local reference serves to register the plugin with the Flutter Engine and unregister it
|
|
||||||
/// when the Flutter Engine is detached from the Activity
|
|
||||||
private MethodChannel channel;
|
|
||||||
|
|
||||||
@Override
|
/** Dart ↔ Java 通信使用的 MethodChannel 名称,需与 Dart 侧保持一致。 */
|
||||||
public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
|
private static final String CHANNEL_NAME = "ch934x_serial";
|
||||||
channel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), "ch934x_serial");
|
|
||||||
channel.setMethodCallHandler(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
/** CH934X SDK 反射时使用的工具类与串口类名。 */
|
||||||
public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
|
private static final String USB_HELPER_CLASS = "com.example.ch934xserial.UsbHelper";
|
||||||
if (call.method.equals("getPlatformVersion")) {
|
private static final String USB_SERIAL_CLASS = "com.example.ch934xserial.UsbSerial";
|
||||||
result.success("Android " + android.os.Build.VERSION.RELEASE);
|
|
||||||
} else {
|
private MethodChannel channel;
|
||||||
result.notImplemented();
|
private Context applicationContext;
|
||||||
|
|
||||||
|
/** 当前会话打开的 UsbSerial 反射代理;仅保留最近一次的对象以与文档 5.2.1 保持一致。 */
|
||||||
|
@Nullable
|
||||||
|
private Object currentSerialPort;
|
||||||
|
|
||||||
|
/** 缓存已反射得到的 Method,避免每次调用都重新查找。 */
|
||||||
|
private final Map<String, Method> methodCache = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) {
|
||||||
|
channel = new MethodChannel(binding.getBinaryMessenger(), CHANNEL_NAME);
|
||||||
|
channel.setMethodCallHandler(this);
|
||||||
|
applicationContext = binding.getApplicationContext();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
|
public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
|
||||||
channel.setMethodCallHandler(null);
|
if (channel != 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
|
||||||
MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
|
public void unknownMethodReturnsNotImplemented() {
|
||||||
plugin.onMethodCall(call, mockResult);
|
Ch934xSerialPlugin plugin = new Ch934xSerialPlugin();
|
||||||
|
final MethodCall call = new MethodCall("__not_exists__", null);
|
||||||
verify(mockResult).success("Android " + android.os.Build.VERSION.RELEASE);
|
MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
|
||||||
}
|
plugin.onMethodCall(call, mockResult);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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';
|
||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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('设备被拔出'));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user