diff --git a/ios/Podfile.lock b/ios/Podfile.lock index c9050a312..77f53258d 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -77,9 +77,9 @@ PODS: - QCloudTrack/Beacon (6.4.7) - record_darwin (1.0.0): - Flutter - - SDWebImage (5.21.0): - - SDWebImage/Core (= 5.21.0) - - SDWebImage/Core (5.21.0) + - SDWebImage (5.21.1): + - SDWebImage/Core (= 5.21.1) + - SDWebImage/Core (5.21.1) - SDWebImageWebPCoder (0.14.6): - libwebp (~> 1.0) - SDWebImage/Core (~> 5.17) @@ -200,7 +200,7 @@ SPEC CHECKSUMS: QCloudCOSXML: 7205e76aa9cf613468222615483c9d7c074e4298 QCloudTrack: 3b53a7fc4fe3920e407f2aa73f2452992a61f7f3 record_darwin: fb1f375f1d9603714f55b8708a903bbb91ffdb0a - SDWebImage: f84b0feeb08d2d11e6a9b843cb06d75ebf5b8868 + SDWebImage: f29024626962457f3470184232766516dee8dfea SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380 spotify_sdk: a48400bb29f70c4fe251ebfdc9135c37097ac5ca tencentcloud_cos_sdk_plugin: 7bed564dbe72df23e7b5cacb1e51b1a20cb1639c diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 02daa62e1..34f3271ab 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -549,6 +549,7 @@ BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 537SFY28XS; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 18.0; MARKETING_VERSION = 1.0; diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index b2cdd80bb..6dd9a6874 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -72,66 +72,10 @@ LSRequiresIPhoneOS - NSAccessorySetupBluetoothCompanyIdentifiers - - EFAB - - NSAccessorySetupDescriptions - - - AccessoryMediaType - Headphones - LocalizedAccessoryName - 语音助理耳机 - - - NSAppTransportSecurity - - NSAllowsArbitraryLoads - - NSAllowsArbitraryLoadsInWebContent - - NSExceptionDomains - - mcp.amap.com - - NSExceptionAllowsInsecureHTTPLoads - - NSExceptionMinimumTLSVersion - TLSv1.0 - NSIncludesSubdomains - - - mcp.ideapsound.com - - NSExceptionAllowsInsecureHTTPLoads - - NSExceptionMinimumTLSVersion - TLSv1.0 - NSIncludesSubdomains - - - - - NSBluetoothAlwaysUsageDescription 需要蓝牙权限用于连接和管理音频设备 NSBluetoothPeripheralUsageDescription 需要蓝牙权限用于连接和管理音频设备 - - - UIBackgroundModes - - bluetooth-central - audio - location - - - - NSAccessorySetupKitSupports - - Bluetooth - NSCalendarsUsageDescription 需要日历权限用于安排和管理您的音频会议和录音任务 NSCameraUsageDescription @@ -148,6 +92,12 @@ 需要相册权限用于保存录音文件和导入音频文件 UIApplicationSupportsIndirectInputEvents + UIBackgroundModes + + bluetooth-central + audio + location + UILaunchStoryboardName LaunchScreen UIMainStoryboardFile @@ -165,5 +115,7 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + UIViewControllerBasedStatusBarAppearance + diff --git a/lib/modules/ble/bindings/ble_binding.dart b/lib/modules/ble/bindings/ble_binding.dart new file mode 100644 index 000000000..4fc2cc5c7 --- /dev/null +++ b/lib/modules/ble/bindings/ble_binding.dart @@ -0,0 +1,9 @@ +import 'package:get/get.dart'; +import '../controllers/ble_controller.dart'; + +class BleBinding extends Bindings { + @override + void dependencies() { + Get.put(BleController()); + } +} \ No newline at end of file diff --git a/lib/modules/ble/controllers/ble_controller.dart b/lib/modules/ble/controllers/ble_controller.dart new file mode 100644 index 000000000..91ce3f8dd --- /dev/null +++ b/lib/modules/ble/controllers/ble_controller.dart @@ -0,0 +1,281 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:flutter_blue_plus/flutter_blue_plus.dart' as fbp; +import 'package:permission_handler/permission_handler.dart'; +import 'package:app_settings/app_settings.dart'; + +class BleController extends GetxController { + // 扫描状态 + final RxBool isScanning = false.obs; + final RxBool isBluetoothOn = false.obs; + final RxBool hasPermissions = false.obs; + + // 发现的设备列表 + final RxList scanResults = [].obs; + + // 连接的设备 + final Rx connectedDevice = + Rx(null); + + // 错误信息 + final RxString errorMessage = ''.obs; + + @override + void onInit() { + super.onInit(); + _initBluetooth(); + } + + @override + void onClose() { + stopScan(); + super.onClose(); + } + + // 初始化蓝牙 + Future _initBluetooth() async { + try { + // 先检查权限 + await _checkPermissions(); + + // 检查蓝牙适配器状态 + fbp.FlutterBluePlus.adapterState.listen((state) { + isBluetoothOn.value = state == fbp.BluetoothAdapterState.on; + if (state == fbp.BluetoothAdapterState.on) { + errorMessage.value = ''; + } else { + errorMessage.value = '请开启蓝牙'; + stopScan(); + } + }); + + // 监听扫描结果 + fbp.FlutterBluePlus.scanResults.listen((results) { + scanResults.value = results; + }); + + // 监听扫描状态 + fbp.FlutterBluePlus.isScanning.listen((scanning) { + isScanning.value = scanning; + }); + } catch (e) { + errorMessage.value = '蓝牙初始化失败: $e'; + } + } + + // 检查权限(不请求) + Future _checkPermissions() async { + try { + Map statuses = await [ + Permission.bluetoothScan, + Permission.bluetoothConnect, + Permission.location, + ].request(); + + hasPermissions.value = + statuses.values.every((status) => status == PermissionStatus.granted); + + if (!hasPermissions.value) { + errorMessage.value = '需要蓝牙和位置权限才能扫描设备'; + } + } catch (e) { + print('检查权限失败: $e'); + hasPermissions.value = false; + } + } + + // 请求权限 + Future _requestPermissions() async { + try { + Map statuses = await [ + Permission.bluetoothScan, + Permission.bluetoothConnect, + Permission.location, + ].request(); + + bool allGranted = + statuses.values.every((status) => status == PermissionStatus.granted); + hasPermissions.value = allGranted; + + if (!allGranted) { + // 检查是否有权限被永久拒绝 + bool hasPermanentlyDenied = statuses.values + .any((status) => status == PermissionStatus.permanentlyDenied); + + if (hasPermanentlyDenied) { + _showPermissionDialog(); + } else { + errorMessage.value = '需要蓝牙和位置权限才能扫描设备'; + } + } + + return allGranted; + } catch (e) { + print('请求权限失败: $e'); + errorMessage.value = '权限请求失败: $e'; + return false; + } + } + + // 显示权限对话框 + void _showPermissionDialog() { + Get.dialog( + AlertDialog( + title: Text('需要权限'), + content: Text('应用需要蓝牙和位置权限才能扫描设备。请在设置中手动开启权限。'), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: Text('取消'), + ), + TextButton( + onPressed: () { + Get.back(); + AppSettings.openAppSettings(); + }, + child: Text('去设置'), + ), + ], + ), + ); + } + + // 显示蓝牙开启对话框 + void _showBluetoothDialog() { + Get.dialog( + AlertDialog( + title: Text('需要开启蓝牙'), + content: Text('请开启蓝牙以扫描和连接设备。'), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: Text('取消'), + ), + TextButton( + onPressed: () { + Get.back(); + // 尝试开启蓝牙 + fbp.FlutterBluePlus.turnOn(); + }, + child: Text('开启蓝牙'), + ), + ], + ), + ); + } + + // 开始扫描 + Future startScan() async { + try { + errorMessage.value = ''; + + // 检查权限 + if (!hasPermissions.value) { + if (!await _requestPermissions()) { + return; + } + } + + // 检查蓝牙状态 + if (!isBluetoothOn.value) { + _showBluetoothDialog(); + return; + } + + // 清空之前的结果 + scanResults.clear(); + + // 开始扫描 + await fbp.FlutterBluePlus.startScan( + timeout: const Duration(seconds: 30), + androidUsesFineLocation: true, + ); + } catch (e) { + errorMessage.value = '扫描失败: $e'; + } + } + + // 停止扫描 + Future stopScan() async { + try { + await fbp.FlutterBluePlus.stopScan(); + } catch (e) { + print('停止扫描失败: $e'); + } + } + + // 连接设备 + Future connectToDevice(fbp.BluetoothDevice device) async { + try { + errorMessage.value = ''; + + // 停止扫描 + await stopScan(); + + // 连接设备 + await device.connect(timeout: const Duration(seconds: 15)); + connectedDevice.value = device; + + Get.snackbar( + '连接成功', + '已连接到 ${device.platformName.isNotEmpty ? device.platformName : '未知设备'}', + backgroundColor: Colors.green, + colorText: Colors.white, + ); + } catch (e) { + errorMessage.value = '连接失败: $e'; + Get.snackbar( + '连接失败', + e.toString(), + backgroundColor: Colors.red, + colorText: Colors.white, + ); + } + } + + // 断开连接 + Future disconnectDevice() async { + try { + if (connectedDevice.value != null) { + await connectedDevice.value!.disconnect(); + connectedDevice.value = null; + + Get.snackbar( + '已断开连接', + '设备已断开连接', + backgroundColor: Colors.orange, + colorText: Colors.white, + ); + } + } catch (e) { + errorMessage.value = '断开连接失败: $e'; + } + } + + // 获取设备名称 + String getDeviceName(fbp.ScanResult result) { + if (result.device.platformName.isNotEmpty) { + return result.device.platformName; + } + if (result.advertisementData.localName.isNotEmpty) { + return result.advertisementData.localName; + } + return '未知设备'; + } + + // 获取信号强度描述 + String getSignalStrengthText(int rssi) { + if (rssi >= -50) return '信号强'; + if (rssi >= -70) return '信号中等'; + if (rssi >= -90) return '信号弱'; + return '信号很弱'; + } + + // 获取信号强度图标 + IconData getSignalStrengthIcon(int rssi) { + if (rssi >= -50) return Icons.signal_wifi_4_bar; + if (rssi >= -70) return Icons.signal_wifi_4_bar; + if (rssi >= -90) return Icons.signal_wifi_4_bar; + return Icons.signal_wifi_4_bar; + } +} diff --git a/lib/modules/ble/views/ble_view.dart b/lib/modules/ble/views/ble_view.dart new file mode 100644 index 000000000..08398c95f --- /dev/null +++ b/lib/modules/ble/views/ble_view.dart @@ -0,0 +1,355 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_blue_plus/flutter_blue_plus.dart' as fbp; +import '../controllers/ble_controller.dart'; + +class BleView extends GetView { + const BleView({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final bool isDarkMode = Theme.of(context).brightness == Brightness.dark; + + return Scaffold( + backgroundColor: isDarkMode ? Colors.black : Colors.white, + appBar: AppBar( + title: Text( + 'BLE设备扫描', + style: TextStyle( + color: isDarkMode ? Colors.white : Colors.black, + fontSize: 18.sp, + fontWeight: FontWeight.bold, + ), + ), + backgroundColor: isDarkMode ? Colors.grey[900] : Colors.white, + elevation: 0, + iconTheme: IconThemeData( + color: isDarkMode ? Colors.white : Colors.black, + ), + actions: [ + Obx(() => IconButton( + onPressed: controller.isScanning.value + ? controller.stopScan + : controller.startScan, + icon: Icon( + controller.isScanning.value ? Icons.stop : Icons.refresh, + color: controller.isScanning.value + ? Colors.red + : (isDarkMode ? Colors.white : Colors.black), + ), + )), + ], + ), + body: SafeArea( + child: Column( + children: [ + // 状态栏 + _buildStatusBar(isDarkMode), + + // 连接的设备信息 + _buildConnectedDevice(isDarkMode), + + // 设备列表 + Expanded( + child: _buildDeviceList(isDarkMode), + ), + ], + ), + ), + floatingActionButton: Obx(() => FloatingActionButton( + onPressed: controller.isScanning.value + ? controller.stopScan + : controller.startScan, + backgroundColor: + controller.isScanning.value ? Colors.red : Colors.blue, + child: Icon( + controller.isScanning.value + ? Icons.stop + : Icons.bluetooth_searching, + color: Colors.white, + ), + )), + ); + } + + // 状态栏 + Widget _buildStatusBar(bool isDarkMode) { + return Obx(() => Container( + width: double.infinity, + padding: EdgeInsets.all(16.w), + margin: EdgeInsets.all(16.w), + decoration: BoxDecoration( + color: isDarkMode ? Colors.grey[800] : Colors.grey[100], + borderRadius: BorderRadius.circular(12.r), + ), + child: Column( + children: [ + Row( + children: [ + Icon( + controller.isBluetoothOn.value + ? Icons.bluetooth + : Icons.bluetooth_disabled, + color: controller.isBluetoothOn.value + ? Colors.blue + : Colors.red, + size: 24.w, + ), + SizedBox(width: 8.w), + Text( + controller.isBluetoothOn.value ? '蓝牙已开启' : '蓝牙已关闭', + style: TextStyle( + color: isDarkMode ? Colors.white : Colors.black, + fontSize: 16.sp, + fontWeight: FontWeight.w500, + ), + ), + Spacer(), + if (controller.isScanning.value) + SizedBox( + width: 20.w, + height: 20.w, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation(Colors.blue), + ), + ), + ], + ), + if (controller.errorMessage.value.isNotEmpty) ...[ + SizedBox(height: 8.h), + Text( + controller.errorMessage.value, + style: TextStyle( + color: Colors.red, + fontSize: 14.sp, + ), + ), + ], + if (controller.isScanning.value) ...[ + SizedBox(height: 8.h), + Text( + '正在扫描设备...', + style: TextStyle( + color: Colors.blue, + fontSize: 14.sp, + ), + ), + ], + ], + ), + )); + } + + // 已连接设备信息 + Widget _buildConnectedDevice(bool isDarkMode) { + return Obx(() { + if (controller.connectedDevice.value == null) { + return SizedBox.shrink(); + } + + final device = controller.connectedDevice.value!; + return Container( + width: double.infinity, + padding: EdgeInsets.all(16.w), + margin: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h), + decoration: BoxDecoration( + color: Colors.green.withOpacity(0.1), + border: Border.all(color: Colors.green, width: 1), + borderRadius: BorderRadius.circular(12.r), + ), + child: Row( + children: [ + Icon( + Icons.bluetooth_connected, + color: Colors.green, + size: 24.w, + ), + SizedBox(width: 12.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '已连接设备', + style: TextStyle( + color: Colors.green, + fontSize: 12.sp, + fontWeight: FontWeight.w500, + ), + ), + Text( + device.platformName.isNotEmpty + ? device.platformName + : '未知设备', + style: TextStyle( + color: isDarkMode ? Colors.white : Colors.black, + fontSize: 16.sp, + fontWeight: FontWeight.bold, + ), + ), + Text( + device.remoteId.toString(), + style: TextStyle( + color: isDarkMode ? Colors.white70 : Colors.black54, + fontSize: 12.sp, + ), + ), + ], + ), + ), + IconButton( + onPressed: controller.disconnectDevice, + icon: Icon( + Icons.close, + color: Colors.red, + size: 20.w, + ), + ), + ], + ), + ); + }); + } + + // 设备列表 + Widget _buildDeviceList(bool isDarkMode) { + return Obx(() { + if (controller.scanResults.isEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.bluetooth_searching, + size: 64.w, + color: isDarkMode ? Colors.white54 : Colors.black54, + ), + SizedBox(height: 16.h), + Text( + controller.isScanning.value ? '正在搜索设备...' : '点击扫描按钮开始搜索BLE设备', + style: TextStyle( + color: isDarkMode ? Colors.white54 : Colors.black54, + fontSize: 16.sp, + ), + textAlign: TextAlign.center, + ), + ], + ), + ); + } + + return ListView.builder( + padding: EdgeInsets.symmetric(horizontal: 16.w), + itemCount: controller.scanResults.length, + itemBuilder: (context, index) { + final result = controller.scanResults[index]; + return _buildDeviceItem(result, isDarkMode); + }, + ); + }); + } + + // 设备项 + Widget _buildDeviceItem(fbp.ScanResult result, bool isDarkMode) { + final deviceName = controller.getDeviceName(result); + final isConnected = + controller.connectedDevice.value?.remoteId == result.device.remoteId; + + return Container( + margin: EdgeInsets.only(bottom: 12.h), + decoration: BoxDecoration( + color: isDarkMode ? Colors.grey[800] : Colors.white, + borderRadius: BorderRadius.circular(12.r), + border: Border.all( + color: isConnected + ? Colors.green + : (isDarkMode ? Colors.grey[700]! : Colors.grey[300]!), + width: isConnected ? 2 : 1, + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 8, + offset: Offset(0, 2), + ), + ], + ), + child: ListTile( + contentPadding: EdgeInsets.all(16.w), + leading: Container( + width: 48.w, + height: 48.w, + decoration: BoxDecoration( + color: isConnected + ? Colors.green.withOpacity(0.1) + : Colors.blue.withOpacity(0.1), + borderRadius: BorderRadius.circular(24.r), + ), + child: Icon( + isConnected ? Icons.bluetooth_connected : Icons.bluetooth, + color: isConnected ? Colors.green : Colors.blue, + size: 24.w, + ), + ), + title: Text( + deviceName, + style: TextStyle( + color: isDarkMode ? Colors.white : Colors.black, + fontSize: 16.sp, + fontWeight: FontWeight.bold, + ), + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 4.h), + Text( + result.device.remoteId.toString(), + style: TextStyle( + color: isDarkMode ? Colors.white70 : Colors.black54, + fontSize: 12.sp, + ), + ), + SizedBox(height: 4.h), + Row( + children: [ + Icon( + controller.getSignalStrengthIcon(result.rssi), + size: 16.w, + color: isDarkMode ? Colors.white70 : Colors.black54, + ), + SizedBox(width: 4.w), + Text( + '${result.rssi} dBm (${controller.getSignalStrengthText(result.rssi)})', + style: TextStyle( + color: isDarkMode ? Colors.white70 : Colors.black54, + fontSize: 12.sp, + ), + ), + ], + ), + ], + ), + trailing: isConnected + ? Icon( + Icons.check_circle, + color: Colors.green, + size: 24.w, + ) + : IconButton( + onPressed: () => controller.connectToDevice(result.device), + icon: Icon( + Icons.connect_without_contact, + color: Colors.blue, + size: 24.w, + ), + ), + onTap: isConnected + ? null + : () => controller.connectToDevice(result.device), + ), + ); + } +} diff --git a/lib/modules/login/views/login_view.dart b/lib/modules/login/views/login_view.dart index 543ba1827..9df8ffb0f 100644 --- a/lib/modules/login/views/login_view.dart +++ b/lib/modules/login/views/login_view.dart @@ -39,13 +39,13 @@ class LoginView extends GetView { SizedBox(height: 32.h), _buildWelcomeText(isDarkMode), - // 输入框和继续按钮 - SizedBox(height: 24.h), - _buildInputField(isDarkMode), - SizedBox(height: 16.h), - _buildVerificationInputField(isDarkMode), - SizedBox(height: 16.h), - _buildContinueButton(isDarkMode), + // // 输入框和继续按钮 + // SizedBox(height: 24.h), + // _buildInputField(isDarkMode), + // SizedBox(height: 16.h), + // _buildVerificationInputField(isDarkMode), + // SizedBox(height: 16.h), + // _buildContinueButton(isDarkMode), // 社交媒体登录 SizedBox(height: 32.h), diff --git a/lib/modules/pairing/controllers/pairing_controller.dart b/lib/modules/pairing/controllers/pairing_controller.dart index ee27f518a..2c31301aa 100644 --- a/lib/modules/pairing/controllers/pairing_controller.dart +++ b/lib/modules/pairing/controllers/pairing_controller.dart @@ -1,6 +1,3 @@ -import 'dart:convert'; -import 'dart:typed_data'; - import 'package:app_settings/app_settings.dart'; import '/data/models/appconfig.dart'; import '/data/models/user_Info.dart'; @@ -14,7 +11,6 @@ import '../../../core/utils/logger.dart'; import 'dart:io'; import '../../../data/services/bluetooth_manager.dart'; import '../../../data/services/ble_manager.dart'; -import '../../../core/utils/permission_util.dart'; /// 配对状态枚举 - 简化版本 enum PairingState { @@ -418,46 +414,16 @@ class PairingController extends GetxController { /// 检查权限 Future _checkPermissions() async { - if (Platform.isAndroid) { - // Android 平台权限检查 - Map statuses = await [ - Permission.bluetoothScan, - Permission.bluetoothConnect, - Permission.locationWhenInUse, - ].request(); - - return statuses[Permission.bluetoothScan]!.isGranted && - statuses[Permission.bluetoothConnect]!.isGranted && - statuses[Permission.locationWhenInUse]!.isGranted; - } else if (Platform.isIOS) { - // iOS 平台权限检查 - Map statuses = await [ - Permission.locationWhenInUse, - Permission.bluetooth, // 添加蓝牙权限检查 - ].request(); - - // 检查位置权限 - bool locationGranted = statuses[Permission.locationWhenInUse]!.isGranted; - - // 检查蓝牙权限 - bool bluetoothGranted = statuses[Permission.bluetooth]!.isGranted; - - // 如果蓝牙权限未确定,强制触发权限申请 - if (!bluetoothGranted) { - try { - await _bleManager.requestBluetoothPermission(); - // 重新检查蓝牙权限状态 - final newBluetoothStatus = await Permission.bluetooth.status; - bluetoothGranted = newBluetoothStatus.isGranted; - } catch (e) { - Logger.e(_tag, '蓝牙权限申请失败: $e'); - } - } - - return locationGranted && bluetoothGranted; - } - - return true; + // Android 平台权限检查 + Map statuses = await [ + Permission.bluetoothScan, + Permission.bluetoothConnect, + Permission.locationWhenInUse, + ].request(); + + return statuses[Permission.bluetoothScan]!.isGranted && + statuses[Permission.bluetoothConnect]!.isGranted && + statuses[Permission.locationWhenInUse]!.isGranted; } /// 验证设备 diff --git a/lib/routes/app_pages.dart b/lib/routes/app_pages.dart index 88f7f07c6..48efd4fe9 100644 --- a/lib/routes/app_pages.dart +++ b/lib/routes/app_pages.dart @@ -1,3 +1,6 @@ +import 'package:deep_voice/modules/ble/bindings/ble_binding.dart'; +import 'package:deep_voice/modules/ble/views/ble_view.dart'; + import '../../modules/devices/bindings/devices_binding.dart'; import '../../modules/devices/views/devices_view.dart'; import 'package:get/get.dart'; @@ -249,5 +252,10 @@ abstract class AppPages { page: () => const DevicesView(), binding: DevicesBinding(), ), + GetPage( + name: Routes.ble, + page: () => const BleView(), + binding: BleBinding(), + ), ]; } diff --git a/lib/routes/app_routes.dart b/lib/routes/app_routes.dart index 7942bfd73..e955f025e 100644 --- a/lib/routes/app_routes.dart +++ b/lib/routes/app_routes.dart @@ -56,4 +56,7 @@ abstract class Routes { // 设备管理 static const devices = '/devices'; + + // ble设备管理 + static const ble = '/ble'; } diff --git a/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleAccessorySetup.swift b/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleAccessorySetup.swift index 3cf85910e..07f6803d0 100644 --- a/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleAccessorySetup.swift +++ b/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleAccessorySetup.swift @@ -1,306 +1,306 @@ -import Foundation -import CoreBluetooth -import os.log -import AVFoundation -import AccessorySetupKit -import UIKit +// import Foundation +// import CoreBluetooth +// import os.log +// import AVFoundation +// import AccessorySetupKit +// import UIKit -@available(iOS 18.0, *) -class BleAccessorySetup: NSObject { - // MARK: - 属性 - static let shared = BleAccessorySetup() +// @available(iOS 18.0, *) +// class BleAccessorySetup: NSObject { +// // MARK: - 属性 +// static let shared = BleAccessorySetup() - private var bleService: BleService? - private var session: ASAccessorySession? - private var isSessionActivated = false - private var isInitializing = false - private let logger = OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleAccessorySetup") +// private var bleService: BleService? +// private var session: ASAccessorySession? +// private var isSessionActivated = false +// private var isInitializing = false +// private let logger = OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleAccessorySetup") - // 杰理蓝牙设备的公司标识符 - private let deviceCompanyIdentifier: UInt16 = 0xEFAB - private let deviceDisplayName = "语音助理耳机" +// // 杰理蓝牙设备的公司标识符 +// private let deviceCompanyIdentifier: UInt16 = 0xEFAB +// private let deviceDisplayName = "语音助理耳机" - // 用于存储配对设备UUID的UserDefaults键 - private let pairedDeviceUUIDKey = "com.yunqiinnovation.ble_service.paired_device_uuid" +// // 用于存储配对设备UUID的UserDefaults键 +// private let pairedDeviceUUIDKey = "com.yunqiinnovation.ble_service.paired_device_uuid" - typealias DeviceDiscoveredCallback = (UUID) -> Void - private var onDeviceDiscovered: DeviceDiscoveredCallback? +// typealias DeviceDiscoveredCallback = (UUID) -> Void +// private var onDeviceDiscovered: DeviceDiscoveredCallback? - // MARK: - 初始化 - private override init() { super.init() } +// // MARK: - 初始化 +// private override init() { super.init() } - // MARK: - 公共方法 - func setBleService(_ service: Any) { - self.bleService = service as? BleService - } +// // MARK: - 公共方法 +// func setBleService(_ service: Any) { +// self.bleService = service as? BleService +// } - func setDeviceDiscoveredCallback(_ callback: @escaping (UUID) -> Void) { - self.onDeviceDiscovered = callback - } +// func setDeviceDiscoveredCallback(_ callback: @escaping (UUID) -> Void) { +// self.onDeviceDiscovered = callback +// } - // 处理发现的设备并连接 - func connectToAccessory(uuid: UUID) { - guard let service = bleService else { - return - } +// // 处理发现的设备并连接 +// func connectToAccessory(uuid: UUID) { +// guard let service = bleService else { +// return +// } - service.connectToAccessory(uuid: uuid) - } +// service.connectToAccessory(uuid: uuid) +// } - func addToReconnectQueue(uuid: UUID) { - guard let service = bleService else { - return - } +// func addToReconnectQueue(uuid: UUID) { +// guard let service = bleService else { +// return +// } - service.addToReconnectQueue(uuid: uuid) - } +// service.addToReconnectQueue(uuid: uuid) +// } - // 从视图控制器显示设备选择器 - 主要入口点 - func showDevicePickerFromViewController(_ viewController: UIViewController) { - initializeSession() - } +// // 从视图控制器显示设备选择器 - 主要入口点 +// func showDevicePickerFromViewController(_ viewController: UIViewController) { +// initializeSession() +// } - // 清除蓝牙状态(仅用于测试或重置,不会清除系统授权) - func resetBluetoothState() { - if let service = bleService { - service.disconnect() - } - } +// // 清除蓝牙状态(仅用于测试或重置,不会清除系统授权) +// func resetBluetoothState() { +// if let service = bleService { +// service.disconnect() +// } +// } - // MARK: - 会话管理 - func initializeSession() { - if isInitializing { - os_log("会话正在初始化中,跳过", log: logger, type: .info) - return - } +// // MARK: - 会话管理 +// func initializeSession() { +// if isInitializing { +// os_log("会话正在初始化中,跳过", log: logger, type: .info) +// return +// } - // 如果会话已激活,检查是否有配对设备 - if session != nil && isSessionActivated { - os_log("会话已激活,检查配对设备", log: logger, type: .info) - // 会话已激活,检查是否有配对设备 - if let accessories = session?.accessories, !accessories.isEmpty { - os_log("发现 %d 个已配对设备,尝试连接", log: logger, type: .info, accessories.count) - // 有配对设备,尝试连接 - if let service = bleService { - let _ = service.getCentralManager() +// // 如果会话已激活,检查是否有配对设备 +// if session != nil && isSessionActivated { +// os_log("会话已激活,检查配对设备", log: logger, type: .info) +// // 会话已激活,检查是否有配对设备 +// if let accessories = session?.accessories, !accessories.isEmpty { +// os_log("发现 %d 个已配对设备,尝试连接", log: logger, type: .info, accessories.count) +// // 有配对设备,尝试连接 +// if let service = bleService { +// let _ = service.getCentralManager() - for accessory in accessories { - if let uuid = accessory.bluetoothIdentifier { - onDeviceDiscovered?(uuid) - connectToAccessory(uuid: uuid) - } - } - } - } else { - os_log("没有已配对设备,显示配对框", log: logger, type: .info) - // 没有配对设备,直接显示配对框 - showDevicePicker() - } - return - } +// for accessory in accessories { +// if let uuid = accessory.bluetoothIdentifier { +// onDeviceDiscovered?(uuid) +// connectToAccessory(uuid: uuid) +// } +// } +// } +// } else { +// os_log("没有已配对设备,显示配对框", log: logger, type: .info) +// // 没有配对设备,直接显示配对框 +// showDevicePicker() +// } +// return +// } - os_log("开始初始化新会话", log: logger, type: .info) - isInitializing = true +// os_log("开始初始化新会话", log: logger, type: .info) +// isInitializing = true - do { - session = try ASAccessorySession() - session?.activate(on: .main) { [weak self] event in - guard let self = self else { return } - self.handleSessionEvent(event) - } - } catch { - os_log("会话初始化失败: %{public}@", log: logger, type: .error, error.localizedDescription) - handleSessionInitError(error) - } - } +// do { +// session = try ASAccessorySession() +// session?.activate(on: .main) { [weak self] event in +// guard let self = self else { return } +// self.handleSessionEvent(event) +// } +// } catch { +// os_log("会话初始化失败: %{public}@", log: logger, type: .error, error.localizedDescription) +// handleSessionInitError(error) +// } +// } - func resetSession() { - session?.invalidate() - session = nil - isSessionActivated = false - isInitializing = false - } +// func resetSession() { +// session?.invalidate() +// session = nil +// isSessionActivated = false +// isInitializing = false +// } - // MARK: - 设备选择器显示 - private func showDevicePicker() { - guard let session = session, isSessionActivated else { - os_log("无法显示配对框:会话未激活", log: logger, type: .error) - return - } +// // MARK: - 设备选择器显示 +// private func showDevicePicker() { +// guard let session = session, isSessionActivated else { +// os_log("无法显示配对框:会话未激活", log: logger, type: .error) +// return +// } - os_log("准备显示设备配对框", log: logger, type: .info) +// os_log("准备显示设备配对框", log: logger, type: .info) - let descriptor = ASDiscoveryDescriptor() - descriptor.bluetoothCompanyIdentifier = ASBluetoothCompanyIdentifier(deviceCompanyIdentifier) +// let descriptor = ASDiscoveryDescriptor() +// descriptor.bluetoothCompanyIdentifier = ASBluetoothCompanyIdentifier(deviceCompanyIdentifier) - let deviceImage = UIImage(systemName: "headphones") ?? UIImage() - let displayItem = ASPickerDisplayItem( - name: deviceDisplayName, - productImage: deviceImage, - descriptor: descriptor - ) +// let deviceImage = UIImage(systemName: "headphones") ?? UIImage() +// let displayItem = ASPickerDisplayItem( +// name: deviceDisplayName, +// productImage: deviceImage, +// descriptor: descriptor +// ) - DispatchQueue.main.async { - os_log("正在显示设备配对框", log: self.logger, type: .info) - session.showPicker(for: [displayItem]) { [weak self] error in - guard let self = self else { return } +// DispatchQueue.main.async { +// os_log("正在显示设备配对框", log: self.logger, type: .info) +// session.showPicker(for: [displayItem]) { [weak self] error in +// guard let self = self else { return } - if let error = error { - let nsError = error as NSError - os_log("设备选择器错误: %{public}@", log: self.logger, type: .error, error.localizedDescription) - } else { - os_log("设备选择器完成", log: self.logger, type: .info) - } - } - } - } +// if let error = error { +// let nsError = error as NSError +// os_log("设备选择器错误: %{public}@", log: self.logger, type: .error, error.localizedDescription) +// } else { +// os_log("设备选择器完成", log: self.logger, type: .info) +// } +// } +// } +// } - // MARK: - 私有辅助方法 - private func handleSessionEvent(_ event: ASAccessoryEvent) { - switch event.eventType { - case .activated: - os_log("会话已激活", log: logger, type: .info) - handleSessionActivated() +// // MARK: - 私有辅助方法 +// private func handleSessionEvent(_ event: ASAccessoryEvent) { +// switch event.eventType { +// case .activated: +// os_log("会话已激活", log: logger, type: .info) +// handleSessionActivated() - case .accessoryAdded: - os_log("发现新设备: %{public}@", log: logger, type: .info, event.accessory?.bluetoothIdentifier?.uuidString ?? "Unknown") - handleAccessoryAdded(event.accessory) +// case .accessoryAdded: +// os_log("发现新设备: %{public}@", log: logger, type: .info, event.accessory?.bluetoothIdentifier?.uuidString ?? "Unknown") +// handleAccessoryAdded(event.accessory) - case .accessoryRemoved: - os_log("设备已移除: %{public}@", log: logger, type: .info, event.accessory?.bluetoothIdentifier?.uuidString ?? "Unknown") - handleAccessoryRemoved(event.accessory) +// case .accessoryRemoved: +// os_log("设备已移除: %{public}@", log: logger, type: .info, event.accessory?.bluetoothIdentifier?.uuidString ?? "Unknown") +// handleAccessoryRemoved(event.accessory) - case .accessoryChanged: - os_log("设备状态已改变", log: logger, type: .info) - break +// case .accessoryChanged: +// os_log("设备状态已改变", log: logger, type: .info) +// break - case .pickerDidPresent: - os_log("配对框已显示", log: logger, type: .info) - break +// case .pickerDidPresent: +// os_log("配对框已显示", log: logger, type: .info) +// break - case .pickerDidDismiss: - os_log("配对框已关闭", log: logger, type: .info) - break +// case .pickerDidDismiss: +// os_log("配对框已关闭", log: logger, type: .info) +// break - case .pickerSetupFailed: - os_log("选择器设置失败: %{public}@", log: logger, type: .error, - event.error?.localizedDescription ?? "未知错误") +// case .pickerSetupFailed: +// os_log("选择器设置失败: %{public}@", log: logger, type: .error, +// event.error?.localizedDescription ?? "未知错误") - default: - os_log("未处理的会话事件: %d", log: logger, type: .info, event.eventType.rawValue) - break - } - } +// default: +// os_log("未处理的会话事件: %d", log: logger, type: .info, event.eventType.rawValue) +// break +// } +// } - private func handleSessionActivated() { - isSessionActivated = true - isInitializing = false +// private func handleSessionActivated() { +// isSessionActivated = true +// isInitializing = false - if let accessories = session?.accessories, !accessories.isEmpty { - if let service = bleService { - let _ = service.getCentralManager() +// if let accessories = session?.accessories, !accessories.isEmpty { +// if let service = bleService { +// let _ = service.getCentralManager() - for accessory in accessories { - if let uuid = accessory.bluetoothIdentifier { - onDeviceDiscovered?(uuid) - connectToAccessory(uuid: uuid) - } - } - } - } else { - showDevicePicker() - } - } +// for accessory in accessories { +// if let uuid = accessory.bluetoothIdentifier { +// onDeviceDiscovered?(uuid) +// connectToAccessory(uuid: uuid) +// } +// } +// } +// } else { +// showDevicePicker() +// } +// } - private func handleAccessoryAdded(_ accessory: ASAccessory?) { - guard let accessory = accessory, let uuid = accessory.bluetoothIdentifier else { - return - } +// private func handleAccessoryAdded(_ accessory: ASAccessory?) { +// guard let accessory = accessory, let uuid = accessory.bluetoothIdentifier else { +// return +// } - // 保存配对设备的UUID - savePairedMacAddress(uuid) +// // 保存配对设备的UUID +// savePairedMacAddress(uuid) - onDeviceDiscovered?(uuid) - connectToAccessory(uuid: uuid) - } +// onDeviceDiscovered?(uuid) +// connectToAccessory(uuid: uuid) +// } - private func handleAccessoryRemoved(_ accessory: ASAccessory?) { - guard let accessory = accessory, let uuid = accessory.bluetoothIdentifier else { - return - } +// private func handleAccessoryRemoved(_ accessory: ASAccessory?) { +// guard let accessory = accessory, let uuid = accessory.bluetoothIdentifier else { +// return +// } - if let service = bleService { - service.disconnect() - } - } +// if let service = bleService { +// service.disconnect() +// } +// } - private func handleSessionInitError(_ error: Error) { - isInitializing = false - session = nil - } +// private func handleSessionInitError(_ error: Error) { +// isInitializing = false +// session = nil +// } - // MARK: - 蓝牙状态检查 - func isBluetoothAvailable() -> Bool { - do { - let audioSession = AVAudioSession.sharedInstance() - let outputs = audioSession.currentRoute.outputs +// // MARK: - 蓝牙状态检查 +// func isBluetoothAvailable() -> Bool { +// do { +// let audioSession = AVAudioSession.sharedInstance() +// let outputs = audioSession.currentRoute.outputs - for output in outputs { - if output.portType == .bluetoothA2DP || - output.portType == .bluetoothHFP || - output.portType == .bluetoothLE { - return true - } - } - } catch { - os_log("检查蓝牙状态出错: %{public}@", log: logger, type: .error, error.localizedDescription) - } +// for output in outputs { +// if output.portType == .bluetoothA2DP || +// output.portType == .bluetoothHFP || +// output.portType == .bluetoothLE { +// return true +// } +// } +// } catch { +// os_log("检查蓝牙状态出错: %{public}@", log: logger, type: .error, error.localizedDescription) +// } - return true - } +// return true +// } - // MARK: - 清除配对关系 - func clearAssociations() { - resetBluetoothState() +// // MARK: - 清除配对关系 +// func clearAssociations() { +// resetBluetoothState() - // 清除保存的配对设备UUID - clearPairedMacAddress() +// // 清除保存的配对设备UUID +// clearPairedMacAddress() - #if canImport(AccessorySetupKit) && swift(>=5.9) - if #available(iOS 18.0, *) { - resetSession() - initializeSession() - } - #endif - } +// #if canImport(AccessorySetupKit) && swift(>=5.9) +// if #available(iOS 18.0, *) { +// resetSession() +// initializeSession() +// } +// #endif +// } - // MARK: - 配对设备UUID管理 +// // MARK: - 配对设备UUID管理 - /// 获取已配对设备的UUID字符串 - /// 返回存储的配对设备UUID,如果没有则返回nil - func getPairedMacAddress() -> String? { - let uuidString = UserDefaults.standard.string(forKey: pairedDeviceUUIDKey) - if let uuidString = uuidString, !uuidString.isEmpty { - os_log("获取已配对设备UUID: %{public}@", log: logger, type: .info, uuidString) - return uuidString - } - os_log("未找到已配对设备UUID", log: logger, type: .info) - return nil - } +// /// 获取已配对设备的UUID字符串 +// /// 返回存储的配对设备UUID,如果没有则返回nil +// func getPairedMacAddress() -> String? { +// let uuidString = UserDefaults.standard.string(forKey: pairedDeviceUUIDKey) +// if let uuidString = uuidString, !uuidString.isEmpty { +// os_log("获取已配对设备UUID: %{public}@", log: logger, type: .info, uuidString) +// return uuidString +// } +// os_log("未找到已配对设备UUID", log: logger, type: .info) +// return nil +// } - /// 保存配对设备的UUID - /// - Parameter uuid: 要保存的设备UUID - func savePairedMacAddress(_ uuid: UUID) { - let uuidString = uuid.uuidString - UserDefaults.standard.set(uuidString, forKey: pairedDeviceUUIDKey) - UserDefaults.standard.synchronize() - os_log("已保存配对设备UUID: %{public}@", log: logger, type: .info, uuidString) - } +// /// 保存配对设备的UUID +// /// - Parameter uuid: 要保存的设备UUID +// func savePairedMacAddress(_ uuid: UUID) { +// let uuidString = uuid.uuidString +// UserDefaults.standard.set(uuidString, forKey: pairedDeviceUUIDKey) +// UserDefaults.standard.synchronize() +// os_log("已保存配对设备UUID: %{public}@", log: logger, type: .info, uuidString) +// } - /// 清除已保存的配对设备UUID - private func clearPairedMacAddress() { - UserDefaults.standard.removeObject(forKey: pairedDeviceUUIDKey) - UserDefaults.standard.synchronize() - os_log("已清除配对设备UUID", log: logger, type: .info) - } -} \ No newline at end of file +// /// 清除已保存的配对设备UUID +// private func clearPairedMacAddress() { +// UserDefaults.standard.removeObject(forKey: pairedDeviceUUIDKey) +// UserDefaults.standard.synchronize() +// os_log("已清除配对设备UUID", log: logger, type: .info) +// } +// } \ No newline at end of file diff --git a/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleScanManager.swift b/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleScanManager.swift index 1be2b0c46..692984773 100644 --- a/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleScanManager.swift +++ b/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleScanManager.swift @@ -1,586 +1,586 @@ -import Foundation -import CoreBluetooth -import UIKit // 添加UIKit导入 -import os.log +// import Foundation +// import CoreBluetooth +// import UIKit // 添加UIKit导入 +// import os.log -/// 简化的 BLE 扫描管理器 -/// 只提供扫描和获取设备列表功能 -@available(iOS 13.0, *) -public class BleScanManager: NSObject { - - // MARK: - 单例 - public static let shared = BleScanManager() - - // MARK: - 数据模型 - public struct BleDeviceInfo { - public let identifier: UUID - public let name: String? - public let rssi: NSNumber - public let advertisementData: [String: Any] - public let discoveryMethod: DiscoveryMethod - public let lastSeen: Date - - public enum DiscoveryMethod { - case traditional - case accessorySetupKit - } - } - - // MARK: - 属性 - private var centralManager: CBCentralManager! - private let logger = OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleScanManager") - - // 扫描状态 - private var isScanning = false - private var scanTimeout: Timer? - - // 设备列表 - private var discoveredDevices: [UUID: BleDeviceInfo] = [:] - - - // MARK: - 初始化 - private override init() { - super.init() - setupCentralManager() - } - - // 在setupCentralManager后添加权限检查 - private func setupCentralManager() { - let options: [String: Any] = [ - CBCentralManagerOptionShowPowerAlertKey: true, - CBCentralManagerOptionRestoreIdentifierKey: "BleScanManager" - ] - - centralManager = CBCentralManager(delegate: self, queue: nil, options: options) - - // 添加权限检查和触发逻辑 - DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in - self?.checkAndTriggerPermission() - } - } - - private func checkAndTriggerPermission() { - // 检查蓝牙权限状态,兼容不同iOS版本 - let authorization: CBManagerAuthorization - if #available(iOS 13.1, *) { - authorization = CBManager.authorization - } else if #available(iOS 13.0, *) { - // iOS 13.0 使用实例方法,需要确保centralManager已初始化 - authorization = centralManager?.authorization ?? .notDetermined - } else { - // iOS 13.0以下版本没有权限检查API,默认为未确定 - authorization = .notDetermined - } - - os_log("📊 蓝牙权限状态: %d", log: logger, type: .info, authorization.rawValue) - os_log("📊 CBCentralManager状态: %d", log: logger, type: .info, centralManager?.state.rawValue ?? -1) - - // 只在权限未确定且蓝牙状态为关闭时触发扫描 - if authorization == .notDetermined && centralManager?.state == .poweredOff { - os_log("📊 主动触发一次扫描来申请权限", log: logger, type: .info) - // 主动触发一次扫描来申请权限 - centralManager?.scanForPeripherals(withServices: nil, options: nil) - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in - self?.centralManager?.stopScan() - } - } - } - - - - // MARK: - 公共接口 - - /// 开始扫描 BLE 设备 - /// - Parameter timeout: 扫描超时时间,默认 10 秒 - /// - Returns: 是否成功开始扫描 - public func startScan(timeout: TimeInterval = 30.0) -> Bool { - os_log("🔍 开始 BLE 扫描,超时时间: %.1f 秒", log: logger, type: .info, timeout) - - guard !isScanning else { - os_log("⚠️ 扫描已在进行中", log: logger, type: .info) - return false - } - - // 清空之前的设备列表 - discoveredDevices.removeAll() - - // iOS 18 兼容性检查 - if #available(iOS 18.0, *) { - // iOS 18 使用新的权限检查方式 - return startScanForIOS18(timeout: timeout) - } else { - // iOS 17 及以下使用传统方式 - return startScanLegacy(timeout: timeout) - } - } - - @available(iOS 18.0, *) - private func startScanForIOS18(timeout: TimeInterval) -> Bool { - // 对于 iOS 18,直接检查 CBCentralManager 状态 - guard let centralManager = centralManager else { - os_log("❌ CBCentralManager 未初始化", log: logger, type: .error) - return false - } - - let currentState = centralManager.state - os_log("📊 iOS 18 蓝牙状态: %d (%{public}@)", log: logger, type: .info, currentState.rawValue, getBluetoothStateString(currentState)) - - switch currentState { - case .poweredOn: - return performScanWithPermission(timeout: timeout) - case .unauthorized: - os_log("❌ 蓝牙权限被拒绝", log: logger, type: .error) - showPermissionAlert() - return false - case .unknown: - os_log("⚠️ 蓝牙状态未知,等待状态更新", log: logger, type: .info) - // 设置监听器等待状态更新 - waitForBluetoothState(timeout: timeout) - return true - default: - os_log("❌ 蓝牙不可用: %{public}@", log: logger, type: .error, getBluetoothStateString(currentState)) - return false - } - } - - private func startScanLegacy(timeout: TimeInterval) -> Bool { - // 检查并申请蓝牙权限 - let authorization: CBManagerAuthorization = CBManager.authorization - os_log("📊 当前蓝牙权限状态: %d", log: logger, type: .info, authorization.rawValue) - - switch authorization { - case .allowedAlways: - return performScanWithPermission(timeout: timeout) - case .denied, .restricted: - os_log("❌ 蓝牙权限被拒绝或受限", log: logger, type: .error) - showPermissionAlert() - return false - case .notDetermined: - // 权限未确定,需要主动触发蓝牙操作来显示权限弹窗 - if centralManager == nil { - setupCentralManager() - } +// /// 简化的 BLE 扫描管理器 +// /// 只提供扫描和获取设备列表功能 +// @available(iOS 13.0, *) +// public class BleScanManager: NSObject { + +// // MARK: - 单例 +// public static let shared = BleScanManager() + +// // MARK: - 数据模型 +// public struct BleDeviceInfo { +// public let identifier: UUID +// public let name: String? +// public let rssi: NSNumber +// public let advertisementData: [String: Any] +// public let discoveryMethod: DiscoveryMethod +// public let lastSeen: Date + +// public enum DiscoveryMethod { +// case traditional +// case accessorySetupKit +// } +// } + +// // MARK: - 属性 +// private var centralManager: CBCentralManager! +// private let logger = OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleScanManager") + +// // 扫描状态 +// private var isScanning = false +// private var scanTimeout: Timer? + +// // 设备列表 +// private var discoveredDevices: [UUID: BleDeviceInfo] = [:] + + +// // MARK: - 初始化 +// private override init() { +// super.init() +// setupCentralManager() +// } + +// // 在setupCentralManager后添加权限检查 +// private func setupCentralManager() { +// let options: [String: Any] = [ +// CBCentralManagerOptionShowPowerAlertKey: true, +// CBCentralManagerOptionRestoreIdentifierKey: "BleScanManager" +// ] + +// centralManager = CBCentralManager(delegate: self, queue: nil, options: options) + +// // 添加权限检查和触发逻辑 +// DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in +// self?.checkAndTriggerPermission() +// } +// } + +// private func checkAndTriggerPermission() { +// // 检查蓝牙权限状态,兼容不同iOS版本 +// let authorization: CBManagerAuthorization +// if #available(iOS 13.1, *) { +// authorization = CBManager.authorization +// } else if #available(iOS 13.0, *) { +// // iOS 13.0 使用实例方法,需要确保centralManager已初始化 +// authorization = centralManager?.authorization ?? .notDetermined +// } else { +// // iOS 13.0以下版本没有权限检查API,默认为未确定 +// authorization = .notDetermined +// } + +// os_log("📊 蓝牙权限状态: %d", log: logger, type: .info, authorization.rawValue) +// os_log("📊 CBCentralManager状态: %d", log: logger, type: .info, centralManager?.state.rawValue ?? -1) + +// // 只在权限未确定且蓝牙状态为关闭时触发扫描 +// if authorization == .notDetermined && centralManager?.state == .poweredOff { +// os_log("📊 主动触发一次扫描来申请权限", log: logger, type: .info) +// // 主动触发一次扫描来申请权限 +// centralManager?.scanForPeripherals(withServices: nil, options: nil) +// DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in +// self?.centralManager?.stopScan() +// } +// } +// } + + + +// // MARK: - 公共接口 + +// /// 开始扫描 BLE 设备 +// /// - Parameter timeout: 扫描超时时间,默认 10 秒 +// /// - Returns: 是否成功开始扫描 +// public func startScan(timeout: TimeInterval = 30.0) -> Bool { +// os_log("🔍 开始 BLE 扫描,超时时间: %.1f 秒", log: logger, type: .info, timeout) + +// guard !isScanning else { +// os_log("⚠️ 扫描已在进行中", log: logger, type: .info) +// return false +// } + +// // 清空之前的设备列表 +// discoveredDevices.removeAll() + +// // iOS 18 兼容性检查 +// if #available(iOS 18.0, *) { +// // iOS 18 使用新的权限检查方式 +// return startScanForIOS18(timeout: timeout) +// } else { +// // iOS 17 及以下使用传统方式 +// return startScanLegacy(timeout: timeout) +// } +// } + +// @available(iOS 18.0, *) +// private func startScanForIOS18(timeout: TimeInterval) -> Bool { +// // 对于 iOS 18,直接检查 CBCentralManager 状态 +// guard let centralManager = centralManager else { +// os_log("❌ CBCentralManager 未初始化", log: logger, type: .error) +// return false +// } + +// let currentState = centralManager.state +// os_log("📊 iOS 18 蓝牙状态: %d (%{public}@)", log: logger, type: .info, currentState.rawValue, getBluetoothStateString(currentState)) + +// switch currentState { +// case .poweredOn: +// return performScanWithPermission(timeout: timeout) +// case .unauthorized: +// os_log("❌ 蓝牙权限被拒绝", log: logger, type: .error) +// showPermissionAlert() +// return false +// case .unknown: +// os_log("⚠️ 蓝牙状态未知,等待状态更新", log: logger, type: .info) +// // 设置监听器等待状态更新 +// waitForBluetoothState(timeout: timeout) +// return true +// default: +// os_log("❌ 蓝牙不可用: %{public}@", log: logger, type: .error, getBluetoothStateString(currentState)) +// return false +// } +// } + +// private func startScanLegacy(timeout: TimeInterval) -> Bool { +// // 检查并申请蓝牙权限 +// let authorization: CBManagerAuthorization = CBManager.authorization +// os_log("📊 当前蓝牙权限状态: %d", log: logger, type: .info, authorization.rawValue) + +// switch authorization { +// case .allowedAlways: +// return performScanWithPermission(timeout: timeout) +// case .denied, .restricted: +// os_log("❌ 蓝牙权限被拒绝或受限", log: logger, type: .error) +// showPermissionAlert() +// return false +// case .notDetermined: +// // 权限未确定,需要主动触发蓝牙操作来显示权限弹窗 +// if centralManager == nil { +// setupCentralManager() +// } - // 等待CBCentralManager状态更新后,主动开始扫描来触发权限弹窗 - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in - guard let self = self else { return } - if self.centralManager.state == .poweredOn { - // 开始一个短暂的扫描来触发权限申请 - self.centralManager.scanForPeripherals(withServices: nil, options: nil) - // 立即停止扫描 - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - self.centralManager.stopScan() - } - } else if self.centralManager.state == .unauthorized { - // 权限被拒绝,显示设置提示 - self.showPermissionAlert() - } - } - return true - @unknown default: - os_log("⚠️ 未知的蓝牙权限状态", log: logger, type: .info) - return false - } - } - - private func waitForBluetoothState(timeout: TimeInterval) { - var checkCount = 0 - let maxChecks = 20 - - let timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] timer in - guard let self = self else { - timer.invalidate() - return - } +// // 等待CBCentralManager状态更新后,主动开始扫描来触发权限弹窗 +// DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in +// guard let self = self else { return } +// if self.centralManager.state == .poweredOn { +// // 开始一个短暂的扫描来触发权限申请 +// self.centralManager.scanForPeripherals(withServices: nil, options: nil) +// // 立即停止扫描 +// DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { +// self.centralManager.stopScan() +// } +// } else if self.centralManager.state == .unauthorized { +// // 权限被拒绝,显示设置提示 +// self.showPermissionAlert() +// } +// } +// return true +// @unknown default: +// os_log("⚠️ 未知的蓝牙权限状态", log: logger, type: .info) +// return false +// } +// } + +// private func waitForBluetoothState(timeout: TimeInterval) { +// var checkCount = 0 +// let maxChecks = 20 + +// let timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] timer in +// guard let self = self else { +// timer.invalidate() +// return +// } - checkCount += 1 - let currentState = self.centralManager?.state ?? .unknown +// checkCount += 1 +// let currentState = self.centralManager?.state ?? .unknown - os_log("🔍 检查蓝牙状态 (%d/%d): %{public}@", log: self.logger, type: .info, checkCount, maxChecks, self.getBluetoothStateString(currentState)) +// os_log("🔍 检查蓝牙状态 (%d/%d): %{public}@", log: self.logger, type: .info, checkCount, maxChecks, self.getBluetoothStateString(currentState)) - switch currentState { - case .poweredOn: - timer.invalidate() - _ = self.performScanWithPermission(timeout: timeout) - case .unauthorized: - timer.invalidate() - self.showPermissionAlert() - case .poweredOff, .unsupported: - timer.invalidate() - os_log("❌ 蓝牙不可用", log: self.logger, type: .error) - default: - if checkCount >= maxChecks { - timer.invalidate() - os_log("⏰ 等待蓝牙状态超时", log: self.logger, type: .error) - } - } - } - } - - private func forceTriggerPermissionRequest(timeout: TimeInterval) { - // 完全清理旧的 centralManager - centralManager?.delegate = nil - centralManager = nil - - // 强制重新初始化,确保触发权限申请 - let options: [String: Any] = [ - CBCentralManagerOptionShowPowerAlertKey: true, - CBCentralManagerOptionRestoreIdentifierKey: "BleScanManager" - ] - - centralManager = CBCentralManager(delegate: self, queue: nil, options: options) - - // 设置一个监听器来等待权限状态更新 - waitForPermissionResponse(timeout: timeout) - } - - private func waitForPermissionResponse(timeout: TimeInterval) { - // 设置一个定时器来检查权限状态变化 - var checkCount = 0 - let maxChecks = 20 // 最多检查20次,每次间隔0.5秒,总共10秒 - - let timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] timer in - guard let self = self else { - timer.invalidate() - return - } +// switch currentState { +// case .poweredOn: +// timer.invalidate() +// _ = self.performScanWithPermission(timeout: timeout) +// case .unauthorized: +// timer.invalidate() +// self.showPermissionAlert() +// case .poweredOff, .unsupported: +// timer.invalidate() +// os_log("❌ 蓝牙不可用", log: self.logger, type: .error) +// default: +// if checkCount >= maxChecks { +// timer.invalidate() +// os_log("⏰ 等待蓝牙状态超时", log: self.logger, type: .error) +// } +// } +// } +// } + +// private func forceTriggerPermissionRequest(timeout: TimeInterval) { +// // 完全清理旧的 centralManager +// centralManager?.delegate = nil +// centralManager = nil + +// // 强制重新初始化,确保触发权限申请 +// let options: [String: Any] = [ +// CBCentralManagerOptionShowPowerAlertKey: true, +// CBCentralManagerOptionRestoreIdentifierKey: "BleScanManager" +// ] + +// centralManager = CBCentralManager(delegate: self, queue: nil, options: options) + +// // 设置一个监听器来等待权限状态更新 +// waitForPermissionResponse(timeout: timeout) +// } + +// private func waitForPermissionResponse(timeout: TimeInterval) { +// // 设置一个定时器来检查权限状态变化 +// var checkCount = 0 +// let maxChecks = 20 // 最多检查20次,每次间隔0.5秒,总共10秒 + +// let timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] timer in +// guard let self = self else { +// timer.invalidate() +// return +// } - checkCount += 1 - let currentAuth = CBManager.authorization +// checkCount += 1 +// let currentAuth = CBManager.authorization - os_log("🔍 检查权限状态 (%d/%d): %d", log: self.logger, type: .info, checkCount, maxChecks, currentAuth.rawValue) +// os_log("🔍 检查权限状态 (%d/%d): %d", log: self.logger, type: .info, checkCount, maxChecks, currentAuth.rawValue) - switch currentAuth { - case .allowedAlways: - timer.invalidate() - os_log("✅ 蓝牙权限已获得,开始扫描", log: self.logger, type: .info) - _ = self.performScanWithPermission(timeout: timeout) - case .denied, .restricted: - timer.invalidate() - os_log("❌ 蓝牙权限被拒绝", log: self.logger, type: .error) - self.showPermissionAlert() - case .notDetermined: - if checkCount >= maxChecks { - timer.invalidate() - os_log("⏰ 等待权限响应超时", log: self.logger, type: .error) - self.showPermissionAlert() - } - // 继续等待 - @unknown default: - timer.invalidate() - os_log("⚠️ 未知权限状态", log: self.logger, type: .error) - } - } - } - - private func reinitializeCentralManager() { - // 清理旧的 centralManager - centralManager?.delegate = nil - centralManager = nil - - // 重新初始化 - setupCentralManager() - } - - /// 检查蓝牙权限状态 - private func checkBluetoothPermissionStatus() -> (hasPermission: Bool, needsRequest: Bool) { - // 检查权限状态 - let authorization: CBManagerAuthorization - if #available(iOS 13.1, *) { - authorization = CBManager.authorization - } else if #available(iOS 13.0, *) { - if centralManager == nil { - setupCentralManager() - } - authorization = centralManager?.authorization ?? .notDetermined - } else { - // iOS 13以下没有权限概念 - return (true, false) - } - - os_log("📊 蓝牙权限状态: %d, CBCentralManager状态: %d", log: logger, type: .info, authorization.rawValue, centralManager?.state.rawValue ?? -1) - - switch authorization { - case .allowedAlways: - return (true, false) - case .denied, .restricted: - return (false, false) - case .notDetermined: - return (false, true) - @unknown default: - return (false, true) - } - } - - private func performScanWithPermission(timeout: TimeInterval) -> Bool { - // 首先检查权限状态 - let permissionStatus = checkBluetoothPermissionStatus() - - if !permissionStatus.hasPermission { - if permissionStatus.needsRequest { - os_log("🔐 需要申请蓝牙权限", log: logger, type: .info) - _ = requestBluetoothPermission() - return false - } else { - os_log("❌ 蓝牙权限被拒绝", log: logger, type: .error) - showPermissionAlert() - return false - } - } - - // 权限已获得,检查蓝牙硬件状态 - let currentState = centralManager.state - os_log("📊 蓝牙硬件状态: %d (%{public}@)", log: logger, type: .info, currentState.rawValue, getBluetoothStateString(currentState)) - - switch currentState { - case .poweredOn: - // 蓝牙已开启,可以扫描 - break - case .unknown: - os_log("⚠️ 蓝牙状态未知,等待状态更新...", log: logger, type: .info) - DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in - _ = self?.performScanWithPermission(timeout: timeout) - } - return false - case .poweredOff: - // 区分是权限问题还是硬件关闭 - if permissionStatus.hasPermission { - os_log("❌ 蓝牙硬件已关闭,请在设备设置中开启蓝牙", log: logger, type: .error) - } else { - os_log("❌ 可能是权限问题导致的蓝牙状态异常", log: logger, type: .error) - _ = requestBluetoothPermission() - } - return false - case .unauthorized: - os_log("❌ 应用未授权使用蓝牙", log: logger, type: .error) - showPermissionAlert() - return false - case .unsupported: - os_log("❌ 设备不支持蓝牙", log: logger, type: .error) - return false - default: - os_log("⚠️ 其他蓝牙状态: %d", log: logger, type: .error, currentState.rawValue) - return false - } - - // 开始BLE扫描 - centralManager.scanForPeripherals(withServices: nil, options: [ - CBCentralManagerScanOptionAllowDuplicatesKey: true - ]) - - isScanning = true - - // 设置扫描超时 - scanTimeout = Timer.scheduledTimer(withTimeInterval: timeout, repeats: false) { [weak self] _ in - self?.stopScan() - } - - os_log("✅ 开始BLE扫描,超时时间: %.1f 秒", log: logger, type: .info, timeout) - return true - } - - private func showPermissionAlert() { - guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, - let window = windowScene.windows.first else { - os_log("❌ 无法获取当前窗口", log: logger, type: .error) - return - } - - let alert = UIAlertController( - title: "需要蓝牙权限", - message: "请在设置中开启蓝牙权限以使用设备扫描功能", - preferredStyle: .alert - ) - - alert.addAction(UIAlertAction(title: "去设置", style: .default) { _ in - if let settingsUrl = URL(string: UIApplication.openSettingsURLString) { - UIApplication.shared.open(settingsUrl) - } - }) - - alert.addAction(UIAlertAction(title: "取消", style: .cancel)) - - window.rootViewController?.present(alert, animated: true) - } - - /// 停止扫描 - public func stopScan() { - guard isScanning else { return } - - os_log("🛑 停止 BLE 扫描", log: logger, type: .info) - - centralManager.stopScan() // 直接使用,不需要可选绑定 - isScanning = false - - scanTimeout?.invalidate() - scanTimeout = nil - } - - /// 获取已发现的设备列表 - /// - Returns: 按最后发现时间排序的设备列表 - public func getDiscoveredDevices() -> [BleDeviceInfo] { - return Array(discoveredDevices.values).sorted { $0.lastSeen > $1.lastSeen } - } - - /// 清除设备列表 - public func clearDeviceList() { - discoveredDevices.removeAll() - os_log("🗑️ 已清除设备列表", log: logger, type: .info) - } - - /// 检查是否正在扫描 - public func isScanningActive() -> Bool { - return isScanning - } - - /// 获取蓝牙状态 - public func getBluetoothState() -> CBManagerState { - return centralManager.state // 直接返回,不需要 ?? .unknown - } - - // MARK: - 私有方法 +// switch currentAuth { +// case .allowedAlways: +// timer.invalidate() +// os_log("✅ 蓝牙权限已获得,开始扫描", log: self.logger, type: .info) +// _ = self.performScanWithPermission(timeout: timeout) +// case .denied, .restricted: +// timer.invalidate() +// os_log("❌ 蓝牙权限被拒绝", log: self.logger, type: .error) +// self.showPermissionAlert() +// case .notDetermined: +// if checkCount >= maxChecks { +// timer.invalidate() +// os_log("⏰ 等待权限响应超时", log: self.logger, type: .error) +// self.showPermissionAlert() +// } +// // 继续等待 +// @unknown default: +// timer.invalidate() +// os_log("⚠️ 未知权限状态", log: self.logger, type: .error) +// } +// } +// } + +// private func reinitializeCentralManager() { +// // 清理旧的 centralManager +// centralManager?.delegate = nil +// centralManager = nil + +// // 重新初始化 +// setupCentralManager() +// } + +// /// 检查蓝牙权限状态 +// private func checkBluetoothPermissionStatus() -> (hasPermission: Bool, needsRequest: Bool) { +// // 检查权限状态 +// let authorization: CBManagerAuthorization +// if #available(iOS 13.1, *) { +// authorization = CBManager.authorization +// } else if #available(iOS 13.0, *) { +// if centralManager == nil { +// setupCentralManager() +// } +// authorization = centralManager?.authorization ?? .notDetermined +// } else { +// // iOS 13以下没有权限概念 +// return (true, false) +// } + +// os_log("📊 蓝牙权限状态: %d, CBCentralManager状态: %d", log: logger, type: .info, authorization.rawValue, centralManager?.state.rawValue ?? -1) + +// switch authorization { +// case .allowedAlways: +// return (true, false) +// case .denied, .restricted: +// return (false, false) +// case .notDetermined: +// return (false, true) +// @unknown default: +// return (false, true) +// } +// } + +// private func performScanWithPermission(timeout: TimeInterval) -> Bool { +// // 首先检查权限状态 +// let permissionStatus = checkBluetoothPermissionStatus() + +// if !permissionStatus.hasPermission { +// if permissionStatus.needsRequest { +// os_log("🔐 需要申请蓝牙权限", log: logger, type: .info) +// _ = requestBluetoothPermission() +// return false +// } else { +// os_log("❌ 蓝牙权限被拒绝", log: logger, type: .error) +// showPermissionAlert() +// return false +// } +// } + +// // 权限已获得,检查蓝牙硬件状态 +// let currentState = centralManager.state +// os_log("📊 蓝牙硬件状态: %d (%{public}@)", log: logger, type: .info, currentState.rawValue, getBluetoothStateString(currentState)) + +// switch currentState { +// case .poweredOn: +// // 蓝牙已开启,可以扫描 +// break +// case .unknown: +// os_log("⚠️ 蓝牙状态未知,等待状态更新...", log: logger, type: .info) +// DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in +// _ = self?.performScanWithPermission(timeout: timeout) +// } +// return false +// case .poweredOff: +// // 区分是权限问题还是硬件关闭 +// if permissionStatus.hasPermission { +// os_log("❌ 蓝牙硬件已关闭,请在设备设置中开启蓝牙", log: logger, type: .error) +// } else { +// os_log("❌ 可能是权限问题导致的蓝牙状态异常", log: logger, type: .error) +// _ = requestBluetoothPermission() +// } +// return false +// case .unauthorized: +// os_log("❌ 应用未授权使用蓝牙", log: logger, type: .error) +// showPermissionAlert() +// return false +// case .unsupported: +// os_log("❌ 设备不支持蓝牙", log: logger, type: .error) +// return false +// default: +// os_log("⚠️ 其他蓝牙状态: %d", log: logger, type: .error, currentState.rawValue) +// return false +// } + +// // 开始BLE扫描 +// centralManager.scanForPeripherals(withServices: nil, options: [ +// CBCentralManagerScanOptionAllowDuplicatesKey: true +// ]) + +// isScanning = true + +// // 设置扫描超时 +// scanTimeout = Timer.scheduledTimer(withTimeInterval: timeout, repeats: false) { [weak self] _ in +// self?.stopScan() +// } + +// os_log("✅ 开始BLE扫描,超时时间: %.1f 秒", log: logger, type: .info, timeout) +// return true +// } + +// private func showPermissionAlert() { +// guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, +// let window = windowScene.windows.first else { +// os_log("❌ 无法获取当前窗口", log: logger, type: .error) +// return +// } + +// let alert = UIAlertController( +// title: "需要蓝牙权限", +// message: "请在设置中开启蓝牙权限以使用设备扫描功能", +// preferredStyle: .alert +// ) + +// alert.addAction(UIAlertAction(title: "去设置", style: .default) { _ in +// if let settingsUrl = URL(string: UIApplication.openSettingsURLString) { +// UIApplication.shared.open(settingsUrl) +// } +// }) + +// alert.addAction(UIAlertAction(title: "取消", style: .cancel)) + +// window.rootViewController?.present(alert, animated: true) +// } + +// /// 停止扫描 +// public func stopScan() { +// guard isScanning else { return } + +// os_log("🛑 停止 BLE 扫描", log: logger, type: .info) + +// centralManager.stopScan() // 直接使用,不需要可选绑定 +// isScanning = false + +// scanTimeout?.invalidate() +// scanTimeout = nil +// } + +// /// 获取已发现的设备列表 +// /// - Returns: 按最后发现时间排序的设备列表 +// public func getDiscoveredDevices() -> [BleDeviceInfo] { +// return Array(discoveredDevices.values).sorted { $0.lastSeen > $1.lastSeen } +// } + +// /// 清除设备列表 +// public func clearDeviceList() { +// discoveredDevices.removeAll() +// os_log("🗑️ 已清除设备列表", log: logger, type: .info) +// } + +// /// 检查是否正在扫描 +// public func isScanningActive() -> Bool { +// return isScanning +// } + +// /// 获取蓝牙状态 +// public func getBluetoothState() -> CBManagerState { +// return centralManager.state // 直接返回,不需要 ?? .unknown +// } + +// // MARK: - 私有方法 - private func addOrUpdateDevice(_ deviceInfo: BleDeviceInfo) { - let identifier = deviceInfo.identifier - discoveredDevices[identifier] = deviceInfo - - os_log("📱 发现设备: %{public}@", log: logger, type: .debug, deviceInfo.name ?? "Unknown") - } - - /// 强制触发蓝牙权限申请 - public func requestBluetoothPermission() -> Bool { - os_log("🔐 强制触发蓝牙权限申请", log: logger, type: .info) - - // 检查当前权限状态 - let authorization: CBManagerAuthorization - if #available(iOS 13.1, *) { - authorization = CBManager.authorization - } else if #available(iOS 13.0, *) { - // 确保centralManager已初始化 - if centralManager == nil { - setupCentralManager() - } - authorization = centralManager.authorization - } else { - // iOS 13以下没有权限概念,直接返回true - return true - } - - os_log("📊 当前权限状态: %d", log: logger, type: .info, authorization.rawValue) - - switch authorization { - case .notDetermined: - // 权限未确定,需要主动触发蓝牙操作来显示权限弹窗 - if centralManager == nil { - os_log("📊 初始化蓝牙管理对象", log: logger, type: .info, "") - setupCentralManager() - } +// private func addOrUpdateDevice(_ deviceInfo: BleDeviceInfo) { +// let identifier = deviceInfo.identifier +// discoveredDevices[identifier] = deviceInfo + +// os_log("📱 发现设备: %{public}@", log: logger, type: .debug, deviceInfo.name ?? "Unknown") +// } + +// /// 强制触发蓝牙权限申请 +// public func requestBluetoothPermission() -> Bool { +// os_log("🔐 强制触发蓝牙权限申请", log: logger, type: .info) + +// // 检查当前权限状态 +// let authorization: CBManagerAuthorization +// if #available(iOS 13.1, *) { +// authorization = CBManager.authorization +// } else if #available(iOS 13.0, *) { +// // 确保centralManager已初始化 +// if centralManager == nil { +// setupCentralManager() +// } +// authorization = centralManager.authorization +// } else { +// // iOS 13以下没有权限概念,直接返回true +// return true +// } + +// os_log("📊 当前权限状态: %d", log: logger, type: .info, authorization.rawValue) + +// switch authorization { +// case .notDetermined: +// // 权限未确定,需要主动触发蓝牙操作来显示权限弹窗 +// if centralManager == nil { +// os_log("📊 初始化蓝牙管理对象", log: logger, type: .info, "") +// setupCentralManager() +// } - // 等待CBCentralManager状态更新后,主动开始扫描来触发权限弹窗 - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in - guard let self = self else { return } +// // 等待CBCentralManager状态更新后,主动开始扫描来触发权限弹窗 +// DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in +// guard let self = self else { return } - if self.centralManager.state == .poweredOn { - // 开始一个短暂的扫描来触发权限申请 - self.centralManager.scanForPeripherals(withServices: nil, options: nil) - // 立即停止扫描 - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - self.centralManager.stopScan() - } - } else if self.centralManager.state == .unauthorized { - // 权限被拒绝,显示设置提示 - self.showPermissionAlert() - } - } - return true - case .allowedAlways: - os_log("✅ 权限已授予", log: logger, type: .info) - return true - case .denied, .restricted: - os_log("❌ 权限被拒绝,显示设置提示", log: logger, type: .error) - showPermissionAlert() - return false - @unknown default: - os_log("⚠️ 未知权限状态", log: logger, type: .info) - return false - } - } -} +// if self.centralManager.state == .poweredOn { +// // 开始一个短暂的扫描来触发权限申请 +// self.centralManager.scanForPeripherals(withServices: nil, options: nil) +// // 立即停止扫描 +// DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { +// self.centralManager.stopScan() +// } +// } else if self.centralManager.state == .unauthorized { +// // 权限被拒绝,显示设置提示 +// self.showPermissionAlert() +// } +// } +// return true +// case .allowedAlways: +// os_log("✅ 权限已授予", log: logger, type: .info) +// return true +// case .denied, .restricted: +// os_log("❌ 权限被拒绝,显示设置提示", log: logger, type: .error) +// showPermissionAlert() +// return false +// @unknown default: +// os_log("⚠️ 未知权限状态", log: logger, type: .info) +// return false +// } +// } +// } -// MARK: - CBCentralManagerDelegate -@available(iOS 13.0, *) -extension BleScanManager: CBCentralManagerDelegate { - - public func centralManagerDidUpdateState(_ central: CBCentralManager) { - let stateString = getBluetoothStateString(central.state) - os_log("🔧 CBCentralManager 状态更新: %{public}@ (原始值: %d)", log: logger, type: .info, stateString, central.state.rawValue) - - switch central.state { - case .poweredOn: - os_log("✅ 蓝牙已开启,可以开始扫描", log: logger, type: .info) - // 如果有待处理的扫描请求,现在可以执行 - case .poweredOff: - os_log("❌ 蓝牙已关闭", log: logger, type: .error) - if isScanning { - stopScan() - } - case .unauthorized: - os_log("❌ 蓝牙权限被拒绝", log: logger, type: .error) - if isScanning { - stopScan() - } - case .unsupported: - os_log("❌ 设备不支持蓝牙", log: logger, type: .error) - if isScanning { - stopScan() - } - case .unknown: - os_log("⚠️ 蓝牙状态未知,等待进一步更新", log: logger, type: .error) - case .resetting: - os_log("⚠️ 蓝牙正在重置", log: logger, type: .error) - if isScanning { - stopScan() - } - @unknown default: - os_log("⚠️ 未知蓝牙状态: %d", log: logger, type: .error, central.state.rawValue) - } - } - - public func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, - advertisementData: [String : Any], rssi RSSI: NSNumber) { - - let deviceInfo = BleDeviceInfo( - identifier: peripheral.identifier, - name: peripheral.name ?? advertisementData[CBAdvertisementDataLocalNameKey] as? String, - rssi: RSSI, - advertisementData: advertisementData, - discoveryMethod: .traditional, - lastSeen: Date() - ) - - addOrUpdateDevice(deviceInfo) - } - - public func centralManager(_ central: CBCentralManager, willRestoreState dict: [String : Any]) { - os_log("🔄 恢复 CBCentralManager 状态", log: logger, type: .info) - - // 恢复扫描状态 - if dict[CBCentralManagerRestoredStateScanServicesKey] != nil { - isScanning = true - } - } - - private func getBluetoothStateString(_ state: CBManagerState) -> String { - switch state { - case .unknown: return "未知状态" - case .resetting: return "重置中" - case .unsupported: return "设备不支持蓝牙" - case .unauthorized: return "应用未授权使用蓝牙" - case .poweredOff: return "蓝牙已关闭" - case .poweredOn: return "蓝牙已开启" - @unknown default: return "未知状态" - } - } -} +// // MARK: - CBCentralManagerDelegate +// @available(iOS 13.0, *) +// extension BleScanManager: CBCentralManagerDelegate { + +// public func centralManagerDidUpdateState(_ central: CBCentralManager) { +// let stateString = getBluetoothStateString(central.state) +// os_log("🔧 CBCentralManager 状态更新: %{public}@ (原始值: %d)", log: logger, type: .info, stateString, central.state.rawValue) + +// switch central.state { +// case .poweredOn: +// os_log("✅ 蓝牙已开启,可以开始扫描", log: logger, type: .info) +// // 如果有待处理的扫描请求,现在可以执行 +// case .poweredOff: +// os_log("❌ 蓝牙已关闭", log: logger, type: .error) +// if isScanning { +// stopScan() +// } +// case .unauthorized: +// os_log("❌ 蓝牙权限被拒绝", log: logger, type: .error) +// if isScanning { +// stopScan() +// } +// case .unsupported: +// os_log("❌ 设备不支持蓝牙", log: logger, type: .error) +// if isScanning { +// stopScan() +// } +// case .unknown: +// os_log("⚠️ 蓝牙状态未知,等待进一步更新", log: logger, type: .error) +// case .resetting: +// os_log("⚠️ 蓝牙正在重置", log: logger, type: .error) +// if isScanning { +// stopScan() +// } +// @unknown default: +// os_log("⚠️ 未知蓝牙状态: %d", log: logger, type: .error, central.state.rawValue) +// } +// } + +// public func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, +// advertisementData: [String : Any], rssi RSSI: NSNumber) { + +// let deviceInfo = BleDeviceInfo( +// identifier: peripheral.identifier, +// name: peripheral.name ?? advertisementData[CBAdvertisementDataLocalNameKey] as? String, +// rssi: RSSI, +// advertisementData: advertisementData, +// discoveryMethod: .traditional, +// lastSeen: Date() +// ) + +// addOrUpdateDevice(deviceInfo) +// } + +// public func centralManager(_ central: CBCentralManager, willRestoreState dict: [String : Any]) { +// os_log("🔄 恢复 CBCentralManager 状态", log: logger, type: .info) + +// // 恢复扫描状态 +// if dict[CBCentralManagerRestoredStateScanServicesKey] != nil { +// isScanning = true +// } +// } + +// private func getBluetoothStateString(_ state: CBManagerState) -> String { +// switch state { +// case .unknown: return "未知状态" +// case .resetting: return "重置中" +// case .unsupported: return "设备不支持蓝牙" +// case .unauthorized: return "应用未授权使用蓝牙" +// case .poweredOff: return "蓝牙已关闭" +// case .poweredOn: return "蓝牙已开启" +// @unknown default: return "未知状态" +// } +// } +// } diff --git a/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleService.swift b/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleService.swift index 93673ada8..ae25e7e3b 100644 --- a/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleService.swift +++ b/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleService.swift @@ -3,15 +3,16 @@ import CoreBluetooth import os.log import AVFoundation -/// BLE服务类 - 专为AccessorySetupKit设备设计,支持自动重连和状态恢复 +/// BLE服务类 - 传统BLE扫描和连接实现 /// /// 核心特性: -/// 1. 外设掉电→上电后自动重连(通过pendingReconnectUUIDs队列) -/// 2. App被系统终止后,外设广播或特征通知时自动恢复连接 -/// 3. 使用CBCentralManagerOptionRestoreIdentifierKey实现系统级状态保留 -/// 4. 支持bluetooth-central后台模式,允许后台BLE操作 -/// 5. willRestoreState恢复系统保留的外设、扫描条件 -/// 6. 统一的重连队列管理,适配AccessorySetupKit工作流程 +/// 1. 传统BLE扫描和连接逻辑 +/// 2. 外设掉电→上电后自动重连(通过pendingReconnectUUIDs队列) +/// 3. App被系统终止后,外设广播或特征通知时自动恢复连接 +/// 4. 使用CBCentralManagerOptionRestoreIdentifierKey实现系统级状态保留 +/// 5. 支持bluetooth-central后台模式,允许后台BLE操作 +/// 6. willRestoreState恢复系统保留的外设、扫描条件 +/// 7. 统一的重连队列管理 @available(iOS 13.0, *) public class BleService: NSObject { // MARK: - 单例实现 @@ -43,6 +44,13 @@ public class BleService: NSObject { private var audioCharacteristic: CBCharacteristic? private var isScanning = false + private var scanTimer: Timer? + + // 新增:设备缓存和广播数据记录 + private var discoveredDevices: [String: [String: Any]] = [:] // 以设备ID为key的设备信息缓存 + private var deviceAdvertisementData: [String: [String: Any]] = [:] // 设备广播数据缓存 + private var scanStartTime: Date? + private let maxCacheTime: TimeInterval = 300 // 5分钟缓存时间 // 是否是ota模式 var isEnterOta = false @@ -50,8 +58,6 @@ public class BleService: NSObject { private var callbacks = [Callback]() private var connectionState = BleConst.STATE_DISCONNECTED - - // 状态恢复相关 private var restoredPeripherals: [CBPeripheral] = [] @@ -66,22 +72,11 @@ public class BleService: NSObject { // MARK: - 初始化 private override init() { super.init() - - #if canImport(AccessorySetupKit) && swift(>=5.9) - if #available(iOS 18.0, *) { - // iOS 18+下不立即初始化,等待AccessorySetupKit - } else { - initializeCentralManager() - } - #else initializeCentralManager() - #endif - - // 初始化Opus音频处理器 initializeOpusProcessor() } - // 延迟初始化中央管理器 + // 初始化中央管理器 private func initializeCentralManager() { if centralManager != nil { os_log("🔧 CBCentralManager已存在,跳过初始化", log: logger, type: .debug) @@ -170,19 +165,13 @@ public class BleService: NSObject { } /// 开始扫描BLE设备 - public func startScan() { - #if canImport(AccessorySetupKit) && swift(>=5.9) - if #available(iOS 18.0, *) { - // iOS 18+不使用传统扫描 - return - } - #endif - + public func startScan(timeout: TimeInterval = 30.0) { if centralManager == nil { initializeCentralManager() } guard let manager = centralManager, manager.state == .poweredOn else { + os_log("❌ 蓝牙未就绪,无法开始扫描", log: logger, type: .error) return } @@ -190,6 +179,9 @@ public class BleService: NSObject { stopScan() } + // 清空之前的扫描结果 + discoveredDevices.removeAll() + isScanning = true let serviceUUIDs = [BleConst.PRIMARY_SERVICE_UUID, BleConst.AUDIO_SERVICE_UUID] @@ -197,7 +189,14 @@ public class BleService: NSObject { CBCentralManagerScanOptionAllowDuplicatesKey: false ]) - os_log("开始扫描BLE设备...", log: logger, type: .info) + os_log("🔍 开始扫描BLE设备,超时时间: %.1f秒", log: logger, type: .info, timeout) + + // 设置扫描超时 + scanTimer?.invalidate() + scanTimer = Timer.scheduledTimer(withTimeInterval: timeout, repeats: false) { [weak self] _ in + self?.stopScan() + os_log("⏰ 扫描超时,自动停止", log: self?.logger ?? OSLog.default, type: .info) + } } /// 停止扫描 @@ -205,10 +204,37 @@ public class BleService: NSObject { if isScanning && centralManager != nil { centralManager?.stopScan() isScanning = false - os_log("停止BLE扫描", log: logger, type: .info) + scanTimer?.invalidate() + scanTimer = nil + os_log("⏹️ 停止BLE扫描", log: logger, type: .info) } } + /// 连接到指定UUID的设备 + public func connectToDevice(uuid: String) -> Bool { + guard let deviceUUID = UUID(uuidString: uuid) else { + os_log("❌ 无效的设备UUID: %@", log: logger, type: .error, uuid) + return false + } + + guard let manager = centralManager else { + os_log("❌ CBCentralManager未初始化", log: logger, type: .error) + return false + } + + // 先尝试从已知外设中获取 + let knownPeripherals = manager.retrievePeripherals(withIdentifiers: [deviceUUID]) + if let peripheral = knownPeripherals.first { + connectPeripheral(peripheral) + return true + } + + // 如果没有找到,添加到重连队列 + addToReconnectQueue(uuid: deviceUUID) + os_log("📱 设备未找到,已添加到重连队列: %@", log: logger, type: .info, uuid) + return false + } + /// 连接到指定设备 public func connect(to peripheral: CBPeripheral) { connectPeripheral(peripheral) @@ -231,18 +257,18 @@ public class BleService: NSObject { } if peripheral.state == .connected { - os_log("设备已连接,恢复会话: %@", log: logger, type: .info, peripheral.identifier.uuidString) + os_log("📱 设备已连接,恢复会话: %@", log: logger, type: .info, peripheral.identifier.uuidString) resumeSession(for: peripheral) return } if peripheral.state == .connecting { - os_log("设备正在连接中: %@", log: logger, type: .info, peripheral.identifier.uuidString) + os_log("📱 设备正在连接中: %@", log: logger, type: .info, peripheral.identifier.uuidString) return } if connectionState == BleConst.STATE_CONNECTED && peripheralDevice?.identifier != peripheral.identifier { - os_log("断开当前设备,连接新设备", log: logger, type: .info) + os_log("📱 断开当前设备,连接新设备", log: logger, type: .info) disconnect() } @@ -257,7 +283,7 @@ public class BleService: NSObject { CBConnectPeripheralOptionNotifyOnNotificationKey: true ]) - os_log("开始连接到设备: %@", log: logger, type: .info, peripheral.identifier.uuidString) + os_log("🔗 开始连接到设备: %@", log: logger, type: .info, peripheral.identifier.uuidString) } /// 恢复已连接设备的会话 @@ -278,7 +304,7 @@ public class BleService: NSObject { os_log("🔍 恢复会话时重新发现服务和特征", log: logger, type: .info) peripheral.discoverServices([BleConst.PRIMARY_SERVICE_UUID, BleConst.AUDIO_SERVICE_UUID]) - os_log("已恢复设备会话: %@", log: logger, type: .info, peripheral.identifier.uuidString) + os_log("✅ 已恢复设备会话: %@", log: logger, type: .info, peripheral.identifier.uuidString) } /// 断开当前连接 @@ -291,7 +317,7 @@ public class BleService: NSObject { if centralManager != nil { updateConnectionState(BleConst.STATE_DISCONNECTING) centralManager?.cancelPeripheralConnection(peripheral) - os_log("开始断开设备连接", log: logger, type: .info) + os_log("🔌 开始断开设备连接", log: logger, type: .info) } } @@ -300,6 +326,11 @@ public class BleService: NSObject { return connectionState == BleConst.STATE_CONNECTED } + /// 获取扫描到的设备列表 + public func getDiscoveredDevices() -> [[String: Any]] { + return Array(discoveredDevices.values) + } + // MARK: - 回调通知方法 /// 通知所有回调扫描结果 @@ -903,25 +934,7 @@ public class BleService: NSObject { ] } - // MARK: - AccessorySetupKit - - /// 在接收到AccessorySetupKit的accessoryAdded事件后调用 - func connectToAccessory(uuid: UUID) { - #if canImport(AccessorySetupKit) && swift(>=5.9) - if #available(iOS 18.0, *) { - os_log("开始连接设备: %{public}@", log: logger, type: .info, uuid.uuidString) - - // 确保CBCentralManager已初始化 - if centralManager == nil { - os_log("CBCentralManager未初始化,开始初始化", log: logger, type: .debug) - initializeCentralManager() - } - - // 添加到重连队列 - addToReconnectQueue(uuid: uuid) - } - #endif - } + } // MARK: - SwiftOpusAudioProcessor.AudioDataCallback @@ -943,7 +956,7 @@ extension BleService: CBCentralManagerDelegate { case .poweredOn: os_log("🔧 蓝牙已开启", log: logger, type: .info) - // 注:AccessorySetupKit应用不需要恢复传统BLE扫描 + // 蓝牙就绪后可以开始扫描 // 恢复外设连接 if !restoredPeripherals.isEmpty { @@ -1042,22 +1055,72 @@ extension BleService: CBCentralManagerDelegate { // 原有方法 public func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) { - // 将扫描结果转换为字典格式 + let deviceId = peripheral.identifier.uuidString + let deviceName = peripheral.name ?? "Unknown" + + // 构建设备基本信息 var deviceInfo: [String: Any] = [ - "name": peripheral.name ?? "Unknown", - "id": peripheral.identifier.uuidString, - "rssi": RSSI.intValue + "name": deviceName, + "id": deviceId, + "rssi": RSSI.intValue, + "discoveredAt": Date().timeIntervalSince1970 ] - // 添加广播数据 + // 详细的广播数据记录 + var advData: [String: Any] = [:] + + // 制造商数据 if let manufacturerData = advertisementData[CBAdvertisementDataManufacturerDataKey] as? Data { deviceInfo["manufacturerData"] = manufacturerData + advData["manufacturerData"] = manufacturerData.map { String(format: "%02x", $0) }.joined(separator: " ") + advData["manufacturerDataRaw"] = manufacturerData } + // 服务UUID列表 if let serviceUUIDs = advertisementData[CBAdvertisementDataServiceUUIDsKey] as? [CBUUID] { - deviceInfo["serviceUUIDs"] = serviceUUIDs.map { $0.uuidString } + let serviceUUIDStrings = serviceUUIDs.map { $0.uuidString } + deviceInfo["serviceUUIDs"] = serviceUUIDStrings + advData["serviceUUIDs"] = serviceUUIDStrings + } + + // 本地名称 + if let localName = advertisementData[CBAdvertisementDataLocalNameKey] as? String { + advData["localName"] = localName + } + + // 发射功率 + if let txPowerLevel = advertisementData[CBAdvertisementDataTxPowerLevelKey] as? NSNumber { + advData["txPowerLevel"] = txPowerLevel.intValue } + // 服务数据 + if let serviceData = advertisementData[CBAdvertisementDataServiceDataKey] as? [CBUUID: Data] { + var serviceDataDict: [String: String] = [:] + for (uuid, data) in serviceData { + serviceDataDict[uuid.uuidString] = data.map { String(format: "%02x", $0) }.joined(separator: " ") + } + advData["serviceData"] = serviceDataDict + } + + // 可连接性 + if let isConnectable = advertisementData[CBAdvertisementDataIsConnectable] as? NSNumber { + advData["isConnectable"] = isConnectable.boolValue + } + + // 完整的广播数据 + advData["rssi"] = RSSI.intValue + advData["timestamp"] = Date().timeIntervalSince1970 + + // 缓存设备信息和广播数据 + discoveredDevices[deviceId] = deviceInfo + deviceAdvertisementData[deviceId] = advData + + // 添加广播数据到设备信息中 + deviceInfo["advertisementData"] = advData + + os_log("📡 发现设备: %{public}@ (ID: %{public}@), RSSI: %d", log: logger, type: .info, deviceName, deviceId, RSSI.intValue) + os_log("📡 广播数据: %{public}@", log: logger, type: .debug, String(describing: advData)) + // 报告给回调 notifyScanResult(devices: [deviceInfo]) } @@ -1267,4 +1330,4 @@ extension BleService: CBPeripheralDelegate { os_log("✅ 描述符写入成功: %{public}@", log: logger, type: .debug, descriptor.uuid.uuidString) } } -} +} diff --git a/local_plugins/ble_service/ios/ble_service/Sources/ble_service/SwiftBleServicePlugin.swift b/local_plugins/ble_service/ios/ble_service/Sources/ble_service/SwiftBleServicePlugin.swift index fb7f92d49..4d94982ec 100644 --- a/local_plugins/ble_service/ios/ble_service/Sources/ble_service/SwiftBleServicePlugin.swift +++ b/local_plugins/ble_service/ios/ble_service/Sources/ble_service/SwiftBleServicePlugin.swift @@ -12,79 +12,20 @@ public class SwiftBleServicePlugin: NSObject, FlutterPlugin { internal var statusEventSink: FlutterEventSink? private let logger = OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleServicePlugin") - // 标记是否已初始化所有服务 - private var servicesInitialized = false - // MARK: - 初始化方法 init(_ channel: FlutterMethodChannel) { self.methodChannel = channel super.init() - #if canImport(AccessorySetupKit) && swift(>=5.9) - if #available(iOS 18.0, *) { - - // iOS 18环境下,只设置回调但不主动初始化会话 - let bleAccessorySetup = BleAccessorySetup.shared - - // 在accessoryAdded回调中延迟设置BleService - bleAccessorySetup.setDeviceDiscoveredCallback { [weak self] uuid in - // 确保服务已初始化 - self?.ensureServicesInitialized() - // 然后处理发现的设备 - self?.handleDiscoveredDevice(uuid: uuid) - } - - // 注释掉自动初始化,改为等待用户主动调用startDeviceAssociation - // bleAccessorySetup.initializeSession() - - os_log("iOS 18环境:已设置设备发现回调,等待用户主动调用关联", log: logger, type: .info) - } else { - // 低于iOS 18,直接初始化所有服务 - initializeAllServices() - } - #else - // 非AccessorySetupKit环境,直接初始化所有服务 - initializeAllServices() - #endif + // 直接初始化BLE服务 + initializeBleService() } - - // 确保所有服务都已初始化 - private func ensureServicesInitialized() { - if !servicesInitialized { - // 设置BLE服务回调 - BleService.shared.addCallback(self) - - // 设置BleService到AccessorySetup - #if canImport(AccessorySetupKit) && swift(>=5.9) - if #available(iOS 18.0, *) { - BleAccessorySetup.shared.setBleService(BleService.shared) - } - #endif - - servicesInitialized = true - } - } - - // 初始化所有服务(iOS 17及以下版本的标准行为) - private func initializeAllServices() { + // 初始化BLE服务 + private func initializeBleService() { // 设置BLE服务回调 BleService.shared.addCallback(self) - - // 初始化AccessorySetup,但不自动开始会话 - #if canImport(AccessorySetupKit) && swift(>=5.9) - if #available(iOS 18.0, *) { - BleAccessorySetup.shared.setBleService(BleService.shared) - BleAccessorySetup.shared.setDeviceDiscoveredCallback { [weak self] uuid in - self?.handleDiscoveredDevice(uuid: uuid) - } - // 移除自动初始化,统一由startDeviceAssociation方法触发 - // BleAccessorySetup.shared.initializeSession() - } - #endif - - os_log("非iOS 18环境:已设置BLE服务但不自动初始化会话", log: logger, type: .info) - servicesInitialized = true + os_log("✅ BLE服务已初始化", log: logger, type: .info) } // MARK: - FlutterPlugin Protocol @@ -106,9 +47,6 @@ public class SwiftBleServicePlugin: NSObject, FlutterPlugin { } public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { - // 检查是否需要初始化服务 - checkAndInitializeServices() - switch call.method { case "isConnected": result(BleService.shared.isConnected()) @@ -123,31 +61,6 @@ public class SwiftBleServicePlugin: NSObject, FlutterPlugin { details: nil)) } - case "isCompanionSupported": - #if canImport(AccessorySetupKit) && swift(>=5.9) - if #available(iOS 18.0, *) { - result(true) - } else { - result(false) - } - #else - result(false) - #endif - - case "getPairedMacAddress": - #if canImport(AccessorySetupKit) && swift(>=5.9) - if #available(iOS 18.0, *) { - let pairedUUID = BleAccessorySetup.shared.getPairedMacAddress() - result(pairedUUID) - } else { - result(nil) - } - #else - result(nil) - #endif - - - case "isBleConnected": result(BleService.shared.isConnected()) @@ -193,90 +106,46 @@ public class SwiftBleServicePlugin: NSObject, FlutterPlugin { case "playA2DP": result(BleService.shared.playA2DP()) - // iOS 18+ AccessorySetupKit相关方法 - case "clearAssociations": - // 清除设备配对关系 - #if canImport(AccessorySetupKit) && swift(>=5.9) - if #available(iOS 18.0, *) { - os_log("清除设备配对关系", log: logger, type: .info) - BleAccessorySetup.shared.clearAssociations() - result(true) - } else { - result(true) - } - #else - result(true) - #endif - - case "startDeviceAssociation": - // 用户主动触发设备关联流程 - #if canImport(AccessorySetupKit) && swift(>=5.9) - if #available(iOS 18.0, *) { - os_log("用户主动触发设备关联流程", log: logger, type: .info) - - // 确保服务已初始化 - ensureServicesInitialized() - - // 重新初始化会话 - BleAccessorySetup.shared.initializeSession() - - // 返回成功,实际配对结果会通过回调通知 - result(true) - } else { - os_log("当前iOS版本不支持AccessorySetupKit", log: logger, type: .info) - result(false) - } - #else - os_log("当前环境不支持AccessorySetupKit", log: logger, type: .info) - result(false) - #endif case "startScan": let arguments = call.arguments as? [String: Any] - let devicename = (arguments?["devicename"] as? String) ?? "" + let timeout = (arguments?["timeout"] as? Double) ?? 30.0 - // 修复:支持所有iOS版本 - if #available(iOS 18.0, *) { - // iOS 18+ 使用新的BleScanManager - os_log("iOS 18+ 使用 BleScanManager 进行扫描", log: logger, type: .info) - let succ = BleScanManager.shared.startScan(timeout: 15.0) - result(succ) - } else { - // iOS 17及以下使用传统BleService - os_log("iOS 17及以下使用传统 BleService 进行扫描", log: logger, type: .info) - BleService.shared.startScan() + os_log("🔍 开始传统BLE扫描,超时: %.1f秒", log: logger, type: .info, timeout) + BleService.shared.startScan(timeout: timeout) result(true) - } - - case "getDeviceInfo": + + case "stopScan": + os_log("⏹️ 停止BLE扫描", log: logger, type: .info) + BleService.shared.stopScan() + result(true) + + case "connectToDevice": let arguments = call.arguments as? [String: Any] - let address = (arguments?["address"] as? String) ?? "" - let info = BleScanManager.shared.getDiscoveredDevices() - result(info) + guard let deviceId = arguments?["deviceId"] as? String else { + result(FlutterError(code: "INVALID_ARGUMENT", + message: "缺少设备ID参数", + details: nil)) + return + } + + os_log("🔗 连接到设备: %@", log: logger, type: .info, deviceId) + let success = BleService.shared.connectToDevice(uuid: deviceId) + result(success) + + case "getDiscoveredDevices": + let devices = BleService.shared.getDiscoveredDevices() + result(devices) + case "requestBluetoothPermission": - BleScanManager.shared.requestBluetoothPermission(); - result(true) + // 移除这个方法调用,让iOS系统自然处理权限 + result(FlutterMethodNotImplemented) + default: result(FlutterMethodNotImplemented) } } // MARK: - 私有方法 - // 检查并按需初始化服务 - private func checkAndInitializeServices() { - #if canImport(AccessorySetupKit) && swift(>=5.9) - if #available(iOS 18.0, *) { - ensureServicesInitialized() - } - #endif - } - - // AccessorySetupKit发现设备的处理 - private func handleDiscoveredDevice(uuid: UUID) { - // BleAccessorySetup内部已进行连接,此处不再重复调用 - // os_log("收到设备发现回调: %{public}@", log: logger, type: .info, uuid.uuidString) - // 可以在这里添加设备记录或其他操作 - } - private func sendEvent(_ eventSink: FlutterEventSink?, data: T, errorPrefix: String) { if let eventSink = eventSink { DispatchQueue.main.async { @@ -284,25 +153,6 @@ public class SwiftBleServicePlugin: NSObject, FlutterPlugin { } } } - - // 获取顶层视图控制器的辅助方法 - private func getTopViewController(_ viewController: UIViewController) -> UIViewController { - if let presented = viewController.presentedViewController { - return getTopViewController(presented) - } - - if let tabBarController = viewController as? UITabBarController, - let selected = tabBarController.selectedViewController { - return getTopViewController(selected) - } - - if let navigationController = viewController as? UINavigationController, - let visibleViewController = navigationController.visibleViewController { - return getTopViewController(visibleViewController) - } - - return viewController - } } // MARK: - BleService.Callback diff --git a/local_plugins/user_portrait/ios/user_portrait.podspec b/local_plugins/user_portrait/ios/user_portrait.podspec deleted file mode 100644 index 65cae4b79..000000000 --- a/local_plugins/user_portrait/ios/user_portrait.podspec +++ /dev/null @@ -1,29 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. -# Run `pod lib lint user_portrait.podspec` to validate before publishing. -# -Pod::Spec.new do |s| - s.name = 'user_portrait' - s.version = '0.0.1' - s.summary = '用户画像' - s.description = <<-DESC -用户画像 - DESC - s.homepage = 'http://example.com' - s.license = { :file => '../LICENSE' } - s.author = { 'Your Company' => 'email@example.com' } - s.source = { :path => '.' } - s.source_files = 'user_portrait/Sources/user_portrait/**/*' - s.dependency 'Flutter' - s.platform = :ios, '12.0' - - # Flutter.framework does not contain a i386 slice. - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } - s.swift_version = '5.0' - - # If your plugin requires a privacy manifest, for example if it uses any - # required reason APIs, update the PrivacyInfo.xcprivacy file to describe your - # plugin's privacy impact, and then uncomment this line. For more information, - # see https://developer.apple.com/documentation/bundleresources/privacy_manifest_files - # s.resource_bundles = {'user_portrait_privacy' => ['user_portrait/Sources/user_portrait/PrivacyInfo.xcprivacy']} -end diff --git a/local_plugins/user_portrait/ios/user_portrait/Package.swift b/local_plugins/user_portrait/ios/user_portrait/Package.swift index d8d28ea06..4d8e0e3fc 100644 --- a/local_plugins/user_portrait/ios/user_portrait/Package.swift +++ b/local_plugins/user_portrait/ios/user_portrait/Package.swift @@ -13,20 +13,12 @@ let package = Package( ], dependencies: [], targets: [ - .target( - name: "user_portrait", - dependencies: [], - resources: [ - // If your plugin requires a privacy manifest, for example if it uses any required - // reason APIs, update the PrivacyInfo.xcprivacy file to describe your plugin's - // privacy impact, and then uncomment these lines. For more information, see - // https://developer.apple.com/documentation/bundleresources/privacy_manifest_files - // .process("PrivacyInfo.xcprivacy"), - - // If you have other resources that need to be bundled with your plugin, refer to - // the following instructions to add them: - // https://developer.apple.com/documentation/xcode/bundling-resources-with-a-swift-package - ] - ) - ] + .target( + name: "user_portrait", + dependencies: [ + + ], + path: "Sources/user_portrait" + ) + ] ) diff --git a/local_plugins/user_portrait/ios/user_portrait/Sources/user_portrait/PrivacyInfo.xcprivacy b/local_plugins/user_portrait/ios/user_portrait/Sources/user_portrait/PrivacyInfo.xcprivacy deleted file mode 100644 index a34b7e2e6..000000000 --- a/local_plugins/user_portrait/ios/user_portrait/Sources/user_portrait/PrivacyInfo.xcprivacy +++ /dev/null @@ -1,14 +0,0 @@ - - - - - NSPrivacyTrackingDomains - - NSPrivacyAccessedAPITypes - - NSPrivacyCollectedDataTypes - - NSPrivacyTracking - - - diff --git a/local_plugins/user_portrait/ios/user_portrait/Sources/user_portrait/UserPortraitPlugin.swift b/local_plugins/user_portrait/ios/user_portrait/Sources/user_portrait/UserPortraitPlugin.swift index b8cdc8b21..f43d0de1a 100644 --- a/local_plugins/user_portrait/ios/user_portrait/Sources/user_portrait/UserPortraitPlugin.swift +++ b/local_plugins/user_portrait/ios/user_portrait/Sources/user_portrait/UserPortraitPlugin.swift @@ -10,8 +10,8 @@ public class UserPortraitPlugin: NSObject, FlutterPlugin { public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { switch call.method { - case "getPlatformVersion": - result("iOS " + UIDevice.current.systemVersion) + case "clean": + result(true) default: result(FlutterMethodNotImplemented) } diff --git a/local_plugins/user_portrait/pubspec.yaml b/local_plugins/user_portrait/pubspec.yaml index 1e3f104cc..862da4ec5 100644 --- a/local_plugins/user_portrait/pubspec.yaml +++ b/local_plugins/user_portrait/pubspec.yaml @@ -17,21 +17,8 @@ dev_dependencies: sdk: flutter flutter_lints: ^5.0.0 -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec -# The following section is specific to Flutter packages. flutter: - # This section identifies this Flutter project as a plugin project. - # The 'pluginClass' specifies the class (in Java, Kotlin, Swift, Objective-C, etc.) - # which should be registered in the plugin registry. This is required for - # using method channels. - # The Android 'package' specifies package in which the registered class is. - # This is required for using method channels on Android. - # The 'ffiPlugin' specifies that native code should be built and bundled. - # This is required for using `dart:ffi`. - # All these are used by the tooling to maintain consistency when - # adding or updating assets for this project. plugin: platforms: android: @@ -39,34 +26,5 @@ flutter: pluginClass: UserPortraitPlugin ios: pluginClass: UserPortraitPlugin - - # To add assets to your plugin package, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - # - # For details regarding assets in packages, see - # https://flutter.dev/to/asset-from-package - # - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/to/resolution-aware-images - - # To add custom fonts to your plugin package, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts in packages, see - # https://flutter.dev/to/font-from-package + swiftPackage: + path: ios/user_portrait diff --git a/local_plugins/user_portrait/test/user_portrait_method_channel_test.dart b/local_plugins/user_portrait/test/user_portrait_method_channel_test.dart deleted file mode 100644 index 7a2dd7e32..000000000 --- a/local_plugins/user_portrait/test/user_portrait_method_channel_test.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:user_portrait/user_portrait_method_channel.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - MethodChannelUserPortrait platform = MethodChannelUserPortrait(); - const MethodChannel channel = MethodChannel('user_portrait'); - - setUp(() { - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler( - channel, - (MethodCall methodCall) async { - return '42'; - }, - ); - }); - - tearDown(() { - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(channel, null); - }); - - test('getPlatformVersion', () async { - expect(await platform.getPlatformVersion(), '42'); - }); -} diff --git a/local_plugins/user_portrait/test/user_portrait_test.dart b/local_plugins/user_portrait/test/user_portrait_test.dart deleted file mode 100644 index 02c2ecd8a..000000000 --- a/local_plugins/user_portrait/test/user_portrait_test.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:user_portrait/user_portrait.dart'; -import 'package:user_portrait/user_portrait_platform_interface.dart'; -import 'package:user_portrait/user_portrait_method_channel.dart'; -import 'package:plugin_platform_interface/plugin_platform_interface.dart'; - -class MockUserPortraitPlatform - with MockPlatformInterfaceMixin - implements UserPortraitPlatform { - - @override - Future getPlatformVersion() => Future.value('42'); -} - -void main() { - final UserPortraitPlatform initialPlatform = UserPortraitPlatform.instance; - - test('$MethodChannelUserPortrait is the default instance', () { - expect(initialPlatform, isInstanceOf()); - }); - - test('getPlatformVersion', () async { - UserPortrait userPortraitPlugin = UserPortrait(); - MockUserPortraitPlatform fakePlatform = MockUserPortraitPlatform(); - UserPortraitPlatform.instance = fakePlatform; - - expect(await userPortraitPlugin.getPlatformVersion(), '42'); - }); -} diff --git a/pubspec.yaml b/pubspec.yaml index 2d8ec5299..a6c743df9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -45,7 +45,6 @@ dependencies: haptic_feedback: ^0.5.1+1 tencentcloud_cos_sdk_plugin: ^1.2.3 photo_view: ^0.15.0 - window_size: git: url: https://github.com/google/flutter-desktop-embedding.git