diff --git a/ios/Podfile b/ios/Podfile index 4c7418ab9..a2084703d 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -1,5 +1,5 @@ # Define a global platform for your project -platform :ios, '13.0' +platform :ios, '18.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/ios/Podfile.lock b/ios/Podfile.lock index c5e3caec9..336d0ad28 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -11,6 +11,8 @@ PODS: - Flutter - audio_waveforms (0.0.1): - Flutter + - ble_service (0.0.1): + - Flutter - chat_storage (0.0.1): - Flutter - classic_bluetooth (0.0.1): @@ -231,6 +233,7 @@ DEPENDENCIES: - app_settings (from `.symlinks/plugins/app_settings/ios`) - audio_session (from `.symlinks/plugins/audio_session/ios`) - audio_waveforms (from `.symlinks/plugins/audio_waveforms/ios`) + - ble_service (from `.symlinks/plugins/ble_service/ios`) - chat_storage (from `.symlinks/plugins/chat_storage/ios`) - classic_bluetooth (from `.symlinks/plugins/classic_bluetooth/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) @@ -302,6 +305,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/audio_session/ios" audio_waveforms: :path: ".symlinks/plugins/audio_waveforms/ios" + ble_service: + :path: ".symlinks/plugins/ble_service/ios" chat_storage: :path: ".symlinks/plugins/chat_storage/ios" classic_bluetooth: @@ -370,6 +375,7 @@ SPEC CHECKSUMS: AppAuth: d4f13a8fe0baf391b2108511793e4b479691fb73 audio_session: 9bb7f6c970f21241b19f5a3658097ae459681ba0 audio_waveforms: a6dde7fe7c0ea05f06ffbdb0f7c1b2b2ba6cedcf + ble_service: 068ebe51800424f980282b16cbf757574eec4e08 chat_storage: 933a96144157a07a2fef7f6acb28c78ff18465d8 classic_bluetooth: c5e5c0c86acab8dcd6b8e9cef7fca654c3518034 device_info_plus: 71ffc6ab7634ade6267c7a93088ed7e4f74e5896 @@ -430,6 +436,6 @@ SPEC CHECKSUMS: webview_flutter_wkwebview: 1821ceac936eba6f7984d89a9f3bcb4dea99ebb2 WechatOpenSDK-XCFramework: 36fb2bea0754266c17184adf4963d7e6ff98b69f -PODFILE CHECKSUM: 017ca79b199d8f37c4acb8d83ec912447003b920 +PODFILE CHECKSUM: f62382080fcf32f2d4640dc419aa07d69eff0328 COCOAPODS: 1.16.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 82ce9f671..70369f46d 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -44,6 +44,7 @@ /* Begin PBXFileReference section */ 0AD944112DD9DB5C00557D21 /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; + 0AD944132DDA0C0D00557D21 /* AccessorySetupKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AccessorySetupKit.framework; path = System/Library/Frameworks/AccessorySetupKit.framework; sourceTree = SDKROOT; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; @@ -106,6 +107,7 @@ 1E5DDD8701F6FB27AA502080 /* Frameworks */ = { isa = PBXGroup; children = ( + 0AD944132DDA0C0D00557D21 /* AccessorySetupKit.framework */, E27AD0DBE6531F2655DB9986 /* Pods_Runner.framework */, F79ADB762804BBED65875C03 /* Pods_RunnerTests.framework */, ); @@ -499,6 +501,7 @@ DEVELOPMENT_TEAM = 42C82SZUVX; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -519,6 +522,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -537,6 +541,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -553,6 +558,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -685,6 +691,7 @@ DEVELOPMENT_TEAM = 42C82SZUVX; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -709,6 +716,7 @@ DEVELOPMENT_TEAM = 42C82SZUVX; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 7425aacd2..eb35dbbf9 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -41,6 +41,12 @@ CFBundleVersion $(FLUTTER_BUILD_NUMBER) + UIBackgroundModes + + bluetooth-central + bluetooth-peripheral + external-accessory + LSApplicationQueriesSchemes weixin @@ -108,5 +114,9 @@ FacebookAutoLogAppEventsEnabled + NSAccessorySetupKitSupports + + Bluetooth + diff --git a/lib/core/bindings/initial_binding.dart b/lib/core/bindings/initial_binding.dart index 8921a6fca..7f5b428bd 100644 --- a/lib/core/bindings/initial_binding.dart +++ b/lib/core/bindings/initial_binding.dart @@ -4,9 +4,7 @@ import '../../data/services/speech_factory.dart'; import '../../data/services/chat_history_service.dart'; import '../../data/services/volcano_translation_service.dart'; import '../../data/services/language_manager.dart'; -import '../../data/services/volcano_ai_service.dart'; import '../../core/utils/logger.dart'; -import '../../data/services/open_ai_service_adapter.dart'; import '../../data/services/bluetooth_manager.dart'; import '../../data/services/ble_manager.dart'; @@ -17,10 +15,11 @@ class InitialBinding extends Bindings { Logger.warning('InitialBinding dependencies'); // 语言管理器(需要最先初始化) - Get.put(LanguageManager(), permanent: true); + // 语言管理器(需要最先初始化) + Get.lazyPut(() => LanguageManager(), fenix: true); //初始化语音工厂 - Get.put(SpeechFactory(), permanent: true); + Get.lazyPut(() => SpeechFactory(), fenix: true); Get.find().initialize(initialType: SpeechServiceType.azure); // 历史记录服务 @@ -30,30 +29,14 @@ class InitialBinding extends Bindings { Get.lazyPut(() => VolcanoTranslationService(), fenix: true); - // OpenAI服务适配器 - Get.lazyPut(() { - final adapter = OpenAIServiceAdapter(); - adapter.initialize(); - return adapter; - }, fenix: true); - // 蓝牙服务 - Get.put(BluetoothManager(), permanent: true); + Get.lazyPut(() => BluetoothManager(), fenix: true); // BleManager - Get.put(BleManager(), permanent: true); + Get.lazyPut(() => BleManager(), fenix: true); // MusicManager - Get.put(MusicManager(), permanent: true); + Get.lazyPut(() => MusicManager(), fenix: true); // 火山AI服务 - Get.lazyPut(() => VolcanoAIService(), fenix: true); - - // 蓝牙媒体按钮服务 - // Get.lazyPut( - // () => BluetoothMediaButtonService(), - // fenix: true); - - // // 语音交互服务 - // Get.lazyPut(() => VoiceInteractionService(), - // fenix: true); + // Get.lazyPut(() => VolcanoAIService(), fenix: true); // 杰理蓝牙服务 diff --git a/lib/modules/pairing/controllers/pairing_controller.dart b/lib/modules/pairing/controllers/pairing_controller.dart index 653c849a2..7f11158bf 100644 --- a/lib/modules/pairing/controllers/pairing_controller.dart +++ b/lib/modules/pairing/controllers/pairing_controller.dart @@ -15,8 +15,6 @@ import 'package:flutter/material.dart'; class PairingController extends GetxController { static PairingController get to => Get.find(); - // 需要设备 - final bool needdeivice = false; // 状态标志 final isLoading = false.obs; @@ -65,6 +63,9 @@ class PairingController extends GetxController { // BLE服务 final _bleService = BleService(); + // 日志tag + static const _tag = 'PairingController'; + @override void onInit() { super.onInit(); @@ -102,7 +103,7 @@ class PairingController extends GetxController { // 检查蓝牙状态并刷新设备 await _checkBluetoothAndRefresh(); } catch (e) { - Logger.error('初始化配对控制器失败: ${e.toString()}'); + Logger.e(_tag, '初始化配对控制器失败: ${e.toString()}'); statusDescription.value = 'initializationFailure'.tr; } finally { isLoading.value = false; @@ -134,13 +135,12 @@ class PairingController extends GetxController { /// 检查设备配对状态 Future _checkDevicePairingStatus() async { - // 只在Android平台且设备已连接时执行 - if (!Platform.isAndroid || !isDeviceConnected) return; + if (!isDeviceConnected) return; try { final isSupported = await _bleService.isCompanionSupported(); if (!isSupported) { - Logger.info('设备不支持Companion功能'); + Logger.i(_tag, '设备不支持Companion功能'); _companionAssociated.value = true; // 不支持则视为已配对 _userRejectedAssociation.value = false; // 重置拒绝状态 return; @@ -149,16 +149,16 @@ class PairingController extends GetxController { final pairedMac = await _bleService.getPairedMacAddress(); if (pairedMac == null) { // 没有配对记录,需要开始配对 - Logger.info('无配对记录,需要开始设备配对'); + Logger.i(_tag, '无配对记录,需要开始设备配对'); return; } - Logger.info('发现已关联的Companion设备: $pairedMac'); + Logger.i(_tag, '发现已关联的Companion设备: $pairedMac'); // 检查当前连接设备与已保存的设备是否一致 if (currentDevice!.address != pairedMac) { // MAC地址不匹配 - Logger.warning('当前连接的设备与已配对的DeepSound设备不符'); + Logger.w(_tag, '当前连接的设备与已配对的DeepSound设备不符'); statusDescription.value = 'pleaseConnectPairedDevice'.tr; return; } @@ -170,13 +170,13 @@ class PairingController extends GetxController { } // 设备匹配且合规,标记为已配对 - Logger.info('当前设备与已配对设备匹配'); + Logger.i(_tag, '当前设备与已配对设备匹配'); _companionAssociated.value = true; _userRejectedAssociation.value = false; // 重置拒绝状态 await _bleService.registerBackgroundScan(); _autoNavigateToHome(); } catch (e) { - Logger.error('检查设备配对状态失败: ${e.toString()}'); + Logger.e(_tag, '检查设备配对状态失败: ${e.toString()}'); } } @@ -198,45 +198,70 @@ class PairingController extends GetxController { permissionsToRequest.add(Permission.bluetoothConnect); } } else if (Platform.isIOS) { - // iOS上单独处理蓝牙权限请求,避免冲突 - PermissionStatus status = await Permission.bluetooth.status; + // iOS需要请求的权限 + permissionsToRequest.add(Permission.bluetooth); + permissionsToRequest.add(Permission.location); + + // 如果需要麦克风权限(例如语音功能) + // permissionsToRequest.add(Permission.microphone); - if (status.isPermanentlyDenied) { - Logger.warning('iOS蓝牙权限状态为permanentlyDenied,这可能是iOS系统误报'); + try { + // 先尝试请求权限 + if (permissionsToRequest.isNotEmpty) { + statuses = await permissionsToRequest.request(); + bool allGranted = true; + statuses.forEach((permission, status) { + if (!status.isGranted) { + allGranted = false; + Logger.w(_tag, 'iOS权限 ${permission.toString()} 未被授予: $status'); + + // 特殊处理蓝牙权限 + if (permission == Permission.bluetooth) { + Logger.w(_tag, 'iOS蓝牙权限未授予,状态: $status'); + } + } + }); + + // 如果有权限未授予,但不是永久拒绝,继续尝试 + if (!allGranted) { + bool hasPermanentlyDenied = statuses.values.any((status) => status.isPermanentlyDenied); + if (hasPermanentlyDenied) { + // 有权限被永久拒绝,需要引导用户去设置 + _showPermissionSettingsDialog('部分权限被永久拒绝,请在设置中启用'); + return false; + } + } + } - // 在iOS上打开蓝牙设置对话框 - Get.dialog( - AlertDialog( - title: Text('需要蓝牙权限'), - content: Text('请在系统设置中手动开启蓝牙权限,以便连接设备'), - actions: [ - TextButton( - onPressed: () => Get.back(), - child: Text('稍后'), - ), - TextButton( - onPressed: () { - Get.back(); - openAppSettings(); - }, - child: Text('前往设置'), - ), - ], - ), - barrierDismissible: false, - ); - return false; - } else if (!status.isGranted) { - // 单独请求蓝牙权限 - PermissionStatus result = await Permission.bluetooth.request(); - if (!result.isGranted) { - Logger.warning('iOS蓝牙权限申请失败: $result'); - return false; + // 直接尝试使用蓝牙功能,这会触发iOS原生权限请求 + bool isEnabled = await _bluetoothService.isBluetoothEnabled(); + Logger.i(_tag, 'iOS蓝牙状态检查: ${isEnabled ? "已开启" : "未开启"}'); + + // 检测AccessorySetupKit支持情况(iOS 18+特性) + bool companionSupported = await _bleService.isCompanionSupported(); + Logger.i(_tag, 'iOS Companion支持检查: ${companionSupported ? "支持" : "不支持"}'); + + if (companionSupported) { + // 针对iOS 18+,尝试预热AccessorySetupKit框架 + try { + // 此调用会触发AccessorySetupKit相关权限请求 + final pairedMac = await _bleService.getPairedMacAddress(); + Logger.i(_tag, '获取已配对设备MAC: ${pairedMac ?? "无"}'); + } catch (e) { + Logger.w(_tag, 'iOS 18+ AccessorySetupKit初始化失败: ${e.toString()}'); + } } + + // 在iOS上视为权限已授予 + _hasPermissions.value = true; + return true; + } catch (e) { + Logger.e(_tag, "iOS蓝牙初始化失败: ${e.toString()}"); + + // 显示自定义对话框引导用户 + _showPermissionSettingsDialog('需要开启蓝牙权限才能配对设备'); + return false; } - - // iOS不再添加到批量请求列表 - // permissionsToRequest.add(Permission.bluetooth); } // 如果还有其他权限需要请求 @@ -247,7 +272,7 @@ class PairingController extends GetxController { statuses.forEach((permission, status) { if (!status.isGranted) { allGranted = false; - Logger.warning('权限 ${permission.toString()} 未被授予: $status'); + Logger.w(_tag, '权限 ${permission.toString()} 未被授予: $status'); } }); @@ -259,6 +284,30 @@ class PairingController extends GetxController { _hasPermissions.value = true; return true; } + + /// 显示权限设置对话框 + void _showPermissionSettingsDialog(String message) { + Get.dialog( + AlertDialog( + title: Text('需要权限'), + content: Text(message), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: Text('稍后'), + ), + TextButton( + onPressed: () { + Get.back(); + openBluetoothSettings(); + }, + child: Text('去设置'), + ), + ], + ), + barrierDismissible: false, + ); + } // 辅助方法获取Android SDK版本 Future _getAndroidSdkVersion() async { @@ -281,14 +330,11 @@ class PairingController extends GetxController { _deviceWorker = ever(_bluetoothService.currentDeviceRx, (BluetoothDevice? device) { if (device != null && !_isNavigatingToHome) { - Logger.info('配对页面发现设备已连接: ${device.name}'); + Logger.i(_tag, '配对页面发现设备已连接: ${device.name}'); _handleDeviceConnected(); } _updateStatusDescription(); }); - if (needdeivice) { - _handleDeviceConnected(); - } } /// 处理设备连接成功事件 @@ -347,9 +393,8 @@ class PairingController extends GetxController { /// 启动Companion设备关联流程 Future _startCompanionAssociation() async { - // 如果不是Android平台,已跳转,已完成关联或正在进行关联,直接返回 - if (!Platform.isAndroid || - _isNavigatingToHome || + // 如果已跳转,已完成关联或正在进行关联,直接返回 + if (_isNavigatingToHome || _companionAssociated.value || _isAssociating || !isDeviceConnected) { @@ -367,7 +412,7 @@ class PairingController extends GetxController { // 检查是否支持Companion功能 final isSupported = await _bleService.isCompanionSupported(); if (!isSupported) { - Logger.info('设备不支持Companion功能,跳过关联'); + Logger.i(_tag, '设备不支持Companion功能,跳过关联'); _companionAssociated.value = true; _autoNavigateToHome(); return; @@ -380,7 +425,7 @@ class PairingController extends GetxController { return; } - Logger.info('开始Companion设备关联流程, 设备MAC: ${currentDevice!.address}'); + Logger.i(_tag, '开始Companion设备关联流程, 设备MAC: ${currentDevice!.address}'); var success = await _bleService.startDeviceAssociation( deviceNamePattern: currentDevice!.name, @@ -389,7 +434,7 @@ class PairingController extends GetxController { _isAssociating = false; if (success) { - Logger.info('Companion设备关联成功'); + Logger.i(_tag, 'Companion设备关联成功'); _companionAssociated.value = true; _userRejectedAssociation.value = false; @@ -400,14 +445,14 @@ class PairingController extends GetxController { // 关联成功后跳转 _autoNavigateToHome(); } else { - Logger.error('Companion设备关联失败或被用户拒绝'); + Logger.e(_tag, 'Companion设备关联失败或被用户拒绝'); // 关联失败 - 用户拒绝关联 _userRejectedAssociation.value = true; // 确保UI状态更新 _updateStatusDescription(); } } catch (e) { - Logger.error('Companion设备关联过程发生错误: ${e.toString()}'); + Logger.e(_tag, 'Companion设备关联过程发生错误: ${e.toString()}'); _isAssociating = false; _userRejectedAssociation.value = true; _updateStatusDescription(); @@ -416,13 +461,13 @@ class PairingController extends GetxController { /// 自动跳转到主页 Future _autoNavigateToHome() async { - if (needdeivice && (_isNavigatingToHome || !isDeviceConnected)) return; + if (_isNavigatingToHome || !isDeviceConnected) return; // 启动BLE后台服务 await _startAgentService(); _isNavigatingToHome = true; - Logger.info('已找到连接的设备 ${currentDevice!.name},自动跳转到主页'); + Logger.i(_tag, '已找到连接的设备 ${currentDevice!.name},自动跳转到主页'); Future.delayed(const Duration(milliseconds: 500), () { Get.offAllNamed(Routes.home); @@ -432,12 +477,13 @@ class PairingController extends GetxController { /// 启动BLE后台服务 Future _startAgentService() async { try { - if (needdeivice && !isDeviceConnected) return; + // 注释掉设备连接检查,确保无论是否连接设备都能启动服务 + // if (!isDeviceConnected) return; final result = await BleManager.to.startAgentService(); - Logger.info(result ? 'BLE后台服务启动成功' : 'BLE后台服务启动失败'); + Logger.i(_tag, result ? 'BLE后台服务启动成功' : 'BLE后台服务启动失败'); } catch (e) { - Logger.error('启动BLE后台服务发生错误: ${e.toString()}'); + Logger.e(_tag, '启动BLE后台服务发生错误: ${e.toString()}'); } } @@ -445,9 +491,9 @@ class PairingController extends GetxController { Future _stopAgentService() async { try { final result = await BleManager.to.stopAgentService(); - Logger.info(result ? 'BLE后台服务停止成功' : 'BLE后台服务停止失败'); + Logger.i(_tag, result ? 'BLE后台服务停止成功' : 'BLE后台服务停止失败'); } catch (e) { - Logger.error('停止BLE后台服务发生错误: ${e.toString()}'); + Logger.e(_tag, '停止BLE后台服务发生错误: ${e.toString()}'); } } @@ -470,15 +516,82 @@ class PairingController extends GetxController { } else if (Platform.isAndroid && !isBluetoothOn) { AppSettings.openAppSettings(type: AppSettingsType.bluetooth); } else if (Platform.isIOS) { - openAppSettings(); + // iOS上优先尝试打开蓝牙设置 + if (!isBluetoothOn) { + // iOS上无法直接打开蓝牙设置,只能打开应用设置 + AppSettings.openAppSettings(); + } else { + // 如果蓝牙开启但有其他权限问题,打开应用设置 + openAppSettings(); + } } else { AppSettings.openAppSettings(type: AppSettingsType.bluetooth); } } catch (e) { - Logger.error('打开设置失败: ${e.toString()}'); + Logger.e(_tag, '打开设置失败: ${e.toString()}'); } } + /// 重新初始化蓝牙 + Future reinitializeBluetooth() async { + try { + isLoading.value = true; + statusDescription.value = '正在重新初始化蓝牙...'; + + // iOS上尝试重置权限状态 + if (Platform.isIOS) { + await Future.delayed(Duration(milliseconds: 500)); + + // 检查蓝牙权限 + PermissionStatus bluetoothStatus = await Permission.bluetooth.status; + Logger.i(_tag, "iOS蓝牙权限当前状态: $bluetoothStatus"); + + if (bluetoothStatus.isPermanentlyDenied) { + Logger.i(_tag, "iOS蓝牙权限处于永久拒绝状态,提示用户手动开启"); + _showPermissionSettingsDialog('蓝牙权限被拒绝,请在设置中手动开启'); + return; + } + + // 尝试重新请求权限 + if (!bluetoothStatus.isGranted) { + bluetoothStatus = await Permission.bluetooth.request(); + Logger.i(_tag, "iOS蓝牙权限请求结果: $bluetoothStatus"); + } + + // 尝试检查蓝牙状态,这会重新触发系统权限弹窗 + try { + bool isEnabled = await _bluetoothService.isBluetoothEnabled(); + Logger.i(_tag, "iOS蓝牙状态: ${isEnabled ? '开启' : '关闭'}"); + } catch (e) { + Logger.e(_tag, "iOS蓝牙状态检查失败: ${e.toString()}"); + } + + // 检查Companion功能(iOS 18+) + try { + bool isSupported = await _bleService.isCompanionSupported(); + if (isSupported) { + Logger.i(_tag, "iOS 18+ Companion功能可用,尝试初始化"); + // 尝试预热AccessorySetupKit + await _bleService.getPairedMacAddress(); + } + } catch (e) { + Logger.w(_tag, "iOS Companion功能检查失败: ${e.toString()}"); + } + } + + // 重新检查权限 + if (await _checkAndRequestPermissions()) { + // 重新检查蓝牙状态 + await _checkBluetoothAndRefresh(); + } + } catch (e) { + Logger.e(_tag, '重新初始化蓝牙失败: ${e.toString()}'); + } finally { + isLoading.value = false; + _updateStatusDescription(); + } + } + /// 打开应用设置 Future openAppSettings() async { try { @@ -488,7 +601,7 @@ class PairingController extends GetxController { reinitializeBluetooth(); }); } catch (e) { - Logger.error('打开应用设置失败: ${e.toString()}'); + Logger.e(_tag, '打开应用设置失败: ${e.toString()}'); } } @@ -501,38 +614,13 @@ class PairingController extends GetxController { /// 跳过连接 void skipConnecting() async { - // 启动BLE后台服务 + // 即使没有设备连接,也启动BLE后台服务 await _startAgentService(); _isNavigatingToHome = true; + + // 记录跳过配对的日志 + Logger.i(_tag, '用户跳过设备配对,直接跳转到主页'); + Get.offAllNamed(Routes.home); } - - /// 重新初始化蓝牙 - Future reinitializeBluetooth() async { - try { - isLoading.value = true; - statusDescription.value = '正在重新初始化蓝牙...'; - - // iOS上尝试重置权限状态 - if (Platform.isIOS) { - await Future.delayed(Duration(milliseconds: 500)); - if (await Permission.bluetooth.isPermanentlyDenied) { - Logger.info("iOS蓝牙权限仍处于永久拒绝状态,尝试直接使用蓝牙功能"); - // 在永久拒绝状态下,尝试使用蓝牙功能,iOS可能会再次弹出系统权限请求 - _bluetoothService.isBluetoothEnabled(); - } - } - - // 重新检查权限 - if (await _checkAndRequestPermissions()) { - // 重新检查蓝牙状态 - await _checkBluetoothAndRefresh(); - } - } catch (e) { - Logger.error('重新初始化蓝牙失败: ${e.toString()}'); - } finally { - isLoading.value = false; - _updateStatusDescription(); - } - } } \ No newline at end of file diff --git a/lib/modules/splash/controllers/splash_controller.dart b/lib/modules/splash/controllers/splash_controller.dart index f03edcf93..e53f146dc 100644 --- a/lib/modules/splash/controllers/splash_controller.dart +++ b/lib/modules/splash/controllers/splash_controller.dart @@ -8,10 +8,6 @@ import '../../../data/models/user_Info.dart'; import '../../../routes/app_routes.dart'; import '../../../core/themes/theme_service.dart'; import '../../../data/services/network/api.dart'; -<<<<<<< HEAD -======= -import 'package:amap_service/amap_service.dart'; ->>>>>>> e77ef1ff0198bb21316fa0e068b78d34f83b013c class SplashController extends GetxController { final GetStorage _storage = GetStorage(); diff --git a/local_plugins/ble_service/LICENSE b/local_plugins/ble_service/LICENSE new file mode 100644 index 000000000..5565c0008 --- /dev/null +++ b/local_plugins/ble_service/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2024 Deep Voice + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/local_plugins/ble_service/ios/Assets/ble_service-Info.plist b/local_plugins/ble_service/ios/Assets/ble_service-Info.plist deleted file mode 100644 index 9ea29bd4c..000000000 --- a/local_plugins/ble_service/ios/Assets/ble_service-Info.plist +++ /dev/null @@ -1,15 +0,0 @@ - - - - - NSBluetoothAlwaysUsageDescription - 需要蓝牙权限与设备进行通信和传输音频数据 - NSBluetoothPeripheralUsageDescription - 需要蓝牙权限与设备进行通信和传输音频数据 - UIBackgroundModes - - bluetooth-central - audio - - - \ No newline at end of file diff --git a/local_plugins/ble_service/ios/Classes/BleAccessorySetup.swift b/local_plugins/ble_service/ios/Classes/BleAccessorySetup.swift new file mode 100644 index 000000000..b4970c3ce --- /dev/null +++ b/local_plugins/ble_service/ios/Classes/BleAccessorySetup.swift @@ -0,0 +1,195 @@ +import Foundation +import CoreBluetooth +import os.log + +// 条件编译只导入在iOS 18+上可用的框架 +#if canImport(AccessorySetupKit) && swift(>=5.9) +import AccessorySetupKit +import UIKit +#endif + +class BleAccessorySetup: NSObject { + // 单例实现 + static let shared = BleAccessorySetup() + + // 基本属性 + private var bleService: Any? + private let logger = OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleAccessorySetup") + + // iOS 18+ 特定属性和回调 + #if canImport(AccessorySetupKit) && swift(>=5.9) + @available(iOS 18.0, *) + class AccessorySessionWrapper { + var session: ASAccessorySession + + init() { + self.session = ASAccessorySession() + } + } + + var accessorySessionWrapper: Any? + + typealias DeviceDiscoveredCallback = (UUID) -> Void + private var onDeviceDiscovered: DeviceDiscoveredCallback? + #endif + + private override init() { + super.init() + } + + // 设置BLE服务实例 + func setBleService(_ service: Any) { + self.bleService = service + } + + // 设置设备发现回调 + func setDeviceDiscoveredCallback(_ callback: @escaping (UUID) -> Void) { + #if canImport(AccessorySetupKit) && swift(>=5.9) + if #available(iOS 18.0, *) { + self.onDeviceDiscovered = callback + } + #endif + } + + // 初始化设备发现会话 + func initializeDiscoverySession() { + #if canImport(AccessorySetupKit) && swift(>=5.9) + if #available(iOS 18.0, *) { + os_log("初始化iOS 18 AccessorySetupKit会话", log: logger, type: .info) + + // 创建会话 + accessorySessionWrapper = AccessorySessionWrapper() + + // 激活会话并设置事件处理 + if let wrapper = accessorySessionWrapper as? AccessorySessionWrapper { + wrapper.session.activate(on: .main) { [weak self] event in + guard let self = self else { return } + + switch event.eventType { + case .activated: + os_log("AccessorySetupKit会话已激活", log: self.logger, type: .info) + + case .accessoryAdded: + if let accessory = event.accessory, let uuid = accessory.bluetoothIdentifier { + os_log("发现新设备: %{public}@", log: self.logger, type: .info, accessory.displayName) + self.onDeviceDiscovered?(uuid) + } + + case .accessoryRemoved: + if let accessory = event.accessory { + os_log("设备已移除: %{public}@", log: self.logger, type: .info, accessory.displayName) + } + + // 根据WWDC24视频,正确的事件类型为accessoryChanged + case .accessoryChanged: + if let accessory = event.accessory { + os_log("设备已更新: %{public}@", log: self.logger, type: .info, accessory.displayName) + } + + // 根据WWDC24视频,失败事件类型已变更 + case .pickerSetupFailed: + if let error = event.error { + os_log("会话出错: %{public}@", log: self.logger, type: .error, error.localizedDescription) + } + + default: + os_log("收到其他会话事件: %{public}d", log: self.logger, type: .debug, event.eventType.rawValue) + } + } + + // 检查现有授权设备 + if !wrapper.session.accessories.isEmpty { + os_log("发现%{public}d个已授权设备", log: logger, type: .info, wrapper.session.accessories.count) + + // 如果有现有设备,触发回调 + for accessory in wrapper.session.accessories { + if let uuid = accessory.bluetoothIdentifier { + os_log("处理已授权设备: %{public}@", log: logger, type: .info, accessory.displayName) + self.onDeviceDiscovered?(uuid) + } + } + } + } + } else { + os_log("iOS 18 AccessorySetupKit不可用", log: logger, type: .info) + } + #else + os_log("iOS 18 AccessorySetupKit不可用", log: logger, type: .info) + #endif + } + + // 显示设备选择器 + func showDevicePicker() { + #if canImport(AccessorySetupKit) && swift(>=5.9) + if #available(iOS 18.0, *) { + if let wrapper = accessorySessionWrapper as? AccessorySessionWrapper { + os_log("显示iOS 18设备选择器", log: logger, type: .info) + + // 创建描述符和选择器项,这些API也需要iOS 18 + @available(iOS 18.0, *) + func createAndShowPicker() { + // 根据WWDC24视频,正确的类名是ASDiscoveryDescriptor + let descriptor = ASDiscoveryDescriptor() + + // 设置过滤条件 + // 使用ASBluetoothCompanyIdentifier类型 + descriptor.bluetoothCompanyIdentifier = ASBluetoothCompanyIdentifier(UInt16(0xABEF)) // BleConst.MANUFACTURER_ID + descriptor.bluetoothServiceUUID = CBUUID(string: "0000abc0-0000-1111-2222-123456789abc") // BleConst.PRIMARY_SERVICE_UUID + + // 创建默认图像以防系统图标不可用 + let headphonesImage = UIImage(systemName: "headphones") ?? UIImage() + + // 根据WWDC24视频,正确的类名是ASPickerDisplayItem + let displayItem = ASPickerDisplayItem( + name: "语音助理耳机", + productImage: headphonesImage, + descriptor: descriptor + ) + + // 显示选择器 + wrapper.session.showPicker(for: [displayItem]) { error in + if let error = error { + // 修复OSLog的错误用法 + os_log("设备选择器显示错误: %{public}@", log: self.logger, type: .error, error.localizedDescription) + } + } + } + + // 调用内部函数 + createAndShowPicker() + } else { + os_log("会话未初始化", log: logger, type: .error) + } + } else { + os_log("iOS 18 AccessorySetupKit不可用", log: logger, type: .info) + } + #else + os_log("iOS 18 AccessorySetupKit不可用", log: logger, type: .info) + #endif + } + + // 处理发现的设备 + func handleDiscoveredAccessory(uuid: UUID) { + #if canImport(AccessorySetupKit) && swift(>=5.9) + if #available(iOS 18.0, *) { + guard let service = bleService as? BleService else { + os_log("BLE服务未设置", log: logger, type: .error) + return + } + + // 获取中央管理器 + let centralManager = service.getCentralManager() + + // 通过UUID检索外设 + let peripherals = centralManager.retrievePeripherals(withIdentifiers: [uuid]) + + if let peripheral = peripherals.first { + os_log("连接到UUID对应的设备: %{public}@", log: logger, type: .info, peripheral.name ?? uuid.uuidString) + service.connect(to: peripheral) + } else { + os_log("未找到UUID对应的设备: %{public}@", log: logger, type: .error, uuid.uuidString) + } + } + #endif + } +} \ No newline at end of file diff --git a/local_plugins/ble_service/ios/Classes/BleConst.swift b/local_plugins/ble_service/ios/Classes/BleConst.swift index 3c551b120..41d31caa7 100644 --- a/local_plugins/ble_service/ios/Classes/BleConst.swift +++ b/local_plugins/ble_service/ios/Classes/BleConst.swift @@ -1,28 +1,30 @@ import Foundation import CoreBluetooth -/// BLE服务常量 -struct BleConst { +/** + * BLE服务常量 + */ +class BleConst { // ================== 协议相关UUID ================== - /// 主服务UUID - 0000ABC0-0000-1111-2222-123456789ABC + /** 主服务UUID - 文档中定义为0000ABC0-0000-1111-2222-123456789ABC */ static let PRIMARY_SERVICE_UUID = CBUUID(string: "0000abc0-0000-1111-2222-123456789abc") - - /// 音频服务UUID - 00001801-0000-1000-8000-00805f9b34fb + + /** 音频服务UUID - 文档中定义为00001801-0000-1000-8000-00805f9b34fb */ static let AUDIO_SERVICE_UUID = CBUUID(string: "0000ae00-0000-1000-8000-00805f9b34fb") - - /// 接收音频特征UUID - 0000ABC1-0000-1111-2222-123456789ABC + + /** 接收音频特征UUID - 文档中定义为0000ABC1-0000-1111-2222-123456789ABC */ static let RECEIVE_AUDIO_CHAR_UUID = CBUUID(string: "0000ae02-0000-1000-8000-00805f9b34fb") - /// 写入特征UUID - 0000ABC1-0000-1111-2222-123456789ABC + /** 写入特征UUID - 文档中定义为0000ABC1-0000-1111-2222-123456789ABC */ static let WRITE_CHAR_UUID = CBUUID(string: "0000abc1-0000-1111-2222-123456789abc") - /// 通知特征UUID - 0000ABC2-0000-1111-2222-123456789ABC + /** 通知特征UUID - 文档中定义为0000ABC2-0000-1111-2222-123456789ABC */ static let NOTIFY_CHAR_UUID = CBUUID(string: "0000abc2-0000-1111-2222-123456789abc") - /// 客户端特征配置描述符UUID - static let CLIENT_CHAR_CONFIG_UUID = CBUUID(string: "0000fd7b-0000-1000-8000-00805f9b34fb") + /** 客户端特征配置描述符UUID */ + static let CLIENT_CHAR_CONFIG_UUID = CBUUID(string: "00002902-0000-1000-8000-00805f9b34fb") - /// BLE扫描Action + /** BLE扫描Action */ static let ACTION_SCAN_RESULT = "com.yunqiinnovation.ble_service.ACTION_BLE_SCAN" // ================== 连接状态 ================== @@ -33,58 +35,59 @@ struct BleConst { static let STATE_ERROR = 4 // ================== 广播厂商信息 ================== - /// 厂商ID - 0xABEF + /** 厂商ID - 文档中定义为0xABEF */ static let MANUFACTURER_ID: UInt16 = 0xABEF - /// 客户ID - 0xA001 + /** 客户ID - 文档中定义为0xA001 */ static let CUSTOMER_ID: UInt16 = 0xA001 - /// 产品ID - 0xA001 + /** 产品ID - 文档中定义为0xA001 */ static let PRODUCT_ID: UInt16 = 0xA001 - /// 蓝牙状态-未连接 + /** 蓝牙状态-未连接 */ static let BT_STATE_DISCONNECTED: UInt8 = 0x00 - /// 蓝牙状态-已连接 + /** 蓝牙状态-已连接 */ static let BT_STATE_CONNECTED: UInt8 = 0x0C // ================== 协议格式 ================== - /// APP请求帧头 + /** APP请求帧头 */ static let APP_REQUEST_HEADER: UInt8 = 0xAA - /// 设备响应帧头 + /** 设备响应帧头 */ static let DEVICE_RESPONSE_HEADER: UInt8 = 0xBB - /// 设备主动上报帧头 + /** 设备主动上报帧头 */ static let DEVICE_NOTIFY_HEADER: UInt8 = 0xCC // ================== 协议指令 (CMD) ================== - /// 获取耳机版本号 + /** 获取耳机版本号 */ static let CMD_GET_VERSION: UInt8 = 0x01 - /// 获取耳机产品ID + /** 获取耳机产品ID */ static let CMD_GET_PRODUCT_ID: UInt8 = 0x02 - /// 获取耳机颜色ID + /** 获取耳机颜色ID */ static let CMD_GET_COLOR_ID: UInt8 = 0x03 - /// 获取耳机电量ID + /** 获取耳机电量ID */ static let CMD_GET_BATTERY_INFO: UInt8 = 0x04 - /// APP控制编解码指令 + /** APP控制编解码指令 */ static let CMD_CONTROL_CODEC: UInt8 = 0x05 - /// 音量增大 + /** 音量增大 */ static let CMD_VOLUME_UP: UInt8 = 0x06 - /// 音量减小 + /** 音量减小 */ static let CMD_VOLUME_DOWN: UInt8 = 0x07 - /// A2DP播放 + /** A2DP播放 */ static let CMD_PLAY_A2DP: UInt8 = 0x08 - /// 唤醒指令 + + /** 唤醒指令 */ static let CMD_WAKE_UP: UInt8 = 0x11 // ================== 编解码控制值 ================== - /// 关闭编解码指令 + /** 关闭编解码指令 */ static let CODEC_CONTROL_CLOSE: UInt8 = 0x00 - /// 打开解码指令 + /** 音乐或者通话远端声音 */ static let CODEC_CONTROL_DECODE_ON: UInt8 = 0xA1 - /// 通过A2DP播放 + /** mic和dac(音乐或者通话远端)声音 */ static let CODEC_CONTROL_A2DP_PLAY: UInt8 = 0xA2 - /// 打开编码指令 + /** 打开编码指令 */ static let CODEC_CONTROL_ENCODE_ON: UInt8 = 0xB1 - /// 左声道 + /** 左声道 */ static let AUDIO_CHANNEL_LEFT: UInt8 = 0x01 - /// 右声道 + /** 右声道 */ static let AUDIO_CHANNEL_RIGHT: UInt8 = 0x02 - /// 立体声 + /** 立体声 */ static let AUDIO_CHANNEL_STEREO: UInt8 = 0x03 } \ No newline at end of file diff --git a/local_plugins/ble_service/ios/Classes/BleDelegate.swift b/local_plugins/ble_service/ios/Classes/BleDelegate.swift deleted file mode 100644 index af104cb20..000000000 --- a/local_plugins/ble_service/ios/Classes/BleDelegate.swift +++ /dev/null @@ -1,25 +0,0 @@ -import Foundation - -/// BLE服务回调代理协议 -protocol BleDelegate: AnyObject { - /// 扫描结果回调 - /// - Parameter devices: 设备列表,每个设备包含id、name、rssi和uuids等信息 - func onScanResult(devices: [[String: Any]]) - - /// 连接状态改变回调 - /// - Parameter state: 连接状态,参考BleConst中的STATE_*常量 - func onConnectionStateChanged(state: Int) - - /// 音频数据接收回调 - /// - Parameter data: 接收到的音频数据 - func onAudioDataReceived(data: Data) - - /// 唤醒信号接收回调 - func onWakeupSignalReceived() - - /// 设备信息接收回调 - /// - Parameters: - /// - infoType: 信息类型 - /// - infoData: 信息数据 - func onDeviceInfoReceived(infoType: Int, infoData: [String: Any]) -} \ No newline at end of file diff --git a/local_plugins/ble_service/ios/Classes/BleService.swift b/local_plugins/ble_service/ios/Classes/BleService.swift index 92f129d9a..a86acc11c 100644 --- a/local_plugins/ble_service/ios/Classes/BleService.swift +++ b/local_plugins/ble_service/ios/Classes/BleService.swift @@ -2,972 +2,455 @@ import Foundation import CoreBluetooth import os.log -/// BLE服务类 - 单例实现 -actor BleService { +@available(iOS 13.0, *) +class BleService: NSObject { // MARK: - 单例实现 static let shared = BleService() - // MARK: - 常量 - - /// 用于状态恢复的唯一标识符 - private let restoreIdentifier = "com.yunqiinnovation.ble_service.restore" - - private init() { - // 初始化连接状态 - connectionState = BleConst.STATE_DISCONNECTED + // MARK: - 回调代理协议 + protocol Callback: AnyObject { + // 扫描相关回调 + func onScanResult(devices: [[String: Any]]) - // 将通知处理移到非actor方法中 - setupNotifications() - } - - deinit { - // 移除通知观察者 - for observer in notificationObservers { - NotificationCenter.default.removeObserver(observer) - } + // 连接状态相关回调 + func onConnectionStateChanged(state: Int) + + // 数据相关回调 + func onAudioDataReceived(data: Data) + + // 设备信息相关回调 + func onDeviceInfoReceived(infoType: Int, infoData: [String: Any]) } // MARK: - 属性 - - /// 中央管理器 - private var centralManager: CBCentralManager? - - /// 当前连接的外设 - private var peripheral: CBPeripheral? - - /// 主服务特征 + private var centralManager: CBCentralManager! + private var peripheralDevice: CBPeripheral? private var writeCharacteristic: CBCharacteristic? private var notifyCharacteristic: CBCharacteristic? private var audioCharacteristic: CBCharacteristic? - /// 连接状态 - private(set) var connectionState: Int { - didSet { - if oldValue != connectionState { - Task { await notifyConnectionStateChanged(state: connectionState) } - } - } - } - - /// 是否正在扫描 - private(set) var isScanning = false - - /// 扫描结果缓存 - private var scanResults = [CBPeripheral: [String: Any]]() + private var isScanning = false + private var delegate: Callback? + private var connectionState = BleConst.STATE_DISCONNECTED - /// 扫描超时任务 - private var scanTimeoutTask: Task? - - /// 是否已初始化 - private var isInitialized = false - - /// 回调代理列表 - 使用actor保证线程安全 - private var delegates = [BleDelegate]() - - /// 日志 private let logger = OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleService") - /// 通知观察者 - private var notificationObservers = [NSObjectProtocol]() - - // MARK: - 通知设置 - - /// 设置应用生命周期通知 - private nonisolated func setupNotifications() { - // 使用nonisolated函数以避免actor隔离问题 - let becomeActiveObserver = NotificationCenter.default.addObserver( - forName: UIApplication.didBecomeActiveNotification, - object: nil, - queue: .main - ) { [weak self] _ in - guard let self = self else { return } - Task { - await self.handleAppDidBecomeActive() - } - } + // MARK: - 初始化 + private override init() { + super.init() - let enterBackgroundObserver = NotificationCenter.default.addObserver( - forName: UIApplication.didEnterBackgroundNotification, - object: nil, - queue: .main - ) { [weak self] _ in - guard let self = self else { return } - Task { - await self.handleAppDidEnterBackground() - } - } + // 初始化中央管理器,提供恢复标识以支持后台恢复 + let options: [String: Any] = [ + CBCentralManagerOptionRestoreIdentifierKey: "BLEAssistantCentral", + CBCentralManagerOptionShowPowerAlertKey: true + ] - // 存储观察者以便后续移除 - notificationObservers = [becomeActiveObserver, enterBackgroundObserver] + centralManager = CBCentralManager(delegate: self, queue: nil, options: options) } // MARK: - 公共方法 - /// 初始化BLE服务 - /// - Returns: 是否初始化成功 - func initialize() -> Bool { - if isInitialized { - return true - } - - do { - // 初始化中央管理器 - 配置状态恢复选项 - let options: [String: Any] = [ - CBCentralManagerOptionRestoreIdentifierKey: restoreIdentifier, - CBCentralManagerOptionShowPowerAlertKey: true - ] - - centralManager = CBCentralManager( - delegate: CBManagerHandler.shared, - queue: nil, - options: options - ) - - CBManagerHandler.shared.bleService = self - - isInitialized = true - os_log("BLE服务初始化成功", log: logger, type: .info) - return true - } catch { - os_log("BLE服务初始化失败: %{public}@", log: logger, type: .error, error.localizedDescription) - return false - } - } - - /// 添加回调代理 - /// - Parameter delegate: 要添加的代理 - /// - Returns: 是否添加成功 - func addDelegate(_ delegate: BleDelegate) -> Bool { - if !delegates.contains(where: { $0 === delegate }) { - delegates.append(delegate) - os_log("已添加BLE回调, 当前回调数: %d", log: logger, type: .debug, delegates.count) - return true - } - return false - } - - /// 移除回调代理 - /// - Parameter delegate: 要移除的代理 - /// - Returns: 是否移除成功 - func removeDelegate(_ delegate: BleDelegate) -> Bool { - if let index = delegates.firstIndex(where: { $0 === delegate }) { - delegates.remove(at: index) - os_log("已移除BLE回调, 当前回调数: %d", log: logger, type: .debug, delegates.count) - return true - } - return false + /// 设置回调代理 + func setDelegate(_ delegate: Callback) { + self.delegate = delegate } - /// 清除所有回调代理 - func clearDelegates() { - delegates.removeAll() - os_log("已清除所有BLE回调", log: logger, type: .debug) + /// 获取中央管理器 + func getCentralManager() -> CBCentralManager { + return centralManager } - /// 检查是否已连接 - /// - Returns: 是否已连接 - func isConnected() -> Bool { - return connectionState == BleConst.STATE_CONNECTED - } - - // MARK: - 扫描方法 - - /// 开始主动扫描BLE设备 - /// - Parameter timeout: 扫描超时时间(秒) - /// - Returns: 是否成功开始扫描 - func startScan(timeout: TimeInterval = 15.0) -> Bool { - if !isInitialized { - os_log("BLE服务未初始化", log: logger, type: .error) - return false + /// 开始扫描BLE设备 + func startScan() { + guard centralManager.state == .poweredOn else { + os_log("蓝牙未打开", log: logger, type: .error) + return } if isScanning { - return false + stopScan() } - guard let manager = centralManager, manager.state == .poweredOn else { - os_log("蓝牙未开启", log: logger, type: .error) - return false - } - - os_log("开始主动扫描BLE设备...", log: logger, type: .info) - unregisterBackgroundScan() // 避免双重扫描 - scanResults.removeAll() - - // 扫描选项 - 允许重复结果,以获取最新的RSSI值 - let options: [String: Any] = [ - CBCentralManagerScanOptionAllowDuplicatesKey: true - ] - - // 设置过滤服务UUID - let scanFilters = [ - // 如果需要过滤,添加CBUUID - // CBUUID(string: BleConst.PRIMARY_SERVICE_UUID.uuidString) - ] - - // 使用过滤器开始扫描 - manager.scanForPeripherals( - withServices: scanFilters.isEmpty ? nil : scanFilters, - options: options - ) - isScanning = true - // 设置扫描超时 - scanTimeoutTask?.cancel() - scanTimeoutTask = Task { [weak self] in - guard let self = self else { return } - - do { - try await Task.sleep(nanoseconds: UInt64(timeout * 1_000_000_000)) - if !Task.isCancelled { - await self.stopScan() - } - } catch { - // 任务被取消,忽略错误 - } - } + // 扫描特定服务的设备 + let serviceUUIDs = [BleConst.PRIMARY_SERVICE_UUID, BleConst.AUDIO_SERVICE_UUID] + centralManager.scanForPeripherals(withServices: serviceUUIDs, options: [ + CBCentralManagerScanOptionAllowDuplicatesKey: false + ]) - return true + os_log("开始扫描BLE设备", log: logger, type: .info) } - /// 停止主动扫描 - /// - Returns: 是否成功停止扫描 - func stopScan() -> Bool { - if !isScanning { - return false + /// 停止扫描 + func stopScan() { + if isScanning { + centralManager.stopScan() + isScanning = false + os_log("停止扫描BLE设备", log: logger, type: .info) } - - os_log("停止主动扫描", log: logger, type: .info) - - centralManager?.stopScan() - scanTimeoutTask?.cancel() - scanTimeoutTask = nil - isScanning = false - - // 重新注册后台扫描,保证唤醒能力 - registerBackgroundScan() - - return true - } - - /// 注册后台扫描 - /// - Returns: 是否成功注册 - func registerBackgroundScan() -> Bool { - // iOS中无法直接实现与Android相同的后台扫描唤醒功能 - os_log("iOS不支持直接的后台扫描唤醒,将使用状态保存和恢复功能", log: logger, type: .info) - return true - } - - /// 注销后台扫描 - /// - Returns: 是否成功注销 - func unregisterBackgroundScan() -> Bool { - // iOS没有直接的后台扫描注销机制 - os_log("iOS不支持直接的后台扫描注销", log: logger, type: .info) - return true } - // MARK: - 连接方法 - - /// 连接到指定地址的BLE设备 - /// - Parameter address: 设备UUID字符串 - func connect(address: String) async { - if connectionState == BleConst.STATE_CONNECTED { - os_log("已经连接中,忽略连接请求", log: logger, type: .warning) - return - } - - guard let central = centralManager, central.state == .poweredOn else { - updateConnectionState(BleConst.STATE_ERROR) + /// 连接到指定设备 + func connect(to peripheral: CBPeripheral) { + if connectionState == BleConst.STATE_CONNECTED || connectionState == BleConst.STATE_CONNECTING { + os_log("已连接或正在连接中", log: logger, type: .info) return } - // 关闭现有连接 - disconnectGatt() - - // 查找对应的外设 - if let uuid = UUID(uuidString: address), - let targetPeripheral = scanResults.keys.first(where: { $0.identifier == uuid }) { - // 如果在扫描结果中找到,直接连接 - os_log("从扫描结果中找到目标设备,开始连接...", log: logger, type: .debug) - await connectToPeripheral(targetPeripheral) - } else { - // 如果没有找到,尝试从已知设备中恢复 - os_log("尝试使用UUID恢复设备连接: %{public}@", log: logger, type: .debug, address) - if let uuid = UUID(uuidString: address) { - let peripherals = central.retrievePeripherals(withIdentifiers: [uuid]) - if let targetPeripheral = peripherals.first { - await connectToPeripheral(targetPeripheral) - } else { - os_log("未找到指定UUID的设备: %{public}@", log: logger, type: .error, address) - updateConnectionState(BleConst.STATE_ERROR) - } - } else { - os_log("无效的UUID格式: %{public}@", log: logger, type: .error, address) - updateConnectionState(BleConst.STATE_ERROR) - } - } - } - - /// 连接到指定外设 - /// - Parameter peripheral: 要连接的外设 - private func connectToPeripheral(_ peripheral: CBPeripheral) async { - self.peripheral = peripheral - peripheral.delegate = CBPeripheralHandler.shared - CBPeripheralHandler.shared.bleService = self updateConnectionState(BleConst.STATE_CONNECTING) - centralManager?.connect(peripheral, options: nil) + + peripheralDevice = peripheral + peripheralDevice?.delegate = self + centralManager.connect(peripheral, options: [ + CBConnectPeripheralOptionNotifyOnConnectionKey: true, + CBConnectPeripheralOptionNotifyOnDisconnectionKey: true, + CBConnectPeripheralOptionNotifyOnNotificationKey: true + ]) } /// 断开当前连接 - func disconnect() async { - guard let peripheral = peripheral, let central = centralManager else { + func disconnect() { + guard let peripheral = peripheralDevice, + connectionState == BleConst.STATE_CONNECTED else { + os_log("未连接设备", log: logger, type: .info) return } updateConnectionState(BleConst.STATE_DISCONNECTING) - central.cancelPeripheralConnection(peripheral) + centralManager.cancelPeripheralConnection(peripheral) } - /// 安全关闭GATT连接 - private func disconnectGatt() { - if let peripheral = peripheral, let central = centralManager { - central.cancelPeripheralConnection(peripheral) - } - - peripheral = nil - writeCharacteristic = nil - notifyCharacteristic = nil - audioCharacteristic = nil - } - - /// 更新连接状态并通知回调 - private func updateConnectionState(_ state: Int) { - connectionState = state + /// 检查是否已连接 + func isConnected() -> Bool { + return connectionState == BleConst.STATE_CONNECTED } - // MARK: - 设备通信方法 + // MARK: - 获取设备信息 /// 获取设备版本号 - /// - Returns: 是否成功发送请求 func getDeviceVersion() -> Bool { return sendCommand(BleConst.CMD_GET_VERSION) } - ///获取产品ID - /// - Returns: 是否成功发送请求 + + /// 获取产品ID func getProductId() -> Bool { return sendCommand(BleConst.CMD_GET_PRODUCT_ID) } /// 获取电池信息 - /// - Returns: 是否成功发送请求 func getBatteryInfo() -> Bool { return sendCommand(BleConst.CMD_GET_BATTERY_INFO) } + // MARK: - 编解码控制 + /// 打开编码器 - /// - Returns: 是否成功发送请求 func openEncoder() -> Bool { - return sendCodecControl(BleConst.CODEC_CONTROL_ENCODE_ON) + return sendCommandWithParam(BleConst.CMD_CONTROL_CODEC, param: BleConst.CODEC_CONTROL_ENCODE_ON) } /// 打开解码器 - /// - Returns: 是否成功发送请求 func openDecoder() -> Bool { - return sendCodecControl(BleConst.CODEC_CONTROL_DECODE_ON) + return sendCommandWithParam(BleConst.CMD_CONTROL_CODEC, param: BleConst.CODEC_CONTROL_DECODE_ON) } - /// 打开解码器 - /// - Returns: 是否成功发送请求 + + /// 打开A2DP解码器 func openA2DPDecoder() -> Bool { - return sendCodecControl(BleConst.CODEC_CONTROL_A2DP_PLAY) + return sendCommandWithParam(BleConst.CMD_CONTROL_CODEC, param: BleConst.CODEC_CONTROL_A2DP_PLAY) } /// 关闭编解码器 - /// - Returns: 是否成功发送请求 func closeCodec() -> Bool { - return sendCodecControl(BleConst.CODEC_CONTROL_CLOSE) + return sendCommandWithParam(BleConst.CMD_CONTROL_CODEC, param: BleConst.CODEC_CONTROL_CLOSE) } /// 播放A2DP - /// - Returns: 是否成功发送请求 func playA2DP() -> Bool { - return sendCodecControl(BleConst.CODEC_CONTROL_A2DP_PLAY) + return sendCommand(BleConst.CMD_PLAY_A2DP) } - /// 发送编解码控制指令 - /// - Parameter control: 控制值 - /// - Returns: 是否成功发送 - private func sendCodecControl(_ control: UInt8) -> Bool { - let data = Data([BleConst.APP_REQUEST_HEADER, BleConst.CMD_CONTROL_CODEC, control]) - return writeData(data) - } + // MARK: - 私有方法 - /// 发送命令 - /// - Parameter command: 命令字 - /// - Returns: 是否成功发送 - private func sendCommand(_ command: UInt8) -> Bool { - let data = Data([BleConst.APP_REQUEST_HEADER, command]) - return writeData(data) + /// 更新连接状态并通知代理 + private func updateConnectionState(_ state: Int) { + connectionState = state + delegate?.onConnectionStateChanged(state: state) } - /// 写入数据 - /// - Parameter data: 要写入的数据 - /// - Returns: 是否成功写入 - private func writeData(_ data: Data) -> Bool { - guard let peripheral = peripheral, - let characteristic = writeCharacteristic, + /// 向设备发送命令(无参数) + private func sendCommand(_ cmd: UInt8) -> Bool { + guard let characteristic = writeCharacteristic, connectionState == BleConst.STATE_CONNECTED else { - os_log("无法写入数据,设备未连接或特征未找到", log: logger, type: .error) + os_log("设备未连接或写特征未找到", log: logger, type: .error) return false } - let hexString = data.map { String(format: "0x%02X", $0) }.joined(separator: " ") - os_log("写入数据: %{public}@", log: logger, type: .debug, hexString) + // 构造帧: 帧头 + 命令ID + 长度(0) + 校验和 + let frameData = Data([BleConst.APP_REQUEST_HEADER, cmd, 0x00, BleConst.APP_REQUEST_HEADER ^ cmd ^ 0x00]) - peripheral.writeValue(data, for: characteristic, type: .withoutResponse) + peripheralDevice?.writeValue(frameData, for: characteristic, type: .withResponse) return true } - // MARK: - 数据处理方法 - - /// 处理音频数据 - /// - Parameter data: 音频数据 - func processAudioData(_ data: Data) async { - os_log("收到音频数据: %d字节", log: logger, type: .debug, data.count) - await notifyAudioDataReceived(data: data) + /// 向设备发送带参数的命令 + private func sendCommandWithParam(_ cmd: UInt8, param: UInt8) -> Bool { + guard let characteristic = writeCharacteristic, + connectionState == BleConst.STATE_CONNECTED else { + os_log("设备未连接或写特征未找到", log: logger, type: .error) + return false + } + + // 构造帧: 帧头 + 命令ID + 长度(1) + 参数 + 校验和 + let frameData = Data([BleConst.APP_REQUEST_HEADER, cmd, 0x01, param, BleConst.APP_REQUEST_HEADER ^ cmd ^ 0x01 ^ param]) + + peripheralDevice?.writeValue(frameData, for: characteristic, type: .withResponse) + return true } - /// 处理设备响应 - /// - Parameter data: 设备响应数据 - func processDeviceResponse(_ data: Data) async { - guard data.count >= 2 else { return } + /// 处理接收到的响应数据 + private func handleResponseData(_ data: Data) { + guard data.count >= 4 else { + os_log("接收数据格式错误", log: logger, type: .error) + return + } + // 提取帧信息 + let header = data[0] let cmd = data[1] - os_log("收到设备响应: CMD=0x%02X", log: logger, type: .debug, cmd) + let length = data[2] + // 检查帧头,确保是设备响应 + guard header == BleConst.DEVICE_RESPONSE_HEADER else { + // 如果是设备主动上报 + if header == BleConst.DEVICE_NOTIFY_HEADER { + handleNotificationData(data) + } + return + } + + // 根据命令类型处理设备响应 switch cmd { case BleConst.CMD_GET_VERSION: - if data.count >= 3 { - // 解析版本号 - let version = String(format: "%d.%d.%d", data[2], data.count > 3 ? data[3] : 0, data.count > 4 ? data[4] : 0) - let infoData: [String: Any] = ["version": version] - await notifyDeviceInfoReceived(infoType: Int(cmd), infoData: infoData) + if length > 0 && data.count >= 4 + Int(length) { + let versionData = data.subdata(in: 3..<(3 + Int(length))) + if let versionStr = String(data: versionData, encoding: .utf8) { + delegate?.onDeviceInfoReceived(infoType: Int(BleConst.CMD_GET_VERSION), + infoData: ["version": versionStr]) + } } + + case BleConst.CMD_GET_PRODUCT_ID: + if length >= 2 && data.count >= 5 { + let productId = UInt16(data[3]) << 8 | UInt16(data[4]) + delegate?.onDeviceInfoReceived(infoType: Int(BleConst.CMD_GET_PRODUCT_ID), + infoData: ["productId": productId]) + } + case BleConst.CMD_GET_BATTERY_INFO: - if data.count >= 3 { - // 解析电池电量 - let batteryLevel = Int(data[2]) - let infoData: [String: Any] = ["level": batteryLevel] - await notifyDeviceInfoReceived(infoType: Int(cmd), infoData: infoData) + if length >= 1 && data.count >= 4 { + let batteryLevel = Int(data[3]) + delegate?.onDeviceInfoReceived(infoType: Int(BleConst.CMD_GET_BATTERY_INFO), + infoData: ["batteryLevel": batteryLevel]) } + default: - os_log("未处理的响应命令: 0x%02X", log: logger, type: .debug, cmd) + os_log("未知的响应命令: %{public}d", log: logger, type: .debug, cmd) } } - /// 处理设备通知 - /// - Parameter data: 设备通知数据 - func processDeviceNotification(_ data: Data) async { - guard data.count >= 2 else { return } + /// 处理设备主动上报的通知数据 + private func handleNotificationData(_ data: Data) { + guard data.count >= 4 else { return } - let notifyType = data[1] - os_log("收到设备通知: Type=0x%02X", log: logger, type: .debug, notifyType) + let cmd = data[1] + let length = data[2] - switch notifyType { + switch cmd { case BleConst.CMD_WAKE_UP: - // 设备唤醒信号 - await notifyWakeupSignalReceived() + // 处理唤醒指令 + os_log("收到设备唤醒指令", log: logger, type: .info) + // 可以在这里添加唤醒回调 + default: - os_log("未处理的通知类型: 0x%02X", log: logger, type: .debug, notifyType) + os_log("未知的通知命令: %{public}d", log: logger, type: .debug, cmd) } } - - // MARK: - 应用状态处理 - - /// 处理应用进入活跃状态 - private func handleAppDidBecomeActive() async { - os_log("应用进入活跃状态", log: logger, type: .debug) - // 重新初始化蓝牙管理器,如果需要 - if centralManager == nil { - _ = initialize() +} + +// MARK: - CBCentralManagerDelegate +@available(iOS 13.0, *) +extension BleService: CBCentralManagerDelegate { + func centralManagerDidUpdateState(_ central: CBCentralManager) { + switch central.state { + case .poweredOn: + os_log("蓝牙已开启", log: logger, type: .info) + // 蓝牙已开启,可以开始扫描 + + case .poweredOff: + os_log("蓝牙已关闭", log: logger, type: .info) + updateConnectionState(BleConst.STATE_DISCONNECTED) + + case .resetting: + os_log("蓝牙正在重置", log: logger, type: .info) + + case .unauthorized: + os_log("蓝牙未授权", log: logger, type: .error) + + case .unsupported: + os_log("设备不支持蓝牙", log: logger, type: .error) + + case .unknown: + os_log("蓝牙状态未知", log: logger, type: .info) + + @unknown default: + os_log("蓝牙状态未定义", log: logger, type: .info) } } - /// 处理应用进入后台 - private func handleAppDidEnterBackground() async { - os_log("应用进入后台", log: logger, type: .debug) - // 在iOS中,后台蓝牙操作有一定限制,这里可以做一些清理工作 - } - - // MARK: - 回调通知方法 - - /// 通知扫描结果 - /// - Parameter devices: 设备列表 - func notifyScanResult(devices: [[String: Any]]) async { - let currentDelegates = delegates + func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) { + // 将扫描结果转换为字典格式 + var deviceInfo: [String: Any] = [ + "name": peripheral.name ?? "Unknown", + "id": peripheral.identifier.uuidString, + "rssi": RSSI.intValue + ] - for delegate in currentDelegates { - await MainActor.run { - delegate.onScanResult(devices: devices) - } + // 添加广播数据 + if let manufacturerData = advertisementData[CBAdvertisementDataManufacturerDataKey] as? Data { + deviceInfo["manufacturerData"] = manufacturerData } - } - - /// 通知连接状态变化 - /// - Parameter state: 连接状态 - func notifyConnectionStateChanged(state: Int) async { - let currentDelegates = delegates - for delegate in currentDelegates { - await MainActor.run { - delegate.onConnectionStateChanged(state: state) - } + if let serviceUUIDs = advertisementData[CBAdvertisementDataServiceUUIDsKey] as? [CBUUID] { + deviceInfo["serviceUUIDs"] = serviceUUIDs.map { $0.uuidString } } - } - - /// 通知音频数据接收 - /// - Parameter data: 音频数据 - func notifyAudioDataReceived(data: Data) async { - let currentDelegates = delegates - for delegate in currentDelegates { - await MainActor.run { - delegate.onAudioDataReceived(data: data) - } - } + // 报告给回调 + delegate?.onScanResult(devices: [deviceInfo]) } - /// 通知唤醒信号接收 - func notifyWakeupSignalReceived() async { - let currentDelegates = delegates + func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) { + os_log("已连接到设备: %{public}@", log: logger, type: .info, peripheral.name ?? "Unknown") - for delegate in currentDelegates { - await MainActor.run { - delegate.onWakeupSignalReceived() - } - } - } - - /// 通知设备信息接收 - /// - Parameters: - /// - infoType: 信息类型 - /// - infoData: 信息数据 - func notifyDeviceInfoReceived(infoType: Int, infoData: [String: Any]) async { - let currentDelegates = delegates + updateConnectionState(BleConst.STATE_CONNECTED) - for delegate in currentDelegates { - await MainActor.run { - delegate.onDeviceInfoReceived(infoType: infoType, infoData: infoData) - } - } + // 查找服务 + peripheral.discoverServices([BleConst.PRIMARY_SERVICE_UUID, BleConst.AUDIO_SERVICE_UUID]) } - /// 处理扫描到的设备 - func handleDiscoveredPeripheral(_ peripheral: CBPeripheral, advertisementData: [String: Any], rssi: NSNumber) async { - // 打印外设信息 - os_log("扫描到设备: 名称=%{public}@, ID=%{public}@, RSSI=%d", log: logger, type: .debug, - peripheral.name ?? "未知", peripheral.identifier.uuidString, rssi.intValue) - - // 提取服务UUID - var uuids: [String] = [] - if let serviceUUIDs = advertisementData[CBAdvertisementDataServiceUUIDsKey] as? [CBUUID] { - uuids = serviceUUIDs.map { $0.uuidString } - } - - // 创建设备信息字典 - let deviceInfo: [String: Any] = [ - "id": peripheral.identifier.uuidString, - "name": peripheral.name ?? "未知", - "rssi": rssi.intValue, - "uuids": uuids - ] - - // 更新扫描缓存 - scanResults[peripheral] = deviceInfo + func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) { + os_log("连接失败: %{public}@", log: logger, type: .error, error?.localizedDescription ?? "Unknown error") - // 通知扫描结果 - await notifyScanResult(devices: [deviceInfo]) + updateConnectionState(BleConst.STATE_ERROR) } - /// 处理连接结果 - func handleConnectionResult(peripheral: CBPeripheral, success: Bool, error: Error?) async { - if success { - os_log("已连接到设备: %{public}@", log: logger, type: .info, peripheral.identifier.uuidString) - updateConnectionState(BleConst.STATE_CONNECTED) - - // 开始服务发现 - peripheral.discoverServices([BleConst.PRIMARY_SERVICE_UUID, BleConst.AUDIO_SERVICE_UUID]) - } else { - os_log("连接设备失败: %{public}@", log: logger, type: .error, error?.localizedDescription ?? "未知错误") - updateConnectionState(BleConst.STATE_ERROR) - - // 清理资源 - self.peripheral = nil - } + func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) { + os_log("设备断开连接: %{public}@", log: logger, type: .info, error?.localizedDescription ?? "Normal disconnection") + + updateConnectionState(BleConst.STATE_DISCONNECTED) } - /// 处理断开连接 - func handleDisconnection(peripheral: CBPeripheral, error: Error?) async { - if let error = error { - os_log("设备连接断开(错误): %{public}@", log: logger, type: .error, error.localizedDescription) - updateConnectionState(BleConst.STATE_ERROR) - } else { - os_log("设备连接断开(正常)", log: logger, type: .info) - updateConnectionState(BleConst.STATE_DISCONNECTED) - } + func centralManager(_ central: CBCentralManager, willRestoreState dict: [String : Any]) { + os_log("恢复先前的连接状态", log: logger, type: .info) - // 清理资源 - if self.peripheral?.identifier == peripheral.identifier { - self.peripheral = nil - self.writeCharacteristic = nil - self.notifyCharacteristic = nil - self.audioCharacteristic = nil + // 从恢复状态中获取之前连接的外设 + if let peripherals = dict[CBCentralManagerRestoredStatePeripheralsKey] as? [CBPeripheral], + let peripheral = peripherals.first { + peripheralDevice = peripheral + peripheralDevice?.delegate = self + + // 如果之前已连接,更新状态 + if peripheral.state == .connected { + updateConnectionState(BleConst.STATE_CONNECTED) + // 重新发现服务 + peripheral.discoverServices([BleConst.PRIMARY_SERVICE_UUID, BleConst.AUDIO_SERVICE_UUID]) + } else { + // 如果之前未连接,尝试重新连接 + central.connect(peripheral, options: nil) + } } } - - /// 处理发现服务 - func handleDiscoveredServices(peripheral: CBPeripheral, error: Error?) async { +} + +// MARK: - CBPeripheralDelegate +@available(iOS 13.0, *) +extension BleService: CBPeripheralDelegate { + func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) { if let error = error { - os_log("发现服务失败: %{public}@", log: logger, type: .error, error.localizedDescription) - updateConnectionState(BleConst.STATE_ERROR) + os_log("服务发现错误: %{public}@", log: logger, type: .error, error.localizedDescription) return } - guard let services = peripheral.services else { - os_log("没有发现任何服务", log: logger, type: .error) - updateConnectionState(BleConst.STATE_ERROR) - return - } + guard let services = peripheral.services else { return } - // 发现所有服务的特征 for service in services { os_log("发现服务: %{public}@", log: logger, type: .debug, service.uuid.uuidString) - peripheral.discoverCharacteristics(nil, for: service) + + if service.uuid == BleConst.PRIMARY_SERVICE_UUID { + // 主服务,发现写入和通知特征 + peripheral.discoverCharacteristics([BleConst.WRITE_CHAR_UUID, BleConst.NOTIFY_CHAR_UUID], for: service) + } else if service.uuid == BleConst.AUDIO_SERVICE_UUID { + // 音频服务,发现音频特征 + peripheral.discoverCharacteristics([BleConst.RECEIVE_AUDIO_CHAR_UUID], for: service) + } } } - /// 处理发现的特征 - func handleDiscoveredCharacteristics(for service: CBService, peripheral: CBPeripheral, error: Error?) async { + func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) { if let error = error { - os_log("发现特征失败: %{public}@", log: logger, type: .error, error.localizedDescription) + os_log("特征发现错误: %{public}@", log: logger, type: .error, error.localizedDescription) return } - guard let characteristics = service.characteristics else { - os_log("服务没有任何特征: %{public}@", log: logger, type: .warning, service.uuid.uuidString) - return - } + guard let characteristics = service.characteristics else { return } - // 根据UUID识别和保存特征 for characteristic in characteristics { os_log("发现特征: %{public}@", log: logger, type: .debug, characteristic.uuid.uuidString) - // 主服务特征 - if service.uuid == BleConst.PRIMARY_SERVICE_UUID { - if characteristic.uuid == BleConst.WRITE_CHAR_UUID { - writeCharacteristic = characteristic - os_log("找到写入特征", log: logger, type: .debug) - } else if characteristic.uuid == BleConst.NOTIFY_CHAR_UUID { - notifyCharacteristic = characteristic - os_log("找到通知特征", log: logger, type: .debug) - // 设置通知 - await setupNotification(for: characteristic, peripheral: peripheral) - } - } - - // 音频服务特征 - if service.uuid == BleConst.AUDIO_SERVICE_UUID { - if characteristic.uuid == BleConst.RECEIVE_AUDIO_CHAR_UUID { - audioCharacteristic = characteristic - os_log("找到音频特征", log: logger, type: .debug) - // 设置通知 - await setupNotification(for: characteristic, peripheral: peripheral) - } + if characteristic.uuid == BleConst.WRITE_CHAR_UUID { + writeCharacteristic = characteristic + } else if characteristic.uuid == BleConst.NOTIFY_CHAR_UUID { + notifyCharacteristic = characteristic + // 订阅通知 + peripheral.setNotifyValue(true, for: characteristic) + } else if characteristic.uuid == BleConst.RECEIVE_AUDIO_CHAR_UUID { + audioCharacteristic = characteristic + // 订阅音频通知 + peripheral.setNotifyValue(true, for: characteristic) } } - - // 检查是否找到所有必要的特征 - if writeCharacteristic == nil || notifyCharacteristic == nil { - os_log("未找到所有必要的特征", log: logger, type: .error) - updateConnectionState(BleConst.STATE_ERROR) - } } - /// 设置特征的通知 - /// - Parameters: - /// - characteristic: 特征 - /// - peripheral: 外设 - private func setupNotification(for characteristic: CBCharacteristic, peripheral: CBPeripheral) async { - if characteristic.properties.contains(.notify) { - peripheral.setNotifyValue(true, for: characteristic) - } - } - - /// 处理特征值更新 - func handleValueUpdate(for characteristic: CBCharacteristic, peripheral: CBPeripheral, error: Error?) async { + func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { if let error = error { - os_log("特征值更新失败: %{public}@", log: logger, type: .error, error.localizedDescription) + os_log("接收数据错误: %{public}@", log: logger, type: .error, error.localizedDescription) return } - guard let data = characteristic.value else { - os_log("特征没有值", log: logger, type: .warning) - return - } + guard let data = characteristic.value, !data.isEmpty else { return } - let hexString = data.map { String(format: "%02X", $0) }.joined(separator: " ") - os_log("收到特征数据: %{public}@ - %{public}@", log: logger, type: .debug, characteristic.uuid.uuidString, hexString) - - // 根据特征UUID处理不同类型的数据 - if characteristic.uuid == BleConst.RECEIVE_AUDIO_CHAR_UUID { - // 音频数据处理 - await processAudioData(data) - } else if characteristic.uuid == BleConst.NOTIFY_CHAR_UUID { - // 通知特征处理 - if !data.isEmpty { - switch data[0] { - case BleConst.DEVICE_RESPONSE_HEADER: - // 设备响应 - await processDeviceResponse(data) - case BleConst.DEVICE_NOTIFY_HEADER: - // 设备通知 - await processDeviceNotification(data) - default: - os_log("未知数据类型: %d", log: logger, type: .debug, data[0]) - } - } - } - } - - // MARK: - 状态恢复处理 - - /// 处理蓝牙状态恢复 - func handleStateRestoration(dict: [String: Any]) async { - os_log("正在恢复蓝牙状态...", log: logger, type: .info) - - // 1. 恢复之前连接的外设 - if let peripherals = dict[CBCentralManagerRestoredStatePeripheralsKey] as? [CBPeripheral] { - os_log("恢复 %d 个已连接的外设", log: logger, type: .debug, peripherals.count) - - for peripheral in peripherals { - self.peripheral = peripheral - peripheral.delegate = CBPeripheralHandler.shared - CBPeripheralHandler.shared.bleService = self - - // 根据外设当前状态采取不同操作 - if peripheral.state == .connected { - os_log("外设 %{public}@ 已连接", log: logger, type: .debug, peripheral.identifier.uuidString) - updateConnectionState(BleConst.STATE_CONNECTED) - - // 重新发现服务 - peripheral.discoverServices([BleConst.PRIMARY_SERVICE_UUID, BleConst.AUDIO_SERVICE_UUID]) - } else if peripheral.state == .connecting { - os_log("外设 %{public}@ 正在连接中", log: logger, type: .debug, peripheral.identifier.uuidString) - updateConnectionState(BleConst.STATE_CONNECTING) - } else { - os_log("外设 %{public}@ 状态异常: %d", log: logger, type: .debug, peripheral.identifier.uuidString, peripheral.state.rawValue) - // 尝试重新连接 - updateConnectionState(BleConst.STATE_CONNECTING) - centralManager?.connect(peripheral, options: nil) - } - } - } - - // 2. 恢复扫描状态 - if let scanServices = dict[CBCentralManagerRestoredStateScanServicesKey] as? [CBUUID] { - os_log("恢复扫描任务,服务UUID数: %d", log: logger, type: .debug, scanServices.count) - - let scanOptions = dict[CBCentralManagerRestoredStateScanOptionsKey] as? [String: Any] ?? [ - CBCentralManagerScanOptionAllowDuplicatesKey: true - ] - - // 重新启动扫描任务 - centralManager?.scanForPeripherals( - withServices: scanServices.isEmpty ? nil : scanServices, - options: scanOptions - ) - - isScanning = true - - // 设置新的扫描超时(默认15秒) - scanTimeoutTask?.cancel() - scanTimeoutTask = Task { [weak self] in - guard let self = self else { return } - - do { - try await Task.sleep(nanoseconds: UInt64(15.0 * 1_000_000_000)) - if !Task.isCancelled { - await self.stopScan() - } - } catch { - // 任务被取消,忽略错误 - } - } - } - - // 通知外部:BLE状态已恢复 - await notifyStateRestored() - } - - /// 通知状态恢复 - private func notifyStateRestored() async { - let currentDelegates = delegates - - for delegate in currentDelegates { - if let stateDelegate = delegate as? BleStateRestorationDelegate { - await MainActor.run { - stateDelegate.onBleStateRestored() - } - } - } - } -} - -// MARK: - 中央管理器委托处理器 - -class CBManagerHandler: NSObject, CBCentralManagerDelegate { - static let shared = CBManagerHandler() - weak var bleService: BleService? - - func centralManagerDidUpdateState(_ central: CBCentralManager) { - Task { - guard let service = bleService else { return } - - switch central.state { - case .poweredOn: - await MainActor.run { - os_log("蓝牙已开启", log: OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleService"), type: .info) - } - case .poweredOff: - await MainActor.run { - os_log("蓝牙已关闭", log: OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleService"), type: .warning) - } - await service.updateConnectionState(BleConst.STATE_DISCONNECTED) - case .unauthorized, .unsupported: - await MainActor.run { - os_log("蓝牙不可用: %d", log: OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleService"), type: .error, central.state.rawValue) - } - await service.updateConnectionState(BleConst.STATE_ERROR) - default: - await MainActor.run { - os_log("蓝牙状态变化: %d", log: OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleService"), type: .debug, central.state.rawValue) - } - } - } - } - - func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) { - Task { - await bleService?.handleDiscoveredPeripheral(peripheral, advertisementData: advertisementData, rssi: RSSI) - } - } - - func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) { - Task { - await bleService?.handleConnectionResult(peripheral: peripheral, success: true, error: nil) - } - } - - func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) { - Task { - await bleService?.handleConnectionResult(peripheral: peripheral, success: false, error: error) - } - } - - func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) { - Task { - await bleService?.handleDisconnection(peripheral: peripheral, error: error) - } - } - - /// 状态恢复处理 - func centralManager(_ central: CBCentralManager, willRestoreState dict: [String : Any]) { - os_log("蓝牙状态恢复被触发", log: OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleService"), type: .info) - - // 打印恢复信息 - if let peripherals = dict[CBCentralManagerRestoredStatePeripheralsKey] as? [CBPeripheral] { - os_log("恢复外设数量: %d", log: OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleService"), type: .debug, peripherals.count) - for (i, peripheral) in peripherals.enumerated() { - os_log("恢复外设[%d]: %{public}@", log: OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleService"), type: .debug, i, peripheral.identifier.uuidString) - } - } - - if let services = dict[CBCentralManagerRestoredStateScanServicesKey] as? [CBUUID] { - os_log("恢复扫描服务数量: %d", log: OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleService"), type: .debug, services.count) - } - - // 异步处理状态恢复 - Task { - if let service = bleService { - await service.handleStateRestoration(dict: dict) - } - } - } -} - -// MARK: - 外设委托处理器 - -class CBPeripheralHandler: NSObject, CBPeripheralDelegate { - static let shared = CBPeripheralHandler() - weak var bleService: BleService? - - func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) { - Task { - await bleService?.handleDiscoveredServices(peripheral: peripheral, error: error) - } - } - - func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) { - Task { - await bleService?.handleDiscoveredCharacteristics(for: service, peripheral: peripheral, error: error) - } - } - - func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { - Task { - await bleService?.handleValueUpdate(for: characteristic, peripheral: peripheral, error: error) + if characteristic.uuid == BleConst.NOTIFY_CHAR_UUID { + // 处理通知数据 + handleResponseData(data) + } else if characteristic.uuid == BleConst.RECEIVE_AUDIO_CHAR_UUID { + // 处理音频数据 + delegate?.onAudioDataReceived(data: data) } } func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) { if let error = error { - os_log("写入特征值失败: %{public}@", log: OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleService"), type: .error, error.localizedDescription) - } else { - os_log("写入特征值成功", log: OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleService"), type: .debug) + os_log("写入数据错误: %{public}@", log: logger, type: .error, error.localizedDescription) } } func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) { if let error = error { - os_log("设置通知状态失败: %{public}@", log: OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleService"), type: .error, error.localizedDescription) - } else { - os_log("设置通知状态成功: %{public}@", log: OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleService"), type: .debug, characteristic.uuid.uuidString) + os_log("订阅通知错误: %{public}@", log: logger, type: .error, error.localizedDescription) + return } + + os_log("特征 %{public}@ 的通知状态更新为 %{public}d", log: logger, type: .debug, + characteristic.uuid.uuidString, characteristic.isNotifying ? 1 : 0) } -} - -// MARK: - BLE状态恢复代理 -/// 蓝牙状态恢复代理 -protocol BleStateRestorationDelegate: AnyObject { - /// 当蓝牙状态被系统恢复时调用 - func onBleStateRestored() -} - -// MARK: - BLE代理 -/// 蓝牙事件代理 -protocol BleDelegate: AnyObject { - /// 扫描结果回调 - func onScanResult(devices: [[String: Any]]) - - /// 连接状态变化回调 - func onConnectionStateChanged(state: Int) - - /// 音频数据接收回调 - func onAudioDataReceived(data: Data) - - /// 唤醒信号接收回调 - func onWakeupSignalReceived() - - /// 设备信息接收回调 - func onDeviceInfoReceived(infoType: Int, infoData: [String: Any]) } \ No newline at end of file diff --git a/local_plugins/ble_service/ios/Classes/SwiftBleServicePlugin.swift b/local_plugins/ble_service/ios/Classes/SwiftBleServicePlugin.swift index e3004c43c..388e3bd92 100644 --- a/local_plugins/ble_service/ios/Classes/SwiftBleServicePlugin.swift +++ b/local_plugins/ble_service/ios/Classes/SwiftBleServicePlugin.swift @@ -1,140 +1,257 @@ import Flutter import UIKit +import CoreBluetooth +import os -public class SwiftBleServicePlugin: NSObject, FlutterPlugin, BleDelegate { - private let TAG = "BleServicePlugin" - - // 方法通道 - private var methodChannel: FlutterMethodChannel? - - // 事件通道 +@available(iOS 13.0, *) +public class SwiftBleServicePlugin: NSObject, FlutterPlugin { + // MARK: - 属性 + private var methodChannel: FlutterMethodChannel private var dataEventSink: FlutterEventSink? private var statusEventSink: FlutterEventSink? - // BLE服务实例 - private let bleService = BleService.shared - - public static func register(with registrar: FlutterPluginRegistrar) { - let methodChannel = FlutterMethodChannel(name: "com.yunqiinnovation.ble_service", binaryMessenger: registrar.messenger()) - - // 创建插件实例 - let instance = SwiftBleServicePlugin() - instance.methodChannel = methodChannel - - // 设置方法通道处理器 - registrar.addMethodCallDelegate(instance, channel: methodChannel) - - // 设置事件通道 - instance.setupEventChannels(with: registrar) + // MARK: - 初始化方法 + init(_ channel: FlutterMethodChannel) { + self.methodChannel = channel + super.init() - // 初始化BLE服务 - _ = instance.bleService.initialize() + // 设置BLE服务回调 + BleService.shared.setDelegate(self) - // 添加自身作为BLE回调 - _ = instance.bleService.addDelegate(instance) + // 初始化AccessorySetup + BleAccessorySetup.shared.setBleService(BleService.shared) + BleAccessorySetup.shared.setDeviceDiscoveredCallback { [weak self] uuid in + self?.handleDiscoveredDevice(uuid: uuid) + } + BleAccessorySetup.shared.initializeDiscoverySession() } - /// 设置事件通道 - private func setupEventChannels(with registrar: FlutterPluginRegistrar) { - // 数据接收事件通道 - let dataEventChannel = FlutterEventChannel(name: "com.yunqiinnovation.ble_service/data_events", binaryMessenger: registrar.messenger()) - dataEventChannel.setStreamHandler(StreamHandler { [weak self] sink in - self?.dataEventSink = sink - } onCancel: { [weak self] in - self?.dataEventSink = nil - }) + // MARK: - FlutterPlugin Protocol + public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel(name: "com.yunqiinnovation.ble_service", binaryMessenger: registrar.messenger()) + let instance = SwiftBleServicePlugin(channel) + registrar.addMethodCallDelegate(instance, channel: channel) - // 状态更新事件通道 + // 注册事件通道 + let dataEventChannel = FlutterEventChannel(name: "com.yunqiinnovation.ble_service/data_events", binaryMessenger: registrar.messenger()) let statusEventChannel = FlutterEventChannel(name: "com.yunqiinnovation.ble_service/status_events", binaryMessenger: registrar.messenger()) - statusEventChannel.setStreamHandler(StreamHandler { [weak self] sink in - self?.statusEventSink = sink - } onCancel: { [weak self] in - self?.statusEventSink = nil - }) - } - - /// 发送事件到Flutter - private func sendEvent(_ eventSink: FlutterEventSink?, _ data: Any) { - DispatchQueue.main.async { - eventSink?(data) - } + + dataEventChannel.setStreamHandler(instance) + statusEventChannel.setStreamHandler(instance) } public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { - NSLog("Method call: \(call.method)") - switch call.method { case "isConnected": - result(bleService.isConnected()) + result(BleService.shared.isConnected()) - case "getBatteryInfo": - result(bleService.getBatteryInfo()) + case "getBatteryLevel": + // 获取电池信息,通过回调返回结果 + if BleService.shared.getBatteryInfo() { + result(0) // 通过回调返回实际值 + } else { + result(FlutterError(code: "UNAVAILABLE", + message: "无法获取电池信息", + details: nil)) + } + + case "isCompanionSupported": + // 检查设备是否支持iOS 18的AccessorySetupKit功能 + #if canImport(AccessorySetupKit) + if #available(iOS 18.0, *) { + // 设备运行iOS 18+,支持AccessorySetupKit + result(true) + } else { + // 设备运行iOS 17或更低版本 + result(false) + } + #else + // 编译时没有AccessorySetupKit框架 + result(false) + #endif + + case "getPairedMacAddress": + // 获取已配对设备的标识符 + #if canImport(AccessorySetupKit) + if #available(iOS 18.0, *) { + if let wrapper = BleAccessorySetup.shared.accessorySessionWrapper as? BleAccessorySetup.AccessorySessionWrapper { + // 检查是否有配对设备 + if let firstAccessory = wrapper.session.accessories.first, + let btId = firstAccessory.bluetoothIdentifier { + // 在iOS中,我们返回UUID字符串作为"MAC地址"的等价物 + result(btId.uuidString) + } else { + // 无配对设备 + result(nil) + } + } else { + // 会话未初始化 + result(nil) + } + } else { + // iOS版本太低 + result(nil) + } + #else + // 编译环境不支持 + result(nil) + #endif + + case "registerBackgroundScan": + // iOS通过AccessorySetupKit自动处理后台扫描,不需要单独注册 + #if canImport(AccessorySetupKit) + if #available(iOS 18.0, *) { + // 在iOS 18+上,不需要额外操作 + result(true) + } else { + // iOS版本太低 + result(false) + } + #else + result(false) + #endif + + case "unregisterBackgroundScan": + // iOS通过AccessorySetupKit自动处理后台扫描,不需要单独注销 + result(true) + + case "clearAssociations": + // 清除设备关联 + #if canImport(AccessorySetupKit) + if #available(iOS 18.0, *) { + if let wrapper = BleAccessorySetup.shared.accessorySessionWrapper as? BleAccessorySetup.AccessorySessionWrapper { + // 遍历所有配对设备 + for accessory in wrapper.session.accessories { + if let btId = accessory.bluetoothIdentifier { + // 移除设备的系统处理已经集成在设置中,这里什么都不做 + os_log("设备需要在系统设置中手动移除: %{public}@", + log: OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleService"), + type: .info, + btId.uuidString) + } + } + } + // 返回成功 + result(true) + } else { + // iOS版本太低 + result(false) + } + #else + result(false) + #endif case "getDeviceVersion": - result(bleService.getDeviceVersion()) + result(BleService.shared.getDeviceVersion()) case "getProductId": - result(bleService.getProductId()) + result(BleService.shared.getProductId()) + + case "getBatteryInfo": + result(BleService.shared.getBatteryInfo()) case "openEncoder": - result(bleService.openEncoder()) + result(BleService.shared.openEncoder()) case "openDecoder": - result(bleService.openDecoder()) - + result(BleService.shared.openDecoder()) + case "openA2DPDecoder": - result(bleService.openA2DPDecoder()) - + result(BleService.shared.openA2DPDecoder()) + case "closeCodec": - result(bleService.closeCodec()) + result(BleService.shared.closeCodec()) case "playA2DP": - result(bleService.playA2DP()) - - case "registerBackgroundScan": - result(bleService.registerBackgroundScan()) - - case "unregisterBackgroundScan": - result(bleService.unregisterBackgroundScan()) + result(BleService.shared.playA2DP()) - case "startBleService": - guard let config = call.arguments as? [String: Any] else { - result(FlutterError(code: "INVALID_ARGUMENTS", message: "配置参数无效", details: nil)) - return + // iOS 18+ AccessorySetupKit相关方法 + case "startDeviceAssociation": + #if canImport(AccessorySetupKit) + if #available(iOS 18.0, *) { + BleAccessorySetup.shared.showDevicePicker() + result(true) + } else { + result(FlutterError(code: "UNAVAILABLE", + message: "AccessorySetupKit需要iOS 18或更高版本", + details: nil)) } - - // 处理配置参数 - if let address = config["mac"] as? String { - // 如果提供了设备地址,尝试连接 - bleService.connect(address: address) - } - - // 开始扫描,寻找设备 - let scanSuccess = bleService.startScan() - result(scanSuccess) - - case "stopBleService": - // 停止扫描和断开连接 - _ = bleService.stopScan() - if bleService.isConnected() { - bleService.disconnect() - } - result(true) + #else + result(FlutterError(code: "UNAVAILABLE", + message: "编译环境不支持AccessorySetupKit", + details: nil)) + #endif default: result(FlutterMethodNotImplemented) } } - // MARK: - BleDelegate Methods + // MARK: - 私有方法 + private func handleDiscoveredDevice(uuid: UUID) { + BleAccessorySetup.shared.handleDiscoveredAccessory(uuid: uuid) + } - func onScanResult(devices: [[String: Any]]) { - // 目前Flutter层没有直接使用此事件,可以选择不转发 + private func sendEvent(_ eventSink: FlutterEventSink?, data: T, errorPrefix: String) { + if let eventSink = eventSink { + DispatchQueue.main.async { + eventSink(data) + } + } + } +} + +// MARK: - FlutterStreamHandler +@available(iOS 13.0, *) +extension SwiftBleServicePlugin: FlutterStreamHandler { + public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { + if let args = arguments as? [String: Any], let type = args["type"] as? String { + if type == "data" { + dataEventSink = events + } else if type == "status" { + statusEventSink = events + } + } else { + // 默认行为:根据注册的通道来确定事件类型 + if arguments == nil { + // 如果无参数,根据函数被调用的顺序确定 + if dataEventSink == nil { + dataEventSink = events + } else { + statusEventSink = events + } + } + } + return nil + } + + public func onCancel(withArguments arguments: Any?) -> FlutterError? { + if let args = arguments as? [String: Any], let type = args["type"] as? String { + if type == "data" { + dataEventSink = nil + } else if type == "status" { + statusEventSink = nil + } + } else { + // 同时清除两个sink + dataEventSink = nil + statusEventSink = nil + } + return nil + } +} + +// MARK: - BleService.Callback +@available(iOS 13.0, *) +extension SwiftBleServicePlugin: BleService.Callback { + func onScanResult(devices: [[String : Any]]) { + sendEvent(statusEventSink, + data: ["type": "scanResult", "devices": devices], + errorPrefix: "发送扫描结果异常") } func onConnectionStateChanged(state: Int) { - // 将连接状态变化通过状态通道发送 - let stateStr: String + var stateStr = "Unknown" switch state { case BleConst.STATE_DISCONNECTED: stateStr = "Disconnected" @@ -156,54 +273,29 @@ public class SwiftBleServicePlugin: NSObject, FlutterPlugin, BleDelegate { "stateStr": stateStr ] - sendEvent(statusEventSink, stateMap) + sendEvent(statusEventSink, data: stateMap, errorPrefix: "发送连接状态异常") } func onAudioDataReceived(data: Data) { - // 将音频数据通过数据通道发送 let dataMap: [String: Any] = [ "type": "audioData", - "data": FlutterStandardTypedData(bytes: data) + "data": data ] - sendEvent(dataEventSink, dataMap) - } - - func onWakeupSignalReceived() { - // 将唤醒事件通过状态通道发送 - sendEvent(statusEventSink, ["type": "wakeup"]) + sendEvent(dataEventSink, data: dataMap, errorPrefix: "发送音频数据异常") } - func onDeviceInfoReceived(infoType: Int, infoData: [String: Any]) { - // 将设备信息通过状态通道发送 - var resultMap = infoData - resultMap["type"] = "deviceInfo" - resultMap["result"] = infoType + func onDeviceInfoReceived(infoType: Int, infoData: [String : Any]) { + var dataMap: [String: Any] = [ + "type": "deviceInfo", + "infoType": infoType + ] - sendEvent(statusEventSink, resultMap) - } -} - -// MARK: - 事件处理器类 - -/// 简化的事件处理器类 -class StreamHandler: NSObject, FlutterStreamHandler { - private let onListen: (FlutterEventSink) -> Void - private let onCancel: () -> Void - - init(onListen: @escaping (FlutterEventSink) -> Void, onCancel: @escaping () -> Void) { - self.onListen = onListen - self.onCancel = onCancel - super.init() - } - - func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { - onListen(events) - return nil - } - - func onCancel(withArguments arguments: Any?) -> FlutterError? { - onCancel() - return nil + // 合并infoData + for (key, value) in infoData { + dataMap[key] = value + } + + sendEvent(statusEventSink, data: dataMap, errorPrefix: "发送设备信息异常") } } \ No newline at end of file diff --git a/local_plugins/ble_service/ios/README.md b/local_plugins/ble_service/ios/README.md new file mode 100644 index 000000000..c5cfd687c --- /dev/null +++ b/local_plugins/ble_service/ios/README.md @@ -0,0 +1,87 @@ +# ble_service iOS 集成说明 + +## 概述 + +该插件提供了蓝牙低功耗(BLE)设备的连接和通信功能,特别支持BLE耳机设备的音频数据传输。 + +- 支持 iOS 13.0+ 的基础蓝牙连接功能 +- 支持 iOS 18.0+ 的 AccessorySetupKit 集成,实现应用后台唤醒 + +## 集成步骤 + +### 1. 权限配置 + +在应用的 `Info.plist` 中添加以下权限描述: + +```xml + +NSBluetoothAlwaysUsageDescription +需要使用蓝牙连接您的耳机设备 + + +NSBluetoothPeripheralUsageDescription +需要使用蓝牙连接您的耳机设备 + + +UIBackgroundModes + + bluetooth-central + +``` + +### 2. iOS 18+ AccessorySetupKit 配置 + +如果需要支持 iOS 18+ 的 AccessorySetupKit 功能(应用后台唤醒),还需要在 `Info.plist` 中添加: + +```xml + +NSAccessorySetupBluetoothCompanyIdentifiers + + ABEF + + +NSAccessorySetupBluetoothServices + + 0000ABC0-0000-1111-2222-123456789ABC + 0000AE00-0000-1000-8000-00805F9B34FB + + +NSAccessorySetupKitSupports + + Bluetooth + +``` + +### 3. 集成到 Runner 项目 + +1. 确保项目的最低部署版本为 iOS 13.0 + +2. 在 Xcode 中编辑 `Runner` 项目的 `info.plist`,添加上述配置 + +3. 如需自定义 UUID 或厂商 ID,请同时修改 `BleConst.swift` 文件中的对应常量 + +## 使用说明 + +### 基本流程 + +1. 首次使用时,应用需要在前台扫描并连接设备 +2. 配对成功后,后续可通过 AccessorySetupKit (iOS 18+) 自动唤醒应用 + +### 支持的命令 + +- 获取设备版本: `getDeviceVersion()` +- 获取电池信息: `getBatteryInfo()` +- 打开/关闭编解码: `openEncoder()`, `openDecoder()`, `closeCodec()` +- A2DP 控制: `playA2DP()` + +## 后台运行说明 + +1. 该插件支持在后台接收 BLE 通知和数据 +2. iOS 18+ 设备上,支持应用在后台/被杀死状态下被设备唤醒 +3. 后台保活和状态恢复由系统管理,无需额外代码 + +## 注意事项 + +- 使用前确保已在用户设备上获得蓝牙权限 +- 由于苹果限制,手动杀死应用后,仍需用户主动重新打开一次应用才能完全激活后台唤醒 +- 厂商 ID 和服务 UUID 需要与实际设备匹配 \ No newline at end of file diff --git a/local_plugins/ble_service/ios/ble_service.podspec b/local_plugins/ble_service/ios/ble_service.podspec index 72f1802e5..cad3ddffb 100644 --- a/local_plugins/ble_service/ios/ble_service.podspec +++ b/local_plugins/ble_service/ios/ble_service.podspec @@ -1,21 +1,23 @@ # # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. -# Run `pod lib lint ble_service.podspec` to validate before publishing. # Pod::Spec.new do |s| s.name = 'ble_service' s.version = '0.0.1' - s.summary = 'BLE服务Flutter插件' + s.summary = 'BLE服务插件,提供蓝牙扫描、连接和通信功能' s.description = <<-DESC -BLE服务Flutter插件,提供蓝牙设备扫描、连接和通信功能。 +BLE服务插件,提供蓝牙扫描、连接和通信功能,支持iOS 18+ AccessorySetupKit唤醒。 DESC s.homepage = 'http://example.com' s.license = { :file => '../LICENSE' } s.author = { 'Your Company' => 'email@example.com' } s.source = { :path => '.' } s.source_files = 'Classes/**/*' + s.info_plist = { + 'NSAccessorySetupKitSupports' => ['Bluetooth'] + } s.dependency 'Flutter' - s.platform = :ios, '11.0' + s.platform = :ios, '18.0' # Flutter.framework does not contain a i386 slice. s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } diff --git a/local_plugins/ble_service/lib/ble_service.dart b/local_plugins/ble_service/lib/ble_service.dart index 9b28d7cfc..076da7705 100644 --- a/local_plugins/ble_service/lib/ble_service.dart +++ b/local_plugins/ble_service/lib/ble_service.dart @@ -161,13 +161,7 @@ class BleService { } /// 启动BLE后台服务 - /// [config] 配置参数,可包含: - /// - scan_interval: 扫描间隔时间(毫秒) - /// - scan_duration: 扫描持续时间(毫秒) - /// - notification_title: 通知标题 - /// - notification_content: 通知内容 - /// - mac: 设备地址 - /// - device_name: 设备名称 + Future startBleService(Map config) async { try { final result = diff --git a/local_plugins/ble_service/pubspec.yaml b/local_plugins/ble_service/pubspec.yaml index a0c48bfaa..ee3cf328f 100644 --- a/local_plugins/ble_service/pubspec.yaml +++ b/local_plugins/ble_service/pubspec.yaml @@ -21,4 +21,6 @@ flutter: platforms: android: package: com.yunqiinnovation.ble_service - pluginClass: BleServicePlugin \ No newline at end of file + pluginClass: BleServicePlugin + ios: + pluginClass: BleServicePlugin \ No newline at end of file diff --git a/local_plugins/classic_bluetooth/ios/Classes/ClassicBluetoothHelper.swift b/local_plugins/classic_bluetooth/ios/Classes/ClassicBluetoothHelper.swift index a9aa9b3b5..f9a9c782c 100644 --- a/local_plugins/classic_bluetooth/ios/Classes/ClassicBluetoothHelper.swift +++ b/local_plugins/classic_bluetooth/ios/Classes/ClassicBluetoothHelper.swift @@ -1,6 +1,6 @@ import Foundation -import ExternalAccessory import CoreBluetooth +import AVFoundation /// 蓝牙事件类型 enum BluetoothEventType { @@ -14,113 +14,205 @@ enum BluetoothState: String { case unknown = "unknown" case off = "off" case on = "on" - case turningOn = "turningOn" - case turningOff = "turningOff" + case unauthorized = "unauthorized" + case unsupported = "unsupported" + case resetting = "resetting" } -/// 经典蓝牙助手类,负责iOS上的蓝牙设备管理 +/// 简化的蓝牙助手类,专注于iOS上的蓝牙耳机检测 class ClassicBluetoothHelper: NSObject { - private let TAG = "ClassicBluetoothHelper" - // 状态监控相关 private var isMonitoring = false private var stateCallback: ((BluetoothEventType, [String: Any]) -> Void)? - // 用于监控蓝牙系统状态 + // 用于监控蓝牙系统状态,延迟初始化 private var centralManager: CBCentralManager? - // 通知观察者 - private var accessoryConnectionObserver: NSObjectProtocol? - private var accessoryDisconnectionObserver: NSObjectProtocol? + // 音频路由变化观察者 + private var routeChangeObserver: NSObjectProtocol? override init() { super.init() - centralManager = CBCentralManager(delegate: nil, queue: nil, options: [CBCentralManagerOptionShowPowerAlertKey: false]) + // 初始化时不立即创建centralManager,延迟到实际需要时 } deinit { stopBluetoothStateMonitoring() } - /// 检查蓝牙是否启用 + /// 延迟初始化CBCentralManager,只在实际需要时创建 + private func initializeCentralManagerIfNeeded() { + if centralManager == nil { + centralManager = CBCentralManager( + delegate: self, + queue: DispatchQueue.main, + options: [CBCentralManagerOptionShowPowerAlertKey: true] + ) + } + } + + /// 检查蓝牙是否启用,此方法会触发权限请求 func isBluetoothEnabled() -> Bool { - guard let centralManager = centralManager else { return false } - return centralManager.state == .poweredOn + // 此方法调用会触发权限申请 + initializeCentralManagerIfNeeded() + + guard let central = centralManager else { + print("检查蓝牙状态失败:centralManager未初始化") + return false + } + + // 检查蓝牙状态并记录 + let stateDescription: String + let isEnabled: Bool + + switch central.state { + case .poweredOn: + stateDescription = "已开启" + isEnabled = true + case .poweredOff: + stateDescription = "已关闭" + isEnabled = false + case .unauthorized: + stateDescription = "未授权" + isEnabled = false + case .unsupported: + stateDescription = "不支持" + isEnabled = false + case .resetting: + stateDescription = "重置中" + isEnabled = false + case .unknown: + stateDescription = "未知" + isEnabled = false + @unknown default: + stateDescription = "未定义状态" + isEnabled = false + } + + print("蓝牙状态检查结果: \(stateDescription), isEnabled: \(isEnabled)") + return isEnabled } - /// 获取已连接的蓝牙设备 - /// - Parameter completion: 完成回调 + /// 获取已连接的蓝牙耳机,此方法会触发权限请求 func getConnectedDevices(completion: @escaping ([[String: String]], Error?) -> Void) { - // 获取所有已连接的外部配件 - let accessoryManager = EAAccessoryManager.shared() - let connectedAccessories = accessoryManager.connectedAccessories - - // 将配件转换为设备信息数组 - let devices = connectedAccessories.map { accessory -> [String: String] in - return [ - "name": accessory.name, - "address": accessory.serialNumber // iOS使用序列号作为唯一标识 + // 确保蓝牙已初始化 + initializeCentralManagerIfNeeded() + + // 获取音频设备(主要是蓝牙耳机) + let audioDevices = getConnectedAudioDevices() + + // 日志输出 + if !audioDevices.isEmpty { + print("已找到\(audioDevices.count)个蓝牙音频设备:") + for device in audioDevices { + print("- \(device["name"] ?? "未知") (\(device["address"] ?? "未知地址"))") + } + } else { + print("未找到蓝牙音频设备") + } + + completion(audioDevices, nil) + } + + // 获取连接的蓝牙音频设备 + private func getConnectedAudioDevices() -> [[String: String]] { + var result: [[String: String]] = [] + + // 获取当前音频路由 + let outputs = AVAudioSession.sharedInstance().currentRoute.outputs + + // 过滤蓝牙相关设备 + for output in outputs where isBluetoothOutput(output.portType) { + let device: [String: String] = [ + "name": output.portName, + "address": cleanDeviceAddress(output.uid) ] + result.append(device) } - // 返回结果 - completion(devices, nil) + return result + } + + // 检查是否为蓝牙输出 + private func isBluetoothOutput(_ portType: AVAudioSession.Port) -> Bool { + return portType == .bluetoothA2DP || portType == .bluetoothHFP || portType == .bluetoothLE + } + + // 清理设备地址 + private func cleanDeviceAddress(_ address: String) -> String { + // 移除类似 "-tacl" 的后缀 + if let range = address.range(of: "-", options: .backwards) { + return String(address[.. Void) { self.stateCallback = callback if !isMonitoring { isMonitoring = true - // 设置蓝牙中央管理器代理 - centralManager?.delegate = self + // 初始化蓝牙管理器 + initializeCentralManagerIfNeeded() - // 注册蓝牙配件连接/断开通知 + // 观察音频路由变化 let notificationCenter = NotificationCenter.default - - // 观察配件连接 - accessoryConnectionObserver = notificationCenter.addObserver( - forName: NSNotification.Name.EAAccessoryDidConnect, + routeChangeObserver = notificationCenter.addObserver( + forName: AVAudioSession.routeChangeNotification, object: nil, queue: nil ) { [weak self] notification in - guard let self = self, - let accessory = notification.userInfo?[EAAccessoryKey] as? EAAccessory else { - return - } - - let device: [String: String] = [ - "name": accessory.name, - "address": accessory.serialNumber - ] - - self.stateCallback?(.deviceConnected, ["device": device]) + self?.handleAudioRouteChange(notification) } - // 观察配件断开 - accessoryDisconnectionObserver = notificationCenter.addObserver( - forName: NSNotification.Name.EAAccessoryDidDisconnect, - object: nil, - queue: nil - ) { [weak self] notification in - guard let self = self, - let accessory = notification.userInfo?[EAAccessoryKey] as? EAAccessory else { - return + // 激活音频会话 + activateAudioSession() + } + } + + // 处理音频路由变化 + private func handleAudioRouteChange(_ notification: Notification) { + guard let userInfo = notification.userInfo, + let reasonValue = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt, + let reason = AVAudioSession.RouteChangeReason(rawValue: reasonValue) else { + return + } + + switch reason { + case .newDeviceAvailable: + // 新音频设备已连接 + let audioDevices = getConnectedAudioDevices() + for device in audioDevices { + stateCallback?(.deviceConnected, ["device": device]) + } + + case .oldDeviceUnavailable: + // 获取断开之前的路由 + if let previousRoute = userInfo[AVAudioSessionRouteChangePreviousRouteKey] as? AVAudioSessionRouteDescription { + for output in previousRoute.outputs where isBluetoothOutput(output.portType) { + let device: [String: String] = [ + "name": output.portName, + "address": cleanDeviceAddress(output.uid) + ] + stateCallback?(.deviceDisconnected, ["device": device]) } - - let device: [String: String] = [ - "name": accessory.name, - "address": accessory.serialNumber - ] - - self.stateCallback?(.deviceDisconnected, ["device": device]) } - // 开始监视配件变化 - EAAccessoryManager.shared().registerForLocalNotifications() + default: + break + } + } + + // 激活音频会话 + private func activateAudioSession() { + do { + let audioSession = AVAudioSession.sharedInstance() + try audioSession.setCategory(.playback, mode: .default) + try audioSession.setActive(true) + } catch { + print("设置音频会话失败: \(error.localizedDescription)") } } @@ -129,48 +221,36 @@ class ClassicBluetoothHelper: NSObject { if isMonitoring { isMonitoring = false - // 移除蓝牙中央管理器代理 - centralManager?.delegate = nil - - // 注销配件通知观察者 - if let observer = accessoryConnectionObserver { + // 移除音频路由观察者 + if let observer = routeChangeObserver { NotificationCenter.default.removeObserver(observer) - accessoryConnectionObserver = nil + routeChangeObserver = nil } - if let observer = accessoryDisconnectionObserver { - NotificationCenter.default.removeObserver(observer) - accessoryDisconnectionObserver = nil - } - - // 停止监视配件变化 - EAAccessoryManager.shared().unregisterForLocalNotifications() + // 关闭音频会话 + deactivateAudioSession() } // 清除回调 stateCallback = nil } + // 关闭音频会话 + private func deactivateAudioSession() { + do { + try AVAudioSession.sharedInstance().setActive(false) + } catch { + print("关闭音频会话失败: \(error.localizedDescription)") + } + } + /// 报告当前蓝牙状态 func reportCurrentBluetoothState() { + // 如果centralManager未初始化,不触发状态报告 guard let centralManager = centralManager else { return } - let state: BluetoothState - - switch centralManager.state { - case .poweredOn: - state = .on - case .poweredOff: - state = .off - case .resetting: - state = .turningOff // 最接近的近似值 - case .unauthorized, .unsupported: - state = .off // 功能上等同于关闭 - default: - state = .unknown - } - - stateCallback?(.stateChanged, ["state": state.rawValue]) + // 直接调用delegate方法保持状态处理一致性 + centralManagerDidUpdateState(centralManager) } } @@ -179,19 +259,47 @@ extension ClassicBluetoothHelper: CBCentralManagerDelegate { func centralManagerDidUpdateState(_ central: CBCentralManager) { let state: BluetoothState + // 添加详细日志输出 + print("iOS蓝牙状态原始值: \(central.state.rawValue)") + switch central.state { case .poweredOn: + print("蓝牙状态: 已开启(.poweredOn)") state = .on case .poweredOff: + print("蓝牙状态: 已关闭(.poweredOff)") state = .off + case .unauthorized: + print("蓝牙状态: 未授权(.unauthorized)") + state = .unauthorized + case .unsupported: + print("蓝牙状态: 不支持(.unsupported)") + state = .unsupported case .resetting: - state = .turningOff - case .unauthorized, .unsupported: - state = .off - default: + print("蓝牙状态: 重置中(.resetting)") + state = .resetting + case .unknown: + print("蓝牙状态: 未知(.unknown)") + state = .unknown + @unknown default: + print("蓝牙状态: 未处理的新状态(\(central.state.rawValue))") state = .unknown } - stateCallback?(.stateChanged, ["state": state.rawValue]) + print("向Flutter端报告的蓝牙状态: \(state.rawValue)") + + // 仅发送兼容的状态(为保持向后兼容,只发送on/off/unknown) + let reportState: String + switch state { + case .on: + reportState = BluetoothState.on.rawValue + case .off, .unauthorized, .unsupported: + // 这些状态对用户来说都意味着蓝牙不可用 + reportState = BluetoothState.off.rawValue + default: + reportState = BluetoothState.unknown.rawValue + } + + stateCallback?(.stateChanged, ["state": reportState]) } } \ No newline at end of file diff --git a/local_plugins/classic_bluetooth/ios/Classes/FileLogger.swift b/local_plugins/classic_bluetooth/ios/Classes/FileLogger.swift deleted file mode 100644 index 7bbc903c9..000000000 --- a/local_plugins/classic_bluetooth/ios/Classes/FileLogger.swift +++ /dev/null @@ -1,136 +0,0 @@ -import Foundation -import os.log - -/// iOS端的文件日志助手类,提供与Android端FileLogger类似的功能 -class FileLogger { - - private static let logQueue = DispatchQueue(label: "com.yunqiinnovation.classic_bluetooth.logger", qos: .background) - private static let dateFormatter: DateFormatter = { - let formatter = DateFormatter() - formatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS" - return formatter - }() - - /// 日志类型 - enum LogLevel: String { - case debug = "DEBUG" - case info = "INFO" - case warning = "WARNING" - case error = "ERROR" - } - - /// 输出调试日志 - /// - Parameters: - /// - tag: 标签 - /// - message: 日志消息 - static func d(_ tag: String, _ message: String) { - log(level: .debug, tag: tag, message: message) - } - - /// 输出信息日志 - /// - Parameters: - /// - tag: 标签 - /// - message: 日志消息 - static func i(_ tag: String, _ message: String) { - log(level: .info, tag: tag, message: message) - } - - /// 输出警告日志 - /// - Parameters: - /// - tag: 标签 - /// - message: 日志消息 - static func w(_ tag: String, _ message: String) { - log(level: .warning, tag: tag, message: message) - } - - /// 输出错误日志 - /// - Parameters: - /// - tag: 标签 - /// - message: 日志消息 - /// - error: 可选的错误对象 - static func e(_ tag: String, _ message: String, _ error: Error? = nil) { - var fullMessage = message - if let error = error { - fullMessage += " - Error: \(error.localizedDescription)" - } - log(level: .error, tag: tag, message: fullMessage) - } - - /// 输出日志 - /// - Parameters: - /// - level: 日志级别 - /// - tag: 标签 - /// - message: 日志消息 - private static func log(level: LogLevel, tag: String, message: String) { - let timestamp = dateFormatter.string(from: Date()) - let logMessage = "[\(timestamp)] [\(level.rawValue)] [\(tag)]: \(message)" - - // 输出到控制台 - print(logMessage) - - // 异步写入日志文件 - logQueue.async { - writeToLogFile(logMessage) - } - - // 使用系统日志 - if #available(iOS 10.0, *) { - let osLog = OSLog(subsystem: "com.yunqiinnovation.classic_bluetooth", category: tag) - let osLogType: OSLogType - - switch level { - case .debug: - osLogType = .debug - case .info: - osLogType = .info - case .warning: - osLogType = .default - case .error: - osLogType = .error - } - - os_log("%{public}@", log: osLog, type: osLogType, message) - } - } - - /// 将日志写入文件 - /// - Parameter message: 日志消息 - private static func writeToLogFile(_ message: String) { - guard let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { - return - } - - let logDirectoryURL = documentsDirectory.appendingPathComponent("Logs") - let dateString = DateFormatter().with { $0.dateFormat = "yyyy-MM-dd" }.string(from: Date()) - let logFileURL = logDirectoryURL.appendingPathComponent("bluetooth_\(dateString).log") - - do { - // 创建日志文件夹 - try FileManager.default.createDirectory(at: logDirectoryURL, withIntermediateDirectories: true) - - // 如果文件不存在,创建文件 - if !FileManager.default.fileExists(atPath: logFileURL.path) { - FileManager.default.createFile(atPath: logFileURL.path, contents: nil) - } - - // 追加日志内容 - if let fileHandle = FileHandle(forWritingAtPath: logFileURL.path) { - fileHandle.seekToEndOfFile() - if let data = "\(message)\n".data(using: .utf8) { - fileHandle.write(data) - } - fileHandle.closeFile() - } - } catch { - print("写入日志文件失败: \(error.localizedDescription)") - } - } -} - -// MARK: - DateFormatter 扩展 -extension DateFormatter { - func with(_ block: (DateFormatter) -> Void) -> DateFormatter { - block(self) - return self - } -} \ No newline at end of file diff --git a/local_plugins/classic_bluetooth/ios/Classes/SwiftClassicBluetoothPlugin.swift b/local_plugins/classic_bluetooth/ios/Classes/SwiftClassicBluetoothPlugin.swift index d24578b5d..c3755d0d7 100644 --- a/local_plugins/classic_bluetooth/ios/Classes/SwiftClassicBluetoothPlugin.swift +++ b/local_plugins/classic_bluetooth/ios/Classes/SwiftClassicBluetoothPlugin.swift @@ -1,6 +1,7 @@ import Flutter import UIKit import ExternalAccessory +import AVFoundation // 这个文件是为了满足Flutter插件的命名约定 // 实际实现在ClassicBluetoothPlugin.swift中 @@ -22,6 +23,9 @@ public class SwiftClassicBluetoothPlugin: NSObject, FlutterPlugin { instance.methodChannel = channel instance.eventChannel = eventChannel + + // 记录插件初始化 + print("经典蓝牙插件初始化成功") } public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { @@ -36,18 +40,37 @@ public class SwiftClassicBluetoothPlugin: NSObject, FlutterPlugin { } private func getConnectedDevices(result: @escaping FlutterResult) { + // 先检查蓝牙状态 if !bluetoothHelper.isBluetoothEnabled() { + print("蓝牙未启用,无法获取设备") result([]) return } bluetoothHelper.getConnectedDevices { devices, error in if let error = error { + print("获取设备失败: \(error.localizedDescription)") result(FlutterError(code: "BLUETOOTH_ERROR", message: error.localizedDescription, details: nil)) return } - result(devices) + print("获取到 \(devices.count) 个蓝牙设备") + for device in devices { + if let name = device["name"], let address = device["address"], let type = device["type"] { + print("设备: \(name), 地址: \(address), 类型: \(type)") + } + } + + // 转换为Flutter可用的格式 + let resultList = devices.map { device -> [String: String] in + // 仅保留Flutter端需要的字段 + return [ + "name": device["name"] ?? "未知设备", + "address": device["address"] ?? "未知地址" + ] + } + + result(resultList) } } @@ -71,6 +94,7 @@ extension SwiftClassicBluetoothPlugin: FlutterStreamHandler { switch eventType { case .stateChanged: if let state = data["state"] as? String { + print("蓝牙状态变化: \(state)") self.sendEvent([ "type": "stateChanged", "state": state, @@ -79,17 +103,29 @@ extension SwiftClassicBluetoothPlugin: FlutterStreamHandler { } case .deviceConnected: if let device = data["device"] as? [String: String] { + print("设备连接: \(device["name"] ?? "未知") [\(device["address"] ?? "未知")]") + // 只保留Flutter端需要的字段 + let deviceData: [String: String] = [ + "name": device["name"] ?? "未知设备", + "address": device["address"] ?? "未知地址" + ] self.sendEvent([ "type": "deviceConnected", - "device": device, + "device": deviceData, "timestamp": Int(Date().timeIntervalSince1970 * 1000) ]) } case .deviceDisconnected: if let device = data["device"] as? [String: String] { + print("设备断开: \(device["name"] ?? "未知") [\(device["address"] ?? "未知")]") + // 只保留Flutter端需要的字段 + let deviceData: [String: String] = [ + "name": device["name"] ?? "未知设备", + "address": device["address"] ?? "未知地址" + ] self.sendEvent([ "type": "deviceDisconnected", - "device": device, + "device": deviceData, "timestamp": Int(Date().timeIntervalSince1970 * 1000) ]) } diff --git a/local_plugins/opus/.gradle/8.10/fileHashes/fileHashes.lock b/local_plugins/opus/.gradle/8.10/fileHashes/fileHashes.lock index 4609b41f4..b21882f77 100644 Binary files a/local_plugins/opus/.gradle/8.10/fileHashes/fileHashes.lock and b/local_plugins/opus/.gradle/8.10/fileHashes/fileHashes.lock differ diff --git a/local_plugins/opus/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/local_plugins/opus/.gradle/buildOutputCleanup/buildOutputCleanup.lock index 737e977c9..3e3066aed 100644 Binary files a/local_plugins/opus/.gradle/buildOutputCleanup/buildOutputCleanup.lock and b/local_plugins/opus/.gradle/buildOutputCleanup/buildOutputCleanup.lock differ diff --git a/local_plugins/opus/.gradle/buildOutputCleanup/cache.properties b/local_plugins/opus/.gradle/buildOutputCleanup/cache.properties index e01706659..9139b185a 100644 --- a/local_plugins/opus/.gradle/buildOutputCleanup/cache.properties +++ b/local_plugins/opus/.gradle/buildOutputCleanup/cache.properties @@ -1,2 +1,2 @@ -#Sat May 17 17:27:45 IST 2025 +#Sun May 18 21:39:38 IST 2025 gradle.version=8.10 diff --git a/test.md b/test.md new file mode 100644 index 000000000..91ff40e92 --- /dev/null +++ b/test.md @@ -0,0 +1,45 @@ + +请帮我生成一个基于 GetX 最佳实践的 Flutter 应用项目骨架。要求如下: + +1. 目录结构 + - lib/ + - main.dart + - app/ + - data/ + - providers/ # 网络请求、API 客户端(使用 GetConnect) + - repositories/ # Repository 层,封装业务数据源 + - services/ # 全局服务(如 ThemeService、AuthService) + - modules/ + - home/ # 示例模块 + - bindings/ # HomeBinding,注册 Controller、Service + - controllers/ # HomeController + - models/ # HomeModel 数据类 + - views/ # HomePage 页面 + - routes/ + - app_routes.dart # 路由常量 + - app_pages.dart # GetPage 列表,包括中间件示例 + - translations/ # 多语言文件(en_US、zh_CN)和 Translations 实现 + - widgets/ # 公共 Widget(Loading、ErrorView) + - utils/ # 日志 Logger、网络拦截器等工具函数 + +2. main.dart + - 使用 GetMaterialApp + - 配置 initialRoute、getPages、theme、darkTheme、locale、translations + +3. 依赖注入(Bindings) + - 在 app/modules/**/bindings 中,用 Get.lazyPut、Get.putAsync、Get.create 分别示例单例、异步初始化和工厂模式 + +4. 状态管理 + - 在 Controller 中使用 Rx + .obs + - 在视图中用 Obx、GetX、GetBuilder + - 在 Controller 的 onInit、onReady、onClose 中写注释说明生命周期 + +5. 路由管理 + - 在 app/routes/app_routes.dart 定义路由名称常量 + - 在 app/routes/app_pages.dart 用 GetPage 配置路由、binding + +8. 架构模式 + - 强调 “Repository → Service → Controller → View” 分层 + - 各层代码注释职责 + +- 代码风格统一、注释清晰 \ No newline at end of file