feat(ch934x_serial): 添加CH934X串口设备权限管理和端口切换功能

- 新增 setActivePort 方法用于在同一USB设备内部切换活跃串口
- 新增 requestUsbPermission 方法用于主动申请指定设备的USB权限
- 实现Android侧USB权限申请广播接收器和同步等待机制
- 在示例应用中集成权限预申请和端口切换UI逻辑
- 优化设备打开后自动拉取真实串口列表的功能
- 更新端口选择下拉框在打开状态下支持实时切换串口
This commit is contained in:
Developer
2026-07-06 16:58:51 +08:00
parent 90de9121ff
commit 76377e52f2
6 changed files with 276 additions and 13 deletions
@@ -1,9 +1,15 @@
package com.xiarui.ch934x_serial;
import android.app.Activity;
import android.app.Application;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbManager;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -12,6 +18,8 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import cn.wch.ch934xlib.CH934XManager;
import cn.wch.ch934xlib.callback.IDataCallback;
@@ -25,6 +33,8 @@ import cn.wch.ch934xlib.exception.UartLibException;
import cn.wch.ch934xlib.gpio.GPIO_DIR;
import cn.wch.ch934xlib.gpio.GPIO_VALUE;
import io.flutter.embedding.engine.plugins.FlutterPlugin;
import io.flutter.embedding.engine.plugins.activity.ActivityAware;
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
@@ -39,14 +49,24 @@ import io.flutter.plugin.common.MethodChannel.Result;
* {@code UsbSerial} 是 SDK 的概念性命名,真实 API 在
* {@code cn.wch.ch934xlib.CH934XManager}。
*/
public class Ch934xSerialPlugin implements FlutterPlugin, MethodCallHandler {
public class Ch934xSerialPlugin implements FlutterPlugin, MethodCallHandler, ActivityAware {
/** Dart ↔ Java 通信使用的 MethodChannel 名称,需与 Dart 侧保持一致。 */
private static final String CHANNEL_NAME = "ch934x_serial";
/** USB 权限申请广播的 Action,需在 AndroidManifest 中注册对应 Receiver。 */
private static final String ACTION_USB_PERMISSION = "com.xiarui.ch934x_serial.USB_PERMISSION";
private MethodChannel channel;
private Context applicationContext;
@Nullable
private Activity activity;
@Nullable
private BroadcastReceiver permissionReceiver;
@Nullable
private CompletableFuture<Boolean> pendingPermissionRequest;
/** 当前 Dart 侧选中的 (deviceId, serialNumber) 对应的 UsbDevice 引用。 */
@Nullable
private UsbDevice activeDevice;
@@ -138,6 +158,30 @@ public class Ch934xSerialPlugin implements FlutterPlugin, MethodCallHandler {
exceptionCallbackEnabled = false;
}
@Override
public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) {
activity = binding.getActivity();
registerPermissionReceiver();
}
@Override
public void onDetachedFromActivityForConfigChanges() {
unregisterPermissionReceiver();
activity = null;
}
@Override
public void onReattachedToActivityForConfigChanges(@NonNull ActivityPluginBinding binding) {
activity = binding.getActivity();
registerPermissionReceiver();
}
@Override
public void onDetachedFromActivity() {
unregisterPermissionReceiver();
activity = null;
}
@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
try {
@@ -157,9 +201,15 @@ public class Ch934xSerialPlugin implements FlutterPlugin, MethodCallHandler {
case "openPort":
result.success(handleOpenPort(call));
break;
case "requestUsbPermission":
result.success(handleRequestUsbPermission(call));
break;
case "closePort":
result.success(handleClosePort());
break;
case "setActivePort":
result.success(handleSetActivePort(call));
break;
case "read":
result.success(handleRead(call));
break;
@@ -187,8 +237,6 @@ public class Ch934xSerialPlugin implements FlutterPlugin, MethodCallHandler {
}
} catch (ChipException e) {
result.error("CHIP_ERROR", e.getMessage(), e.getClass().getName());
} catch (NoPermissionException e) {
result.error("NO_PERMISSION", e.getMessage(), e.getClass().getName());
} catch (UartLibException e) {
result.error("UART_LIB_ERROR", e.getMessage(), e.getClass().getName());
} catch (Throwable t) {
@@ -286,9 +334,13 @@ public class Ch934xSerialPlugin implements FlutterPlugin, MethodCallHandler {
// 设备打开 / 关闭(对应文档 5.x)
// ------------------------------------------------------------------------
/** 文档 5.1.1。 */
/** 文档 5.1.1。
*
* <p>若 SDK 抛 {@link NoPermissionException},自动弹出系统
* 授权对话框,等待用户选择后再重试一次。
*/
private boolean handleOpenPort(@NonNull MethodCall call)
throws ChipException, NoPermissionException, UartLibException {
throws ChipException, UartLibException {
Integer deviceId = call.argument("deviceId");
Integer serialPortIndex = call.argument("serialPortIndex");
if (deviceId == null || serialPortIndex == null) {
@@ -298,10 +350,25 @@ public class Ch934xSerialPlugin implements FlutterPlugin, MethodCallHandler {
if (device == null) {
return false;
}
boolean opened = CH934XManager.getInstance().openDevice(device);
try {
boolean opened = openDeviceInternal(device);
if (!opened) {
return false;
}
} catch (NoPermissionException npe) {
// 没有权限,自动弹出系统授权框,等用户选择。
if (!requestUsbPermissionBlocking(device)) {
return false;
}
// 重新尝试打开。
try {
if (!openDeviceInternal(device)) {
return false;
}
} catch (NoPermissionException retryFail) {
return false;
}
}
activeDevice = device;
activeSerialNumber = serialPortIndex;
ensureDataCallback();
@@ -309,6 +376,104 @@ public class Ch934xSerialPlugin implements FlutterPlugin, MethodCallHandler {
return true;
}
/** 内部真正调 SDK 的打开,让 [handleOpenPort] 能在外层捕获权限异常。 */
private boolean openDeviceInternal(@NonNull UsbDevice device)
throws ChipException, NoPermissionException, UartLibException {
return CH934XManager.getInstance().openDevice(device);
}
/** 处理 Dart 主动发起的"申请 USB 权限"调用。 */
private boolean handleRequestUsbPermission(@NonNull MethodCall call) {
UsbDevice device = resolveDevice(call);
if (device == null) {
return false;
}
return requestUsbPermissionBlocking(device);
}
/** 同步等待用户对 USB 权限对话框的回应,返回是否授权。 */
private boolean requestUsbPermissionBlocking(@NonNull UsbDevice device) {
if (activity == null) {
return false;
}
UsbManager usbManager = (UsbManager) applicationContext
.getSystemService(Context.USB_SERVICE);
if (usbManager == null) {
return false;
}
if (usbManager.hasPermission(device)) {
return true;
}
ensurePermissionReceiver();
CompletableFuture<Boolean> future = new CompletableFuture<>();
pendingPermissionRequest = future;
int flags = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
: PendingIntent.FLAG_UPDATE_CURRENT;
PendingIntent intent = PendingIntent.getBroadcast(
activity, 0, new Intent(ACTION_USB_PERMISSION), flags);
usbManager.requestPermission(device, intent);
try {
Boolean result = future.get(60, TimeUnit.SECONDS);
return result != null && result;
} catch (Throwable t) {
return false;
} finally {
pendingPermissionRequest = null;
}
}
/** 注册用于接收 USB 授权结果的 BroadcastReceiver(仅注册一次)。 */
private void ensurePermissionReceiver() {
if (permissionReceiver != null || activity == null) {
return;
}
permissionReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (!ACTION_USB_PERMISSION.equals(intent.getAction())) {
return;
}
boolean granted = intent.getBooleanExtra(
UsbManager.EXTRA_PERMISSION_GRANTED, false);
CompletableFuture<Boolean> future = pendingPermissionRequest;
if (future != null) {
future.complete(granted);
}
}
};
IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
// 13+ 强制要求显式 flag;USB 授权响应只来自系统,使用 NOT_EXPORTED。
activity.registerReceiver(permissionReceiver, filter,
Context.RECEIVER_NOT_EXPORTED);
} else {
activity.registerReceiver(permissionReceiver, filter);
}
}
private void registerPermissionReceiver() {
// Receiver 在真正需要时(申请权限前)才注册,避免在 manifest 中额外声明。
}
private void unregisterPermissionReceiver() {
if (permissionReceiver != null && activity != null) {
try {
activity.unregisterReceiver(permissionReceiver);
} catch (Throwable ignored) {
// Receiver 未注册时 ignore。
}
}
permissionReceiver = null;
CompletableFuture<Boolean> future = pendingPermissionRequest;
if (future != null) {
future.complete(false);
pendingPermissionRequest = null;
}
}
/** 文档 5.2.1。 */
private boolean handleClosePort() {
if (activeDevice == null) {
@@ -327,6 +492,19 @@ public class Ch934xSerialPlugin implements FlutterPlugin, MethodCallHandler {
return true;
}
/** 切换当前活跃串口索引(无需重新 open 设备)。 */
private boolean handleSetActivePort(@NonNull MethodCall call) {
if (activeDevice == null) {
return false;
}
Integer idx = call.argument("serialPortIndex");
if (idx == null || idx < 0) {
return false;
}
activeSerialNumber = idx;
return true;
}
// ------------------------------------------------------------------------
// 串口读写(对应文档 6.x)
// ------------------------------------------------------------------------
+46 -5
View File
@@ -86,6 +86,9 @@ class _Ch934xSerialExamplePageState extends State<Ch934xSerialExamplePage> {
}
/// 触发设备列表刷新(对应 4.1.4)。
///
/// 刷新时主动给每个设备申请一次 USB 权限,避免用户点
/// "打开"时再被弹框打断。已授权的设备 SDK 会立即返回 true。
Future<void> _refreshDeviceList() async {
try {
final devices = await _plugin.getDeviceList();
@@ -109,6 +112,10 @@ class _Ch934xSerialExamplePageState extends State<Ch934xSerialExamplePage> {
}
_status = '已扫描到 ${devices.length} 台 CH934X 设备';
});
// 后台并发请求权限,失败也不阻塞 UI。
for (final device in devices) {
unawaited(_plugin.requestUsbPermission(device.deviceId));
}
} on Exception catch (e) {
if (!mounted) return;
setState(() => _status = '设备扫描失败: $e');
@@ -117,8 +124,8 @@ class _Ch934xSerialExamplePageState extends State<Ch934xSerialExamplePage> {
/// 把设备的串口描述转换为可用索引列表。
///
/// 优先使用 SDK 返回的 [Ch934xSerialPortInfo.portIndex],缺
/// 失时回退到 0..N-1,确保 UI 始终有可选项
/// SDK 还未在原生侧拿到真实串口数(需要 openPort 之后才能问出来),
/// 退回到 [Ch934xDeviceInfo.interfaceCount] 或最小占位 [0]
List<int> _resolvePortIndices(Ch934xDeviceInfo device) {
if (device.serialPorts.isNotEmpty) {
return device.serialPorts.map((p) => p.portIndex).toList()..sort();
@@ -126,10 +133,30 @@ class _Ch934xSerialExamplePageState extends State<Ch934xSerialExamplePage> {
if (device.interfaceCount > 0) {
return List<int>.generate(device.interfaceCount, (i) => i);
}
// 最坏情况下,允许打开串口 0;用户可通过业务调用切换。
return const <int>[0];
}
/// 打开设备后,主动从原生侧拉取真实串口列表。
Future<void> _refreshPortListAfterOpen(Ch934xDeviceInfo device) async {
final ports = await _plugin.getSerialPortList(
device.deviceId,
interfaceNumber: 0,
);
if (!mounted) return;
setState(() {
// 用 SDK 返回的端口索引覆盖,确保与真实设备保持一致。
_availablePortIndices =
ports.map((p) => p.portIndex).toList()..sort();
if (_availablePortIndices.isEmpty) {
_availablePortIndices = const <int>[0];
}
if (_selectedPortIndex == null ||
!_availablePortIndices.contains(_selectedPortIndex)) {
_selectedPortIndex = _availablePortIndices.first;
}
});
}
Future<void> _openPort() async {
final device = _selectedDevice;
final portIndex = _selectedPortIndex;
@@ -149,9 +176,14 @@ class _Ch934xSerialExamplePageState extends State<Ch934xSerialExamplePage> {
}
setState(() {
_portOpened = true;
_status = '串口 $portIndex 已打开';
_status = '串口 $portIndex 已打开,正在拉取真实串口列表';
});
_startReceiving();
// openDevice 之后 SDK 才能正确返回串口数,刷新下拉框。
await _refreshPortListAfterOpen(device);
if (mounted && _portOpened) {
setState(() => _status = '已打开串口 $portIndex,可用 ${_availablePortIndices.length}');
}
}
Future<void> _closePort() async {
@@ -275,7 +307,16 @@ class _Ch934xSerialExamplePageState extends State<Ch934xSerialExamplePage> {
),
)
.toList(),
onChanged: (idx) => setState(() => _selectedPortIndex = idx),
onChanged: _portOpened
? (idx) async {
if (idx == null) return;
setState(() => _selectedPortIndex = idx);
await _plugin.setActivePort(idx);
if (mounted) {
setState(() => _status = '已切换到串口 $idx');
}
}
: null,
),
],
const SizedBox(height: 16),
+15
View File
@@ -60,6 +60,21 @@ class Ch934xSerial {
/// 关闭当前会话最近一次打开的串口。
Future<bool> closePort() => _platform.closePort();
/// 切换当前活跃串口(同一 UsbDevice 内部的不同串口)。
///
/// CH934X 设备一次 `openPort` 之后所有串口已连接,后续
/// 读写只需切换 `serialPortIndex`,无需重新 open。
Future<bool> setActivePort(int serialPortIndex) =>
_platform.setActivePort(serialPortIndex);
/// 主动申请指定设备的 USB 权限。
///
/// 正常情况下 `openPort` 内部会自动申请,本方法用于业务
/// 方希望提前引导用户授权的场景,例如在主界面"刷新设备
/// 列表"之后立刻弹一次授权请求。
Future<bool> requestUsbPermission(int deviceId) =>
_platform.requestUsbPermission(deviceId);
// ---------------------------------------------------------------------------
// 串口读写
// ---------------------------------------------------------------------------
+9
View File
@@ -114,6 +114,15 @@ class MethodChannelCh934xSerial extends Ch934xSerialPlatform {
return result ?? false;
}
@override
Future<bool> requestUsbPermission(int deviceId) async {
final result = await methodChannel.invokeMethod<bool>(
'requestUsbPermission',
<String, Object>{'deviceId': deviceId},
);
return result ?? false;
}
@override
Future<Uint8List> read(int length) async {
if (length <= 0) {
+14
View File
@@ -56,6 +56,20 @@ abstract class Ch934xSerialPlatform extends PlatformInterface {
/// 关闭当前线程/会话最近一次打开的串口(对应 5.2.1 `UsbSerial.close`)。
Future<bool> closePort();
/// 切换当前活跃串口。
///
/// 同一 UsbDevice 在调用 [openPort] 之后,所有串口都已连
/// 接;此方法仅切换后续读写/GPIO/Modem 操作的目标串口,
/// 不重复打开设备。返回是否切换成功。
Future<bool> setActivePort(int serialPortIndex);
/// 主动向系统申请指定设备的 USB 使用权限。
///
/// 在用户未授权时弹出系统对话框,等待用户选择后返回结果。
/// [openPort] 内部已自动处理权限申请,业务方通常无需手动
/// 调用;仅在希望提前引导用户授权时使用。
Future<bool> requestUsbPermission(int deviceId);
// ---------------------------------------------------------------------------
// 串口读写
// ---------------------------------------------------------------------------
+6
View File
@@ -72,6 +72,12 @@ class _MockCh934xSerialPlatform extends Ch934xSerialPlatform
@override
Future<bool> closePort() async => closeResult;
@override
Future<bool> setActivePort(int serialPortIndex) async => true;
@override
Future<bool> requestUsbPermission(int deviceId) async => true;
@override
Future<Uint8List> read(int length) async => readBytes ?? Uint8List(0);