feat(ch934x_serial): 添加CH934X串口设备权限管理和端口切换功能
- 新增 setActivePort 方法用于在同一USB设备内部切换活跃串口 - 新增 requestUsbPermission 方法用于主动申请指定设备的USB权限 - 实现Android侧USB权限申请广播接收器和同步等待机制 - 在示例应用中集成权限预申请和端口切换UI逻辑 - 优化设备打开后自动拉取真实串口列表的功能 - 更新端口选择下拉框在打开状态下支持实时切换串口
This commit is contained in:
@@ -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,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<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
@@ -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),
|
||||
|
||||
@@ -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);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 串口读写
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 串口读写
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user