From 76377e52f2c61be0fb32252d4cc857878f649ca7 Mon Sep 17 00:00:00 2001 From: Developer <91611@user.local> Date: Mon, 6 Jul 2026 16:58:51 +0800 Subject: [PATCH] =?UTF-8?q?feat(ch934x=5Fserial):=20=E6=B7=BB=E5=8A=A0CH93?= =?UTF-8?q?4X=E4=B8=B2=E5=8F=A3=E8=AE=BE=E5=A4=87=E6=9D=83=E9=99=90?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E5=92=8C=E7=AB=AF=E5=8F=A3=E5=88=87=E6=8D=A2?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 setActivePort 方法用于在同一USB设备内部切换活跃串口 - 新增 requestUsbPermission 方法用于主动申请指定设备的USB权限 - 实现Android侧USB权限申请广播接收器和同步等待机制 - 在示例应用中集成权限预申请和端口切换UI逻辑 - 优化设备打开后自动拉取真实串口列表的功能 - 更新端口选择下拉框在打开状态下支持实时切换串口 --- .../ch934x_serial/Ch934xSerialPlugin.java | 194 +++++++++++++++++- example/lib/main.dart | 51 ++++- lib/ch934x_serial.dart | 15 ++ lib/ch934x_serial_method_channel.dart | 9 + lib/ch934x_serial_platform_interface.dart | 14 ++ test/ch934x_serial_test.dart | 6 + 6 files changed, 276 insertions(+), 13 deletions(-) diff --git a/android/src/main/java/com/xiarui/ch934x_serial/Ch934xSerialPlugin.java b/android/src/main/java/com/xiarui/ch934x_serial/Ch934xSerialPlugin.java index d1a7d91..07f3247 100644 --- a/android/src/main/java/com/xiarui/ch934x_serial/Ch934xSerialPlugin.java +++ b/android/src/main/java/com/xiarui/ch934x_serial/Ch934xSerialPlugin.java @@ -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 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。 + * + *

