From 705053f49046ba15568fdeaacef21dd60cb054b8 Mon Sep 17 00:00:00 2001
From: lxm <1274844347@qq.com>
Date: Mon, 19 May 2025 16:23:19 +0800
Subject: [PATCH 1/3] =?UTF-8?q?chore:=20=E6=9B=B4=E6=96=B0GoogleService-In?=
=?UTF-8?q?fo.plist=E4=B8=AD=E7=9A=84=E5=AE=A2=E6=88=B7=E7=AB=AFID?=
=?UTF-8?q?=E5=92=8C=E5=8C=85=E5=90=8D?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
更新了GoogleService-Info.plist文件中的CLIENT_ID、REVERSED_CLIENT_ID、BUNDLE_ID和GOOGLE_APP_ID,以匹配最新的配置需求。
---
GoogleService-Info.plist | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/GoogleService-Info.plist b/GoogleService-Info.plist
index c90ada3a3..590c85a5f 100644
--- a/GoogleService-Info.plist
+++ b/GoogleService-Info.plist
@@ -3,9 +3,9 @@
CLIENT_ID
- 907754492821-oc9e77svgmmlgouo0op7vv00e1hb8278.apps.googleusercontent.com
+ 907754492821-tl2blr39g82re40sttkve36ovt6qcu6c.apps.googleusercontent.com
REVERSED_CLIENT_ID
- com.googleusercontent.apps.907754492821-oc9e77svgmmlgouo0op7vv00e1hb8278
+ com.googleusercontent.apps.907754492821-tl2blr39g82re40sttkve36ovt6qcu6c
ANDROID_CLIENT_ID
907754492821-9jakqvathu61e6b8g95i41ssi7o4dnka.apps.googleusercontent.com
API_KEY
@@ -15,7 +15,7 @@
PLIST_VERSION
1
BUNDLE_ID
- com.yunqiinnovation.deepsound
+ com.chuangxiangyunqi.deapsound
PROJECT_ID
deepsound-e0e3d
STORAGE_BUCKET
@@ -31,6 +31,6 @@
IS_SIGNIN_ENABLED
GOOGLE_APP_ID
- 1:907754492821:ios:246794fc44b23174145c9c
+ 1:907754492821:ios:e4b4f3b37e58a7d1145c9c
\ No newline at end of file
From d985da52b99c946ba63262157041097c119a99b6 Mon Sep 17 00:00:00 2001
From: wolfplus
Date: Mon, 19 May 2025 09:30:33 +0100
Subject: [PATCH 2/3] add
---
ios/Podfile | 2 +-
ios/Podfile.lock | 8 +-
ios/Runner.xcodeproj/project.pbxproj | 8 +
ios/Runner/Info.plist | 10 +
lib/core/bindings/initial_binding.dart | 31 +-
.../controllers/pairing_controller.dart | 286 +++--
.../splash/controllers/splash_controller.dart | 4 -
local_plugins/ble_service/LICENSE | 19 +
.../ios/Assets/ble_service-Info.plist | 15 -
.../ios/Classes/BleAccessorySetup.swift | 195 +++
.../ble_service/ios/Classes/BleConst.swift | 75 +-
.../ble_service/ios/Classes/BleDelegate.swift | 25 -
.../ble_service/ios/Classes/BleService.swift | 1067 +++++------------
.../ios/Classes/SwiftBleServicePlugin.swift | 360 +++---
local_plugins/ble_service/ios/README.md | 87 ++
.../ble_service/ios/ble_service.podspec | 10 +-
.../ble_service/lib/ble_service.dart | 8 +-
local_plugins/ble_service/pubspec.yaml | 4 +-
.../ios/Classes/ClassicBluetoothHelper.swift | 304 +++--
.../ios/Classes/FileLogger.swift | 136 ---
.../Classes/SwiftClassicBluetoothPlugin.swift | 42 +-
.../.gradle/8.10/fileHashes/fileHashes.lock | Bin 17 -> 17 bytes
.../buildOutputCleanup.lock | Bin 17 -> 17 bytes
.../buildOutputCleanup/cache.properties | 2 +-
test.md | 45 +
25 files changed, 1362 insertions(+), 1381 deletions(-)
create mode 100644 local_plugins/ble_service/LICENSE
delete mode 100644 local_plugins/ble_service/ios/Assets/ble_service-Info.plist
create mode 100644 local_plugins/ble_service/ios/Classes/BleAccessorySetup.swift
delete mode 100644 local_plugins/ble_service/ios/Classes/BleDelegate.swift
create mode 100644 local_plugins/ble_service/ios/README.md
delete mode 100644 local_plugins/classic_bluetooth/ios/Classes/FileLogger.swift
create mode 100644 test.md
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 4609b41f49aed24bed3654adeba76d8de3e4b1bb..b21882f77e2204b6110635b730bbfcb7b504d626 100644
GIT binary patch
literal 17
UcmZQRo$b{-r|q*H0|aOR05?_yjsO4v
literal 17
UcmZQRo$b{-r|q*H0|aOQ05? + .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
From 5eb0de4a513fba445c197fac3465074fc556ea5d Mon Sep 17 00:00:00 2001
From: wolfplus
Date: Mon, 19 May 2025 10:20:39 +0100
Subject: [PATCH 3/3] add
---
.../controllers/settings_controller.dart | 78 ++++
.../settings/views/permissions_view.dart | 324 ++++++++++++++
lib/modules/settings/views/settings_view.dart | 92 +++-
lib/routes/app_pages.dart | 8 +
lib/routes/app_routes.dart | 1 +
.../ios/Classes/BleServicePlugin.h | 2 +-
local_plugins/ble_service/pubspec.yaml | 2 +-
.../android/src/main/AndroidManifest.xml | 2 +
.../open_ai_service/SystemFunctionHandler.kt | 421 +++++++++++++++---
9 files changed, 871 insertions(+), 59 deletions(-)
create mode 100644 lib/modules/settings/views/permissions_view.dart
diff --git a/lib/modules/settings/controllers/settings_controller.dart b/lib/modules/settings/controllers/settings_controller.dart
index 05bca4804..984376632 100644
--- a/lib/modules/settings/controllers/settings_controller.dart
+++ b/lib/modules/settings/controllers/settings_controller.dart
@@ -12,6 +12,9 @@ import '../../../data/models/user_Info.dart';
import 'package:path_provider/path_provider.dart';
import 'dart:io';
import 'package:package_info_plus/package_info_plus.dart';
+import 'package:permission_handler/permission_handler.dart';
+import 'dart:async';
+import 'package:app_settings/app_settings.dart';
class SettingsController extends GetxController {
// 语言设置
@@ -101,6 +104,81 @@ class SettingsController extends GetxController {
final appVersion = ''.obs;
final buildNumber = ''.obs;
+ // 权限相关
+ // 权限组,用于在UI中显示
+ final List