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

- 在 AndroidManifest.xml 中添加 USB Host 权限声明
- 集成 CH934X Android SDK 并通过反射调用原生功能
- 实现设备查找、串口读写、GPIO 和 Modem 控制功能
- 添加异常回调机制处理设备拔出等情况
- 提供 Stream 数据流支持实时串口数据监听
- 完善单元测试覆盖所有核心功能模块
This commit is contained in:
Developer
2026-07-06 16:06:54 +08:00
parent 6efaf5671f
commit e0d1a1775d
26 changed files with 2055 additions and 184 deletions
+2
View File
@@ -50,6 +50,8 @@ android {
}
dependencies {
// CH934X Android SDK,随插件发布。
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar"))))
testImplementation("junit:junit:4.13.2")
testImplementation("org.mockito:mockito-core:5.0.0")
}
Binary file not shown.
+6 -1
View File
@@ -1,3 +1,8 @@
<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>
@@ -1,6 +1,18 @@
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.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.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.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 {
/// 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
public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
channel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), "ch934x_serial");
channel.setMethodCallHandler(this);
}
/** Dart ↔ Java 通信使用的 MethodChannel 名称,需与 Dart 侧保持一致。 */
private static final String CHANNEL_NAME = "ch934x_serial";
@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
if (call.method.equals("getPlatformVersion")) {
result.success("Android " + android.os.Build.VERSION.RELEASE);
} else {
result.notImplemented();
/** CH934X SDK 反射时使用的工具类与串口类名。 */
private static final String USB_HELPER_CLASS = "com.example.ch934xserial.UsbHelper";
private static final String USB_SERIAL_CLASS = "com.example.ch934xserial.UsbSerial";
private MethodChannel channel;
private 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
public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
channel.setMethodCallHandler(null);
}
@Override
public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
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;
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.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
* line by running `./gradlew testDebugUnitTest` in the `example/android/` directory, or
* you can run them directly from IDEs that support JUnit such as Android Studio.
* <p>当前插件通过反射调用 CH934X SDK,在标准 JVM 单元测试环境
* 下没有真实 USB 设备,因此我们仅校验"未实现"分支,以保证
* 编译期 API 表面稳定。集成测试需要在真机上运行。
*/
public class Ch934xSerialPluginTest {
@Test
public void onMethodCall_getPlatformVersion_returnsExpectedValue() {
Ch934xSerialPlugin plugin = new Ch934xSerialPlugin();
final MethodCall call = new MethodCall("getPlatformVersion", null);
MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
plugin.onMethodCall(call, mockResult);
verify(mockResult).success("Android " + android.os.Build.VERSION.RELEASE);
}
@Test
public void unknownMethodReturnsNotImplemented() {
Ch934xSerialPlugin plugin = new Ch934xSerialPlugin();
final MethodCall call = new MethodCall("__not_exists__", null);
MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
plugin.onMethodCall(call, mockResult);
}
}