若 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,9 +350,24 @@ public class Ch934xSerialPlugin implements FlutterPlugin, MethodCallHandler { if (device == null) { return false; } - boolean opened = CH934XManager.getInstance().openDevice(device); - if (!opened) { - return false; + 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; @@ -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 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 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 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) // ------------------------------------------------------------------------ diff --git a/example/lib/main.dart b/example/lib/main.dart index 990cddb..f101456 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -86,6 +86,9 @@ class _Ch934xSerialExamplePageState extends State { } /// 触发设备列表刷新(对应 4.1.4)。 + /// + /// 刷新时主动给每个设备申请一次 USB 权限,避免用户点 + /// "打开"时再被弹框打断。已授权的设备 SDK 会立即返回 true。 Future _refreshDeviceList() async { try { final devices = await _plugin.getDeviceList(); @@ -109,6 +112,10 @@ class _Ch934xSerialExamplePageState extends State { } _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 { /// 把设备的串口描述转换为可用索引列表。 /// - /// 优先使用 SDK 返回的 [Ch934xSerialPortInfo.portIndex],缺 - /// 失时回退到 0..N-1,确保 UI 始终有可选项。 + /// 若 SDK 还未在原生侧拿到真实串口数(需要 openPort 之后才能问出来), + /// 退回到 [Ch934xDeviceInfo.interfaceCount] 或最小占位 [0]。 List _resolvePortIndices(Ch934xDeviceInfo device) { if (device.serialPorts.isNotEmpty) { return device.serialPorts.map((p) => p.portIndex).toList()..sort(); @@ -126,10 +133,30 @@ class _Ch934xSerialExamplePageState extends State { if (device.interfaceCount > 0) { return List.generate(device.interfaceCount, (i) => i); } - // 最坏情况下,允许打开串口 0;用户可通过业务调用切换。 return const [0]; } + /// 打开设备后,主动从原生侧拉取真实串口列表。 + Future _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 [0]; + } + if (_selectedPortIndex == null || + !_availablePortIndices.contains(_selectedPortIndex)) { + _selectedPortIndex = _availablePortIndices.first; + } + }); + } + Future _openPort() async { final device = _selectedDevice; final portIndex = _selectedPortIndex; @@ -149,9 +176,14 @@ class _Ch934xSerialExamplePageState extends State { } setState(() { _portOpened = true; - _status = '串口 $portIndex 已打开'; + _status = '串口 $portIndex 已打开,正在拉取真实串口列表'; }); _startReceiving(); + // openDevice 之后 SDK 才能正确返回串口数,刷新下拉框。 + await _refreshPortListAfterOpen(device); + if (mounted && _portOpened) { + setState(() => _status = '已打开串口 $portIndex,可用 ${_availablePortIndices.length} 路'); + } } Future _closePort() async { @@ -275,7 +307,16 @@ class _Ch934xSerialExamplePageState extends State { ), ) .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), diff --git a/lib/ch934x_serial.dart b/lib/ch934x_serial.dart index 8666dc0..fe2d6c2 100644 --- a/lib/ch934x_serial.dart +++ b/lib/ch934x_serial.dart @@ -60,6 +60,21 @@ class Ch934xSerial { /// 关闭当前会话最近一次打开的串口。 Future closePort() => _platform.closePort(); + /// 切换当前活跃串口(同一 UsbDevice 内部的不同串口)。 + /// + /// CH934X 设备一次 `openPort` 之后所有串口已连接,后续 + /// 读写只需切换 `serialPortIndex`,无需重新 open。 + Future setActivePort(int serialPortIndex) => + _platform.setActivePort(serialPortIndex); + + /// 主动申请指定设备的 USB 权限。 + /// + /// 正常情况下 `openPort` 内部会自动申请,本方法用于业务 + /// 方希望提前引导用户授权的场景,例如在主界面"刷新设备 + /// 列表"之后立刻弹一次授权请求。 + Future requestUsbPermission(int deviceId) => + _platform.requestUsbPermission(deviceId); + // --------------------------------------------------------------------------- // 串口读写 // --------------------------------------------------------------------------- diff --git a/lib/ch934x_serial_method_channel.dart b/lib/ch934x_serial_method_channel.dart index d9d5eee..73d54f8 100644 --- a/lib/ch934x_serial_method_channel.dart +++ b/lib/ch934x_serial_method_channel.dart @@ -114,6 +114,15 @@ class MethodChannelCh934xSerial extends Ch934xSerialPlatform { return result ?? false; } + @override + Future requestUsbPermission(int deviceId) async { + final result = await methodChannel.invokeMethod( + 'requestUsbPermission', + {'deviceId': deviceId}, + ); + return result ?? false; + } + @override Future read(int length) async { if (length <= 0) { diff --git a/lib/ch934x_serial_platform_interface.dart b/lib/ch934x_serial_platform_interface.dart index 85fa96e..1afccbf 100644 --- a/lib/ch934x_serial_platform_interface.dart +++ b/lib/ch934x_serial_platform_interface.dart @@ -56,6 +56,20 @@ abstract class Ch934xSerialPlatform extends PlatformInterface { /// 关闭当前线程/会话最近一次打开的串口(对应 5.2.1 `UsbSerial.close`)。 Future closePort(); + /// 切换当前活跃串口。 + /// + /// 同一 UsbDevice 在调用 [openPort] 之后,所有串口都已连 + /// 接;此方法仅切换后续读写/GPIO/Modem 操作的目标串口, + /// 不重复打开设备。返回是否切换成功。 + Future setActivePort(int serialPortIndex); + + /// 主动向系统申请指定设备的 USB 使用权限。 + /// + /// 在用户未授权时弹出系统对话框,等待用户选择后返回结果。 + /// [openPort] 内部已自动处理权限申请,业务方通常无需手动 + /// 调用;仅在希望提前引导用户授权时使用。 + Future requestUsbPermission(int deviceId); + // --------------------------------------------------------------------------- // 串口读写 // --------------------------------------------------------------------------- diff --git a/test/ch934x_serial_test.dart b/test/ch934x_serial_test.dart index 7f1b4ab..378855e 100644 --- a/test/ch934x_serial_test.dart +++ b/test/ch934x_serial_test.dart @@ -72,6 +72,12 @@ class _MockCh934xSerialPlatform extends Ch934xSerialPlatform @override Future closePort() async => closeResult; + @override + Future setActivePort(int serialPortIndex) async => true; + + @override + Future requestUsbPermission(int deviceId) async => true; + @override Future read(int length) async => readBytes ?? Uint8List(0);