wolfplus 1 year ago
parent
commit
288f6819ea
  1. 2
      ios/Podfile
  2. 8
      ios/Podfile.lock
  3. 8
      ios/Runner.xcodeproj/project.pbxproj
  4. 10
      ios/Runner/Info.plist
  5. 31
      lib/core/bindings/initial_binding.dart
  6. 278
      lib/modules/pairing/controllers/pairing_controller.dart
  7. 4
      lib/modules/splash/controllers/splash_controller.dart
  8. 19
      local_plugins/ble_service/LICENSE
  9. 15
      local_plugins/ble_service/ios/Assets/ble_service-Info.plist
  10. 195
      local_plugins/ble_service/ios/Classes/BleAccessorySetup.swift
  11. 71
      local_plugins/ble_service/ios/Classes/BleConst.swift
  12. 25
      local_plugins/ble_service/ios/Classes/BleDelegate.swift
  13. 1023
      local_plugins/ble_service/ios/Classes/BleService.swift
  14. 342
      local_plugins/ble_service/ios/Classes/SwiftBleServicePlugin.swift
  15. 87
      local_plugins/ble_service/ios/README.md
  16. 10
      local_plugins/ble_service/ios/ble_service.podspec
  17. 8
      local_plugins/ble_service/lib/ble_service.dart
  18. 2
      local_plugins/ble_service/pubspec.yaml
  19. 290
      local_plugins/classic_bluetooth/ios/Classes/ClassicBluetoothHelper.swift
  20. 136
      local_plugins/classic_bluetooth/ios/Classes/FileLogger.swift
  21. 42
      local_plugins/classic_bluetooth/ios/Classes/SwiftClassicBluetoothPlugin.swift
  22. BIN
      local_plugins/opus/.gradle/8.10/fileHashes/fileHashes.lock
  23. BIN
      local_plugins/opus/.gradle/buildOutputCleanup/buildOutputCleanup.lock
  24. 2
      local_plugins/opus/.gradle/buildOutputCleanup/cache.properties
  25. 45
      test.md

2
ios/Podfile

@ -1,5 +1,5 @@
# Define a global platform for your project # 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. # CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true' ENV['COCOAPODS_DISABLE_STATS'] = 'true'

8
ios/Podfile.lock

@ -11,6 +11,8 @@ PODS:
- Flutter - Flutter
- audio_waveforms (0.0.1): - audio_waveforms (0.0.1):
- Flutter - Flutter
- ble_service (0.0.1):
- Flutter
- chat_storage (0.0.1): - chat_storage (0.0.1):
- Flutter - Flutter
- classic_bluetooth (0.0.1): - classic_bluetooth (0.0.1):
@ -231,6 +233,7 @@ DEPENDENCIES:
- app_settings (from `.symlinks/plugins/app_settings/ios`) - app_settings (from `.symlinks/plugins/app_settings/ios`)
- audio_session (from `.symlinks/plugins/audio_session/ios`) - audio_session (from `.symlinks/plugins/audio_session/ios`)
- audio_waveforms (from `.symlinks/plugins/audio_waveforms/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`) - chat_storage (from `.symlinks/plugins/chat_storage/ios`)
- classic_bluetooth (from `.symlinks/plugins/classic_bluetooth/ios`) - classic_bluetooth (from `.symlinks/plugins/classic_bluetooth/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
@ -302,6 +305,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/audio_session/ios" :path: ".symlinks/plugins/audio_session/ios"
audio_waveforms: audio_waveforms:
:path: ".symlinks/plugins/audio_waveforms/ios" :path: ".symlinks/plugins/audio_waveforms/ios"
ble_service:
:path: ".symlinks/plugins/ble_service/ios"
chat_storage: chat_storage:
:path: ".symlinks/plugins/chat_storage/ios" :path: ".symlinks/plugins/chat_storage/ios"
classic_bluetooth: classic_bluetooth:
@ -370,6 +375,7 @@ SPEC CHECKSUMS:
AppAuth: d4f13a8fe0baf391b2108511793e4b479691fb73 AppAuth: d4f13a8fe0baf391b2108511793e4b479691fb73
audio_session: 9bb7f6c970f21241b19f5a3658097ae459681ba0 audio_session: 9bb7f6c970f21241b19f5a3658097ae459681ba0
audio_waveforms: a6dde7fe7c0ea05f06ffbdb0f7c1b2b2ba6cedcf audio_waveforms: a6dde7fe7c0ea05f06ffbdb0f7c1b2b2ba6cedcf
ble_service: 068ebe51800424f980282b16cbf757574eec4e08
chat_storage: 933a96144157a07a2fef7f6acb28c78ff18465d8 chat_storage: 933a96144157a07a2fef7f6acb28c78ff18465d8
classic_bluetooth: c5e5c0c86acab8dcd6b8e9cef7fca654c3518034 classic_bluetooth: c5e5c0c86acab8dcd6b8e9cef7fca654c3518034
device_info_plus: 71ffc6ab7634ade6267c7a93088ed7e4f74e5896 device_info_plus: 71ffc6ab7634ade6267c7a93088ed7e4f74e5896
@ -430,6 +436,6 @@ SPEC CHECKSUMS:
webview_flutter_wkwebview: 1821ceac936eba6f7984d89a9f3bcb4dea99ebb2 webview_flutter_wkwebview: 1821ceac936eba6f7984d89a9f3bcb4dea99ebb2
WechatOpenSDK-XCFramework: 36fb2bea0754266c17184adf4963d7e6ff98b69f WechatOpenSDK-XCFramework: 36fb2bea0754266c17184adf4963d7e6ff98b69f
PODFILE CHECKSUM: 017ca79b199d8f37c4acb8d83ec912447003b920 PODFILE CHECKSUM: f62382080fcf32f2d4640dc419aa07d69eff0328
COCOAPODS: 1.16.2 COCOAPODS: 1.16.2

8
ios/Runner.xcodeproj/project.pbxproj

@ -44,6 +44,7 @@
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
0AD944112DD9DB5C00557D21 /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = "<group>"; }; 0AD944112DD9DB5C00557D21 /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = "<group>"; };
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 = "<group>"; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; }; 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
@ -106,6 +107,7 @@
1E5DDD8701F6FB27AA502080 /* Frameworks */ = { 1E5DDD8701F6FB27AA502080 /* Frameworks */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
0AD944132DDA0C0D00557D21 /* AccessorySetupKit.framework */,
E27AD0DBE6531F2655DB9986 /* Pods_Runner.framework */, E27AD0DBE6531F2655DB9986 /* Pods_Runner.framework */,
F79ADB762804BBED65875C03 /* Pods_RunnerTests.framework */, F79ADB762804BBED65875C03 /* Pods_RunnerTests.framework */,
); );
@ -499,6 +501,7 @@
DEVELOPMENT_TEAM = 42C82SZUVX; DEVELOPMENT_TEAM = 42C82SZUVX;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
@ -519,6 +522,7 @@
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound.RunnerTests; PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
@ -537,6 +541,7 @@
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound.RunnerTests; PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
@ -553,6 +558,7 @@
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound.RunnerTests; PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
@ -685,6 +691,7 @@
DEVELOPMENT_TEAM = 42C82SZUVX; DEVELOPMENT_TEAM = 42C82SZUVX;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
@ -709,6 +716,7 @@
DEVELOPMENT_TEAM = 42C82SZUVX; DEVELOPMENT_TEAM = 42C82SZUVX;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",

10
ios/Runner/Info.plist

@ -41,6 +41,12 @@
</array> </array>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string> <string>$(FLUTTER_BUILD_NUMBER)</string>
<key>UIBackgroundModes</key>
<array>
<string>bluetooth-central</string>
<string>bluetooth-peripheral</string>
<string>external-accessory</string>
</array>
<key>LSApplicationQueriesSchemes</key> <key>LSApplicationQueriesSchemes</key>
<array> <array>
<string>weixin</string> <string>weixin</string>
@ -108,5 +114,9 @@
<true/> <true/>
<key>FacebookAutoLogAppEventsEnabled</key> <key>FacebookAutoLogAppEventsEnabled</key>
<true/> <true/>
<key>NSAccessorySetupKitSupports</key>
<array>
<string>Bluetooth</string>
</array>
</dict> </dict>
</plist> </plist>

31
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/chat_history_service.dart';
import '../../data/services/volcano_translation_service.dart'; import '../../data/services/volcano_translation_service.dart';
import '../../data/services/language_manager.dart'; import '../../data/services/language_manager.dart';
import '../../data/services/volcano_ai_service.dart';
import '../../core/utils/logger.dart'; import '../../core/utils/logger.dart';
import '../../data/services/open_ai_service_adapter.dart';
import '../../data/services/bluetooth_manager.dart'; import '../../data/services/bluetooth_manager.dart';
import '../../data/services/ble_manager.dart'; import '../../data/services/ble_manager.dart';
@ -17,10 +15,11 @@ class InitialBinding extends Bindings {
Logger.warning('InitialBinding dependencies'); Logger.warning('InitialBinding dependencies');
// 语言管理器(需要最先初始化) // 语言管理器(需要最先初始化)
Get.put<LanguageManager>(LanguageManager(), permanent: true); // 语言管理器(需要最先初始化)
Get.lazyPut<LanguageManager>(() => LanguageManager(), fenix: true);
//初始化语音工厂 //初始化语音工厂
Get.put<SpeechFactory>(SpeechFactory(), permanent: true); Get.lazyPut<SpeechFactory>(() => SpeechFactory(), fenix: true);
Get.find<SpeechFactory>().initialize(initialType: SpeechServiceType.azure); Get.find<SpeechFactory>().initialize(initialType: SpeechServiceType.azure);
// 历史记录服务 // 历史记录服务
@ -30,30 +29,14 @@ class InitialBinding extends Bindings {
Get.lazyPut<VolcanoTranslationService>(() => VolcanoTranslationService(), Get.lazyPut<VolcanoTranslationService>(() => VolcanoTranslationService(),
fenix: true); fenix: true);
// OpenAI服务适配器
Get.lazyPut<OpenAIServiceAdapter>(() {
final adapter = OpenAIServiceAdapter();
adapter.initialize();
return adapter;
}, fenix: true);
// 蓝牙服务 // 蓝牙服务
Get.put<BluetoothManager>(BluetoothManager(), permanent: true); Get.lazyPut<BluetoothManager>(() => BluetoothManager(), fenix: true);
// BleManager // BleManager
Get.put<BleManager>(BleManager(), permanent: true); Get.lazyPut<BleManager>(() => BleManager(), fenix: true);
// MusicManager // MusicManager
Get.put<MusicManager>(MusicManager(), permanent: true); Get.lazyPut<MusicManager>(() => MusicManager(), fenix: true);
// 火山AI服务 // 火山AI服务
Get.lazyPut<VolcanoAIService>(() => VolcanoAIService(), fenix: true); // Get.lazyPut<VolcanoAIService>(() => VolcanoAIService(), fenix: true);
// 蓝牙媒体按钮服务
// Get.lazyPut<BluetoothMediaButtonService>(
// () => BluetoothMediaButtonService(),
// fenix: true);
// // 语音交互服务
// Get.lazyPut<VoiceInteractionService>(() => VoiceInteractionService(),
// fenix: true);
// 杰理蓝牙服务 // 杰理蓝牙服务

278
lib/modules/pairing/controllers/pairing_controller.dart

@ -15,8 +15,6 @@ import 'package:flutter/material.dart';
class PairingController extends GetxController { class PairingController extends GetxController {
static PairingController get to => Get.find(); static PairingController get to => Get.find();
// 需要设备
final bool needdeivice = false;
// 状态标志 // 状态标志
final isLoading = false.obs; final isLoading = false.obs;
@ -65,6 +63,9 @@ class PairingController extends GetxController {
// BLE服务 // BLE服务
final _bleService = BleService(); final _bleService = BleService();
// 日志tag
static const _tag = 'PairingController';
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
@ -102,7 +103,7 @@ class PairingController extends GetxController {
// 检查蓝牙状态并刷新设备 // 检查蓝牙状态并刷新设备
await _checkBluetoothAndRefresh(); await _checkBluetoothAndRefresh();
} catch (e) { } catch (e) {
Logger.error('初始化配对控制器失败: ${e.toString()}'); Logger.e(_tag, '初始化配对控制器失败: ${e.toString()}');
statusDescription.value = 'initializationFailure'.tr; statusDescription.value = 'initializationFailure'.tr;
} finally { } finally {
isLoading.value = false; isLoading.value = false;
@ -134,13 +135,12 @@ class PairingController extends GetxController {
/// 检查设备配对状态 /// 检查设备配对状态
Future<void> _checkDevicePairingStatus() async { Future<void> _checkDevicePairingStatus() async {
// 只在Android平台且设备已连接时执行 if (!isDeviceConnected) return;
if (!Platform.isAndroid || !isDeviceConnected) return;
try { try {
final isSupported = await _bleService.isCompanionSupported(); final isSupported = await _bleService.isCompanionSupported();
if (!isSupported) { if (!isSupported) {
Logger.info('设备不支持Companion功能'); Logger.i(_tag, '设备不支持Companion功能');
_companionAssociated.value = true; // 不支持则视为已配对 _companionAssociated.value = true; // 不支持则视为已配对
_userRejectedAssociation.value = false; // 重置拒绝状态 _userRejectedAssociation.value = false; // 重置拒绝状态
return; return;
@ -149,16 +149,16 @@ class PairingController extends GetxController {
final pairedMac = await _bleService.getPairedMacAddress(); final pairedMac = await _bleService.getPairedMacAddress();
if (pairedMac == null) { if (pairedMac == null) {
// 没有配对记录,需要开始配对 // 没有配对记录,需要开始配对
Logger.info('无配对记录,需要开始设备配对'); Logger.i(_tag, '无配对记录,需要开始设备配对');
return; return;
} }
Logger.info('发现已关联的Companion设备: $pairedMac'); Logger.i(_tag, '发现已关联的Companion设备: $pairedMac');
// 检查当前连接设备与已保存的设备是否一致 // 检查当前连接设备与已保存的设备是否一致
if (currentDevice!.address != pairedMac) { if (currentDevice!.address != pairedMac) {
// MAC地址不匹配 // MAC地址不匹配
Logger.warning('当前连接的设备与已配对的DeepSound设备不符'); Logger.w(_tag, '当前连接的设备与已配对的DeepSound设备不符');
statusDescription.value = 'pleaseConnectPairedDevice'.tr; statusDescription.value = 'pleaseConnectPairedDevice'.tr;
return; return;
} }
@ -170,13 +170,13 @@ class PairingController extends GetxController {
} }
// 设备匹配且合规,标记为已配对 // 设备匹配且合规,标记为已配对
Logger.info('当前设备与已配对设备匹配'); Logger.i(_tag, '当前设备与已配对设备匹配');
_companionAssociated.value = true; _companionAssociated.value = true;
_userRejectedAssociation.value = false; // 重置拒绝状态 _userRejectedAssociation.value = false; // 重置拒绝状态
await _bleService.registerBackgroundScan(); await _bleService.registerBackgroundScan();
_autoNavigateToHome(); _autoNavigateToHome();
} catch (e) { } catch (e) {
Logger.error('检查设备配对状态失败: ${e.toString()}'); Logger.e(_tag, '检查设备配对状态失败: ${e.toString()}');
} }
} }
@ -198,45 +198,70 @@ class PairingController extends GetxController {
permissionsToRequest.add(Permission.bluetoothConnect); permissionsToRequest.add(Permission.bluetoothConnect);
} }
} else if (Platform.isIOS) { } else if (Platform.isIOS) {
// iOS上单独处理蓝牙权限请求,避免冲突 // iOS需要请求的权限
PermissionStatus status = await Permission.bluetooth.status; permissionsToRequest.add(Permission.bluetooth);
permissionsToRequest.add(Permission.location);
if (status.isPermanentlyDenied) { // 如果需要麦克风权限(例如语音功能)
Logger.warning('iOS蓝牙权限状态为permanentlyDenied,这可能是iOS系统误报'); // permissionsToRequest.add(Permission.microphone);
// 在iOS上打开蓝牙设置对话框 try {
Get.dialog( // 先尝试请求权限
AlertDialog( if (permissionsToRequest.isNotEmpty) {
title: Text('需要蓝牙权限'), statuses = await permissionsToRequest.request();
content: Text('请在系统设置中手动开启蓝牙权限,以便连接设备'), bool allGranted = true;
actions: [ statuses.forEach((permission, status) {
TextButton( if (!status.isGranted) {
onPressed: () => Get.back(), allGranted = false;
child: Text('稍后'), Logger.w(_tag, 'iOS权限 ${permission.toString()} 未被授予: $status');
),
TextButton( // 特殊处理蓝牙权限
onPressed: () { if (permission == Permission.bluetooth) {
Get.back(); Logger.w(_tag, 'iOS蓝牙权限未授予,状态: $status');
openAppSettings(); }
}, }
child: Text('前往设置'), });
),
], // 如果有权限未授予,但不是永久拒绝,继续尝试
), if (!allGranted) {
barrierDismissible: false, bool hasPermanentlyDenied = statuses.values.any((status) => status.isPermanentlyDenied);
); if (hasPermanentlyDenied) {
return false; // 有权限被永久拒绝,需要引导用户去设置
} else if (!status.isGranted) { _showPermissionSettingsDialog('部分权限被永久拒绝,请在设置中启用');
// 单独请求蓝牙权限
PermissionStatus result = await Permission.bluetooth.request();
if (!result.isGranted) {
Logger.warning('iOS蓝牙权限申请失败: $result');
return false; return false;
} }
} }
}
// iOS不再添加到批量请求列表 // 直接尝试使用蓝牙功能,这会触发iOS原生权限请求
// permissionsToRequest.add(Permission.bluetooth); 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;
}
} }
// 如果还有其他权限需要请求 // 如果还有其他权限需要请求
@ -247,7 +272,7 @@ class PairingController extends GetxController {
statuses.forEach((permission, status) { statuses.forEach((permission, status) {
if (!status.isGranted) { if (!status.isGranted) {
allGranted = false; allGranted = false;
Logger.warning('权限 ${permission.toString()} 未被授予: $status'); Logger.w(_tag, '权限 ${permission.toString()} 未被授予: $status');
} }
}); });
@ -260,6 +285,30 @@ class PairingController extends GetxController {
return 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版本 // 辅助方法获取Android SDK版本
Future<int> _getAndroidSdkVersion() async { Future<int> _getAndroidSdkVersion() async {
if (Platform.isAndroid) { if (Platform.isAndroid) {
@ -281,14 +330,11 @@ class PairingController extends GetxController {
_deviceWorker = _deviceWorker =
ever(_bluetoothService.currentDeviceRx, (BluetoothDevice? device) { ever(_bluetoothService.currentDeviceRx, (BluetoothDevice? device) {
if (device != null && !_isNavigatingToHome) { if (device != null && !_isNavigatingToHome) {
Logger.info('配对页面发现设备已连接: ${device.name}'); Logger.i(_tag, '配对页面发现设备已连接: ${device.name}');
_handleDeviceConnected(); _handleDeviceConnected();
} }
_updateStatusDescription(); _updateStatusDescription();
}); });
if (needdeivice) {
_handleDeviceConnected();
}
} }
/// 处理设备连接成功事件 /// 处理设备连接成功事件
@ -347,9 +393,8 @@ class PairingController extends GetxController {
/// 启动Companion设备关联流程 /// 启动Companion设备关联流程
Future<void> _startCompanionAssociation() async { Future<void> _startCompanionAssociation() async {
// 如果不是Android平台,已跳转,已完成关联或正在进行关联,直接返回 // 如果已跳转,已完成关联或正在进行关联,直接返回
if (!Platform.isAndroid || if (_isNavigatingToHome ||
_isNavigatingToHome ||
_companionAssociated.value || _companionAssociated.value ||
_isAssociating || _isAssociating ||
!isDeviceConnected) { !isDeviceConnected) {
@ -367,7 +412,7 @@ class PairingController extends GetxController {
// 检查是否支持Companion功能 // 检查是否支持Companion功能
final isSupported = await _bleService.isCompanionSupported(); final isSupported = await _bleService.isCompanionSupported();
if (!isSupported) { if (!isSupported) {
Logger.info('设备不支持Companion功能,跳过关联'); Logger.i(_tag, '设备不支持Companion功能,跳过关联');
_companionAssociated.value = true; _companionAssociated.value = true;
_autoNavigateToHome(); _autoNavigateToHome();
return; return;
@ -380,7 +425,7 @@ class PairingController extends GetxController {
return; return;
} }
Logger.info('开始Companion设备关联流程, 设备MAC: ${currentDevice!.address}'); Logger.i(_tag, '开始Companion设备关联流程, 设备MAC: ${currentDevice!.address}');
var success = await _bleService.startDeviceAssociation( var success = await _bleService.startDeviceAssociation(
deviceNamePattern: currentDevice!.name, deviceNamePattern: currentDevice!.name,
@ -389,7 +434,7 @@ class PairingController extends GetxController {
_isAssociating = false; _isAssociating = false;
if (success) { if (success) {
Logger.info('Companion设备关联成功'); Logger.i(_tag, 'Companion设备关联成功');
_companionAssociated.value = true; _companionAssociated.value = true;
_userRejectedAssociation.value = false; _userRejectedAssociation.value = false;
@ -400,14 +445,14 @@ class PairingController extends GetxController {
// 关联成功后跳转 // 关联成功后跳转
_autoNavigateToHome(); _autoNavigateToHome();
} else { } else {
Logger.error('Companion设备关联失败或被用户拒绝'); Logger.e(_tag, 'Companion设备关联失败或被用户拒绝');
// 关联失败 - 用户拒绝关联 // 关联失败 - 用户拒绝关联
_userRejectedAssociation.value = true; _userRejectedAssociation.value = true;
// 确保UI状态更新 // 确保UI状态更新
_updateStatusDescription(); _updateStatusDescription();
} }
} catch (e) { } catch (e) {
Logger.error('Companion设备关联过程发生错误: ${e.toString()}'); Logger.e(_tag, 'Companion设备关联过程发生错误: ${e.toString()}');
_isAssociating = false; _isAssociating = false;
_userRejectedAssociation.value = true; _userRejectedAssociation.value = true;
_updateStatusDescription(); _updateStatusDescription();
@ -416,13 +461,13 @@ class PairingController extends GetxController {
/// 自动跳转到主页 /// 自动跳转到主页
Future<void> _autoNavigateToHome() async { Future<void> _autoNavigateToHome() async {
if (needdeivice && (_isNavigatingToHome || !isDeviceConnected)) return; if (_isNavigatingToHome || !isDeviceConnected) return;
// 启动BLE后台服务 // 启动BLE后台服务
await _startAgentService(); await _startAgentService();
_isNavigatingToHome = true; _isNavigatingToHome = true;
Logger.info('已找到连接的设备 ${currentDevice!.name},自动跳转到主页'); Logger.i(_tag, '已找到连接的设备 ${currentDevice!.name},自动跳转到主页');
Future.delayed(const Duration(milliseconds: 500), () { Future.delayed(const Duration(milliseconds: 500), () {
Get.offAllNamed(Routes.home); Get.offAllNamed(Routes.home);
@ -432,12 +477,13 @@ class PairingController extends GetxController {
/// 启动BLE后台服务 /// 启动BLE后台服务
Future<void> _startAgentService() async { Future<void> _startAgentService() async {
try { try {
if (needdeivice && !isDeviceConnected) return; // 注释掉设备连接检查,确保无论是否连接设备都能启动服务
// if (!isDeviceConnected) return;
final result = await BleManager.to.startAgentService(); final result = await BleManager.to.startAgentService();
Logger.info(result ? 'BLE后台服务启动成功' : 'BLE后台服务启动失败'); Logger.i(_tag, result ? 'BLE后台服务启动成功' : 'BLE后台服务启动失败');
} catch (e) { } catch (e) {
Logger.error('启动BLE后台服务发生错误: ${e.toString()}'); Logger.e(_tag, '启动BLE后台服务发生错误: ${e.toString()}');
} }
} }
@ -445,9 +491,9 @@ class PairingController extends GetxController {
Future<void> _stopAgentService() async { Future<void> _stopAgentService() async {
try { try {
final result = await BleManager.to.stopAgentService(); final result = await BleManager.to.stopAgentService();
Logger.info(result ? 'BLE后台服务停止成功' : 'BLE后台服务停止失败'); Logger.i(_tag, result ? 'BLE后台服务停止成功' : 'BLE后台服务停止失败');
} catch (e) { } catch (e) {
Logger.error('停止BLE后台服务发生错误: ${e.toString()}'); Logger.e(_tag, '停止BLE后台服务发生错误: ${e.toString()}');
} }
} }
@ -470,12 +516,79 @@ class PairingController extends GetxController {
} else if (Platform.isAndroid && !isBluetoothOn) { } else if (Platform.isAndroid && !isBluetoothOn) {
AppSettings.openAppSettings(type: AppSettingsType.bluetooth); AppSettings.openAppSettings(type: AppSettingsType.bluetooth);
} else if (Platform.isIOS) { } else if (Platform.isIOS) {
// iOS上优先尝试打开蓝牙设置
if (!isBluetoothOn) {
// iOS上无法直接打开蓝牙设置,只能打开应用设置
AppSettings.openAppSettings();
} else {
// 如果蓝牙开启但有其他权限问题,打开应用设置
openAppSettings(); openAppSettings();
}
} else { } else {
AppSettings.openAppSettings(type: AppSettingsType.bluetooth); AppSettings.openAppSettings(type: AppSettingsType.bluetooth);
} }
} catch (e) { } catch (e) {
Logger.error('打开设置失败: ${e.toString()}'); Logger.e(_tag, '打开设置失败: ${e.toString()}');
}
}
/// 重新初始化蓝牙
Future<void> 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();
} }
} }
@ -488,7 +601,7 @@ class PairingController extends GetxController {
reinitializeBluetooth(); reinitializeBluetooth();
}); });
} catch (e) { } catch (e) {
Logger.error('打开应用设置失败: ${e.toString()}'); Logger.e(_tag, '打开应用设置失败: ${e.toString()}');
} }
} }
@ -501,38 +614,13 @@ class PairingController extends GetxController {
/// 跳过连接 /// 跳过连接
void skipConnecting() async { void skipConnecting() async {
// 启动BLE后台服务 // 即使没有设备连接,也启动BLE后台服务
await _startAgentService(); await _startAgentService();
_isNavigatingToHome = true; _isNavigatingToHome = true;
Get.offAllNamed(Routes.home);
}
/// 重新初始化蓝牙 // 记录跳过配对的日志
Future<void> reinitializeBluetooth() async { Logger.i(_tag, '用户跳过设备配对,直接跳转到主页');
try {
isLoading.value = true;
statusDescription.value = '正在重新初始化蓝牙...';
// iOS上尝试重置权限状态 Get.offAllNamed(Routes.home);
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();
}
} }
} }

4
lib/modules/splash/controllers/splash_controller.dart

@ -8,10 +8,6 @@ import '../../../data/models/user_Info.dart';
import '../../../routes/app_routes.dart'; import '../../../routes/app_routes.dart';
import '../../../core/themes/theme_service.dart'; import '../../../core/themes/theme_service.dart';
import '../../../data/services/network/api.dart'; import '../../../data/services/network/api.dart';
<<<<<<< HEAD
=======
import 'package:amap_service/amap_service.dart';
>>>>>>> e77ef1ff0198bb21316fa0e068b78d34f83b013c
class SplashController extends GetxController { class SplashController extends GetxController {
final GetStorage _storage = GetStorage(); final GetStorage _storage = GetStorage();

19
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.

15
local_plugins/ble_service/ios/Assets/ble_service-Info.plist

@ -1,15 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSBluetoothAlwaysUsageDescription</key>
<string>需要蓝牙权限与设备进行通信和传输音频数据</string>
<key>NSBluetoothPeripheralUsageDescription</key>
<string>需要蓝牙权限与设备进行通信和传输音频数据</string>
<key>UIBackgroundModes</key>
<array>
<string>bluetooth-central</string>
<string>audio</string>
</array>
</dict>
</plist>

195
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
}
}

71
local_plugins/ble_service/ios/Classes/BleConst.swift

@ -1,28 +1,30 @@
import Foundation import Foundation
import CoreBluetooth import CoreBluetooth
/// BLE服务常量 /**
struct BleConst { * BLE服务常量
*/
class BleConst {
// ================== 协议相关UUID ================== // ================== 协议相关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") 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") 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") 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") 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") static let NOTIFY_CHAR_UUID = CBUUID(string: "0000abc2-0000-1111-2222-123456789abc")
/// 客户端特征配置描述符UUID /** 客户端特征配置描述符UUID */
static let CLIENT_CHAR_CONFIG_UUID = CBUUID(string: "0000fd7b-0000-1000-8000-00805f9b34fb") 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" static let ACTION_SCAN_RESULT = "com.yunqiinnovation.ble_service.ACTION_BLE_SCAN"
// ================== 连接状态 ================== // ================== 连接状态 ==================
@ -33,58 +35,59 @@ struct BleConst {
static let STATE_ERROR = 4 static let STATE_ERROR = 4
// ================== 广播厂商信息 ================== // ================== 广播厂商信息 ==================
/// 厂商ID - 0xABEF /** 厂商ID - 文档中定义为0xABEF */
static let MANUFACTURER_ID: UInt16 = 0xABEF static let MANUFACTURER_ID: UInt16 = 0xABEF
/// 客户ID - 0xA001 /** 客户ID - 文档中定义为0xA001 */
static let CUSTOMER_ID: UInt16 = 0xA001 static let CUSTOMER_ID: UInt16 = 0xA001
/// 产品ID - 0xA001 /** 产品ID - 文档中定义为0xA001 */
static let PRODUCT_ID: UInt16 = 0xA001 static let PRODUCT_ID: UInt16 = 0xA001
/// 蓝牙状态-未连接 /** 蓝牙状态-未连接 */
static let BT_STATE_DISCONNECTED: UInt8 = 0x00 static let BT_STATE_DISCONNECTED: UInt8 = 0x00
/// 蓝牙状态-已连接 /** 蓝牙状态-已连接 */
static let BT_STATE_CONNECTED: UInt8 = 0x0C static let BT_STATE_CONNECTED: UInt8 = 0x0C
// ================== 协议格式 ================== // ================== 协议格式 ==================
/// APP请求帧头 /** APP请求帧头 */
static let APP_REQUEST_HEADER: UInt8 = 0xAA static let APP_REQUEST_HEADER: UInt8 = 0xAA
/// 设备响应帧头 /** 设备响应帧头 */
static let DEVICE_RESPONSE_HEADER: UInt8 = 0xBB static let DEVICE_RESPONSE_HEADER: UInt8 = 0xBB
/// 设备主动上报帧头 /** 设备主动上报帧头 */
static let DEVICE_NOTIFY_HEADER: UInt8 = 0xCC static let DEVICE_NOTIFY_HEADER: UInt8 = 0xCC
// ================== 协议指令 (CMD) ================== // ================== 协议指令 (CMD) ==================
/// 获取耳机版本号 /** 获取耳机版本号 */
static let CMD_GET_VERSION: UInt8 = 0x01 static let CMD_GET_VERSION: UInt8 = 0x01
/// 获取耳机产品ID /** 获取耳机产品ID */
static let CMD_GET_PRODUCT_ID: UInt8 = 0x02 static let CMD_GET_PRODUCT_ID: UInt8 = 0x02
/// 获取耳机颜色ID /** 获取耳机颜色ID */
static let CMD_GET_COLOR_ID: UInt8 = 0x03 static let CMD_GET_COLOR_ID: UInt8 = 0x03
/// 获取耳机电量ID /** 获取耳机电量ID */
static let CMD_GET_BATTERY_INFO: UInt8 = 0x04 static let CMD_GET_BATTERY_INFO: UInt8 = 0x04
/// APP控制编解码指令 /** APP控制编解码指令 */
static let CMD_CONTROL_CODEC: UInt8 = 0x05 static let CMD_CONTROL_CODEC: UInt8 = 0x05
/// 音量增大 /** 音量增大 */
static let CMD_VOLUME_UP: UInt8 = 0x06 static let CMD_VOLUME_UP: UInt8 = 0x06
/// 音量减小 /** 音量减小 */
static let CMD_VOLUME_DOWN: UInt8 = 0x07 static let CMD_VOLUME_DOWN: UInt8 = 0x07
/// A2DP播放 /** A2DP播放 */
static let CMD_PLAY_A2DP: UInt8 = 0x08 static let CMD_PLAY_A2DP: UInt8 = 0x08
/// 唤醒指令
/** 唤醒指令 */
static let CMD_WAKE_UP: UInt8 = 0x11 static let CMD_WAKE_UP: UInt8 = 0x11
// ================== 编解码控制值 ================== // ================== 编解码控制值 ==================
/// 关闭编解码指令 /** 关闭编解码指令 */
static let CODEC_CONTROL_CLOSE: UInt8 = 0x00 static let CODEC_CONTROL_CLOSE: UInt8 = 0x00
/// 打开解码指令 /** 音乐或者通话远端声音 */
static let CODEC_CONTROL_DECODE_ON: UInt8 = 0xA1 static let CODEC_CONTROL_DECODE_ON: UInt8 = 0xA1
/// 通过A2DP播放 /** mic和dac(音乐或者通话远端)声音 */
static let CODEC_CONTROL_A2DP_PLAY: UInt8 = 0xA2 static let CODEC_CONTROL_A2DP_PLAY: UInt8 = 0xA2
/// 打开编码指令 /** 打开编码指令 */
static let CODEC_CONTROL_ENCODE_ON: UInt8 = 0xB1 static let CODEC_CONTROL_ENCODE_ON: UInt8 = 0xB1
/// 左声道 /** 左声道 */
static let AUDIO_CHANNEL_LEFT: UInt8 = 0x01 static let AUDIO_CHANNEL_LEFT: UInt8 = 0x01
/// 右声道 /** 右声道 */
static let AUDIO_CHANNEL_RIGHT: UInt8 = 0x02 static let AUDIO_CHANNEL_RIGHT: UInt8 = 0x02
/// 立体声 /** 立体声 */
static let AUDIO_CHANNEL_STEREO: UInt8 = 0x03 static let AUDIO_CHANNEL_STEREO: UInt8 = 0x03
} }

25
local_plugins/ble_service/ios/Classes/BleDelegate.swift

@ -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])
}

1023
local_plugins/ble_service/ios/Classes/BleService.swift

File diff suppressed because it is too large

342
local_plugins/ble_service/ios/Classes/SwiftBleServicePlugin.swift

@ -1,140 +1,257 @@
import Flutter import Flutter
import UIKit import UIKit
import CoreBluetooth
import os
public class SwiftBleServicePlugin: NSObject, FlutterPlugin, BleDelegate { @available(iOS 13.0, *)
private let TAG = "BleServicePlugin" public class SwiftBleServicePlugin: NSObject, FlutterPlugin {
// MARK: - 属性
// 方法通道 private var methodChannel: FlutterMethodChannel
private var methodChannel: FlutterMethodChannel?
// 事件通道
private var dataEventSink: FlutterEventSink? private var dataEventSink: FlutterEventSink?
private var statusEventSink: FlutterEventSink? private var statusEventSink: FlutterEventSink?
// BLE服务实例 // MARK: - 初始化方法
private let bleService = BleService.shared init(_ channel: FlutterMethodChannel) {
self.methodChannel = channel
public static func register(with registrar: FlutterPluginRegistrar) { super.init()
let methodChannel = FlutterMethodChannel(name: "com.yunqiinnovation.ble_service", binaryMessenger: registrar.messenger())
// 创建插件实例 // 设置BLE服务回调
let instance = SwiftBleServicePlugin() BleService.shared.setDelegate(self)
instance.methodChannel = methodChannel
// 设置方法通道处理器 // 初始化AccessorySetup
registrar.addMethodCallDelegate(instance, channel: methodChannel) BleAccessorySetup.shared.setBleService(BleService.shared)
BleAccessorySetup.shared.setDeviceDiscoveredCallback { [weak self] uuid in
self?.handleDiscoveredDevice(uuid: uuid)
}
BleAccessorySetup.shared.initializeDiscoverySession()
}
// 设置事件通道 // MARK: - FlutterPlugin Protocol
instance.setupEventChannels(with: registrar) 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)
// 初始化BLE服务 // 注册事件通道
_ = instance.bleService.initialize() 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())
// 添加自身作为BLE回调 dataEventChannel.setStreamHandler(instance)
_ = instance.bleService.addDelegate(instance) statusEventChannel.setStreamHandler(instance)
} }
/// 设置事件通道 public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
private func setupEventChannels(with registrar: FlutterPluginRegistrar) { switch call.method {
// 数据接收事件通道 case "isConnected":
let dataEventChannel = FlutterEventChannel(name: "com.yunqiinnovation.ble_service/data_events", binaryMessenger: registrar.messenger()) result(BleService.shared.isConnected())
dataEventChannel.setStreamHandler(StreamHandler { [weak self] sink in
self?.dataEventSink = sink case "getBatteryLevel":
} onCancel: { [weak self] in // 获取电池信息,通过回调返回结果
self?.dataEventSink = nil if BleService.shared.getBatteryInfo() {
}) result(0) // 通过回调返回实际值
} else {
// 状态更新事件通道 result(FlutterError(code: "UNAVAILABLE",
let statusEventChannel = FlutterEventChannel(name: "com.yunqiinnovation.ble_service/status_events", binaryMessenger: registrar.messenger()) message: "无法获取电池信息",
statusEventChannel.setStreamHandler(StreamHandler { [weak self] sink in details: nil))
self?.statusEventSink = sink
} onCancel: { [weak self] in
self?.statusEventSink = nil
})
} }
/// 发送事件到Flutter case "isCompanionSupported":
private func sendEvent(_ eventSink: FlutterEventSink?, _ data: Any) { // 检查设备是否支持iOS 18的AccessorySetupKit功能
DispatchQueue.main.async { #if canImport(AccessorySetupKit)
eventSink?(data) 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
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { case "registerBackgroundScan":
NSLog("Method call: \(call.method)") // iOS通过AccessorySetupKit自动处理后台扫描,不需要单独注册
#if canImport(AccessorySetupKit)
if #available(iOS 18.0, *) {
// 在iOS 18+上,不需要额外操作
result(true)
} else {
// iOS版本太低
result(false)
}
#else
result(false)
#endif
switch call.method { case "unregisterBackgroundScan":
case "isConnected": // iOS通过AccessorySetupKit自动处理后台扫描,不需要单独注销
result(bleService.isConnected()) result(true)
case "getBatteryInfo": case "clearAssociations":
result(bleService.getBatteryInfo()) // 清除设备关联
#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": case "getDeviceVersion":
result(bleService.getDeviceVersion()) result(BleService.shared.getDeviceVersion())
case "getProductId": case "getProductId":
result(bleService.getProductId()) result(BleService.shared.getProductId())
case "getBatteryInfo":
result(BleService.shared.getBatteryInfo())
case "openEncoder": case "openEncoder":
result(bleService.openEncoder()) result(BleService.shared.openEncoder())
case "openDecoder": case "openDecoder":
result(bleService.openDecoder()) result(BleService.shared.openDecoder())
case "openA2DPDecoder": case "openA2DPDecoder":
result(bleService.openA2DPDecoder()) result(BleService.shared.openA2DPDecoder())
case "closeCodec": case "closeCodec":
result(bleService.closeCodec()) result(BleService.shared.closeCodec())
case "playA2DP": case "playA2DP":
result(bleService.playA2DP()) result(BleService.shared.playA2DP())
case "registerBackgroundScan":
result(bleService.registerBackgroundScan())
case "unregisterBackgroundScan":
result(bleService.unregisterBackgroundScan())
case "startBleService": // iOS 18+ AccessorySetupKit相关方法
guard let config = call.arguments as? [String: Any] else { case "startDeviceAssociation":
result(FlutterError(code: "INVALID_ARGUMENTS", message: "配置参数无效", details: nil)) #if canImport(AccessorySetupKit)
return if #available(iOS 18.0, *) {
BleAccessorySetup.shared.showDevicePicker()
result(true)
} else {
result(FlutterError(code: "UNAVAILABLE",
message: "AccessorySetupKit需要iOS 18或更高版本",
details: nil))
} }
#else
result(FlutterError(code: "UNAVAILABLE",
message: "编译环境不支持AccessorySetupKit",
details: nil))
#endif
// 处理配置参数 default:
if let address = config["mac"] as? String { result(FlutterMethodNotImplemented)
// 如果提供了设备地址,尝试连接 }
bleService.connect(address: address)
} }
// 开始扫描,寻找设备 // MARK: - 私有方法
let scanSuccess = bleService.startScan() private func handleDiscoveredDevice(uuid: UUID) {
result(scanSuccess) BleAccessorySetup.shared.handleDiscoveredAccessory(uuid: uuid)
}
case "stopBleService": private func sendEvent<T>(_ eventSink: FlutterEventSink?, data: T, errorPrefix: String) {
// 停止扫描和断开连接 if let eventSink = eventSink {
_ = bleService.stopScan() DispatchQueue.main.async {
if bleService.isConnected() { eventSink(data)
bleService.disconnect()
} }
result(true) }
}
}
default: // MARK: - FlutterStreamHandler
result(FlutterMethodNotImplemented) @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
} }
// MARK: - BleDelegate Methods 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
}
}
func onScanResult(devices: [[String: Any]]) { // MARK: - BleService.Callback
// 目前Flutter层没有直接使用此事件,可以选择不转发 @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) { func onConnectionStateChanged(state: Int) {
// 将连接状态变化通过状态通道发送 var stateStr = "Unknown"
let stateStr: String
switch state { switch state {
case BleConst.STATE_DISCONNECTED: case BleConst.STATE_DISCONNECTED:
stateStr = "Disconnected" stateStr = "Disconnected"
@ -156,54 +273,29 @@ public class SwiftBleServicePlugin: NSObject, FlutterPlugin, BleDelegate {
"stateStr": stateStr "stateStr": stateStr
] ]
sendEvent(statusEventSink, stateMap) sendEvent(statusEventSink, data: stateMap, errorPrefix: "发送连接状态异常")
} }
func onAudioDataReceived(data: Data) { func onAudioDataReceived(data: Data) {
// 将音频数据通过数据通道发送
let dataMap: [String: Any] = [ let dataMap: [String: Any] = [
"type": "audioData", "type": "audioData",
"data": FlutterStandardTypedData(bytes: data) "data": data
] ]
sendEvent(dataEventSink, dataMap) sendEvent(dataEventSink, data: dataMap, errorPrefix: "发送音频数据异常")
}
func onWakeupSignalReceived() {
// 将唤醒事件通过状态通道发送
sendEvent(statusEventSink, ["type": "wakeup"])
}
func onDeviceInfoReceived(infoType: Int, infoData: [String: Any]) {
// 将设备信息通过状态通道发送
var resultMap = infoData
resultMap["type"] = "deviceInfo"
resultMap["result"] = infoType
sendEvent(statusEventSink, resultMap)
} }
}
// MARK: - 事件处理器类
/// 简化的事件处理器类 func onDeviceInfoReceived(infoType: Int, infoData: [String : Any]) {
class StreamHandler: NSObject, FlutterStreamHandler { var dataMap: [String: Any] = [
private let onListen: (FlutterEventSink) -> Void "type": "deviceInfo",
private let onCancel: () -> Void "infoType": infoType
]
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? { // 合并infoData
onListen(events) for (key, value) in infoData {
return nil dataMap[key] = value
} }
func onCancel(withArguments arguments: Any?) -> FlutterError? { sendEvent(statusEventSink, data: dataMap, errorPrefix: "发送设备信息异常")
onCancel()
return nil
} }
} }

87
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
<!-- 蓝牙权限 -->
<key>NSBluetoothAlwaysUsageDescription</key>
<string>需要使用蓝牙连接您的耳机设备</string>
<!-- 蓝牙外设权限 -->
<key>NSBluetoothPeripheralUsageDescription</key>
<string>需要使用蓝牙连接您的耳机设备</string>
<!-- 后台蓝牙模式 -->
<key>UIBackgroundModes</key>
<array>
<string>bluetooth-central</string>
</array>
```
### 2. iOS 18+ AccessorySetupKit 配置
如果需要支持 iOS 18+ 的 AccessorySetupKit 功能(应用后台唤醒),还需要在 `Info.plist` 中添加:
```xml
<!-- 配置 AccessorySetupKit -->
<key>NSAccessorySetupBluetoothCompanyIdentifiers</key>
<array>
<string>ABEF</string> <!-- 耳机厂商ID 0xABEF -->
</array>
<key>NSAccessorySetupBluetoothServices</key>
<array>
<string>0000ABC0-0000-1111-2222-123456789ABC</string> <!-- 主服务 UUID -->
<string>0000AE00-0000-1000-8000-00805F9B34FB</string> <!-- 音频服务 UUID -->
</array>
<key>NSAccessorySetupKitSupports</key>
<array>
<string>Bluetooth</string>
</array>
```
### 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 需要与实际设备匹配

10
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. # 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| Pod::Spec.new do |s|
s.name = 'ble_service' s.name = 'ble_service'
s.version = '0.0.1' s.version = '0.0.1'
s.summary = 'BLE服务Flutter插件' s.summary = 'BLE服务插件,提供蓝牙扫描、连接和通信功能'
s.description = <<-DESC s.description = <<-DESC
BLE服务Flutter插件,提供蓝牙设备扫描、连接和通信功能。 BLE服务插件,提供蓝牙扫描、连接和通信功能,支持iOS 18+ AccessorySetupKit唤醒。
DESC DESC
s.homepage = 'http://example.com' s.homepage = 'http://example.com'
s.license = { :file => '../LICENSE' } s.license = { :file => '../LICENSE' }
s.author = { 'Your Company' => 'email@example.com' } s.author = { 'Your Company' => 'email@example.com' }
s.source = { :path => '.' } s.source = { :path => '.' }
s.source_files = 'Classes/**/*' s.source_files = 'Classes/**/*'
s.info_plist = {
'NSAccessorySetupKitSupports' => ['Bluetooth']
}
s.dependency 'Flutter' s.dependency 'Flutter'
s.platform = :ios, '11.0' s.platform = :ios, '18.0'
# Flutter.framework does not contain a i386 slice. # Flutter.framework does not contain a i386 slice.
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }

8
local_plugins/ble_service/lib/ble_service.dart

@ -161,13 +161,7 @@ class BleService {
} }
/// 启动BLE后台服务 /// 启动BLE后台服务
/// [config] 配置参数,可包含:
/// - scan_interval: 扫描间隔时间(毫秒)
/// - scan_duration: 扫描持续时间(毫秒)
/// - notification_title: 通知标题
/// - notification_content: 通知内容
/// - mac: 设备地址
/// - device_name: 设备名称
Future<bool> startBleService(Map<String, dynamic> config) async { Future<bool> startBleService(Map<String, dynamic> config) async {
try { try {
final result = final result =

2
local_plugins/ble_service/pubspec.yaml

@ -22,3 +22,5 @@ flutter:
android: android:
package: com.yunqiinnovation.ble_service package: com.yunqiinnovation.ble_service
pluginClass: BleServicePlugin pluginClass: BleServicePlugin
ios:
pluginClass: BleServicePlugin

290
local_plugins/classic_bluetooth/ios/Classes/ClassicBluetoothHelper.swift

@ -1,6 +1,6 @@
import Foundation import Foundation
import ExternalAccessory
import CoreBluetooth import CoreBluetooth
import AVFoundation
/// 蓝牙事件类型 /// 蓝牙事件类型
enum BluetoothEventType { enum BluetoothEventType {
@ -14,113 +14,205 @@ enum BluetoothState: String {
case unknown = "unknown" case unknown = "unknown"
case off = "off" case off = "off"
case on = "on" case on = "on"
case turningOn = "turningOn" case unauthorized = "unauthorized"
case turningOff = "turningOff" case unsupported = "unsupported"
case resetting = "resetting"
} }
/// 经典蓝牙助手类,负责iOS上的蓝牙设备管理 /// 简化的蓝牙助手类,专注于iOS上的蓝牙耳机检测
class ClassicBluetoothHelper: NSObject { class ClassicBluetoothHelper: NSObject {
private let TAG = "ClassicBluetoothHelper"
// 状态监控相关 // 状态监控相关
private var isMonitoring = false private var isMonitoring = false
private var stateCallback: ((BluetoothEventType, [String: Any]) -> Void)? private var stateCallback: ((BluetoothEventType, [String: Any]) -> Void)?
// 用于监控蓝牙系统状态 // 用于监控蓝牙系统状态,延迟初始化
private var centralManager: CBCentralManager? private var centralManager: CBCentralManager?
// 通知观察者 // 音频路由变化观察者
private var accessoryConnectionObserver: NSObjectProtocol? private var routeChangeObserver: NSObjectProtocol?
private var accessoryDisconnectionObserver: NSObjectProtocol?
override init() { override init() {
super.init() super.init()
centralManager = CBCentralManager(delegate: nil, queue: nil, options: [CBCentralManagerOptionShowPowerAlertKey: false]) // 初始化时不立即创建centralManager,延迟到实际需要时
} }
deinit { deinit {
stopBluetoothStateMonitoring() stopBluetoothStateMonitoring()
} }
/// 检查蓝牙是否启用 /// 延迟初始化CBCentralManager,只在实际需要时创建
private func initializeCentralManagerIfNeeded() {
if centralManager == nil {
centralManager = CBCentralManager(
delegate: self,
queue: DispatchQueue.main,
options: [CBCentralManagerOptionShowPowerAlertKey: true]
)
}
}
/// 检查蓝牙是否启用,此方法会触发权限请求
func isBluetoothEnabled() -> Bool { 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) { func getConnectedDevices(completion: @escaping ([[String: String]], Error?) -> Void) {
// 获取所有已连接的外部配件 // 确保蓝牙已初始化
let accessoryManager = EAAccessoryManager.shared() initializeCentralManagerIfNeeded()
let connectedAccessories = accessoryManager.connectedAccessories
// 获取音频设备(主要是蓝牙耳机)
// 将配件转换为设备信息数组 let audioDevices = getConnectedAudioDevices()
let devices = connectedAccessories.map { accessory -> [String: String] in
return [ // 日志输出
"name": accessory.name, if !audioDevices.isEmpty {
"address": accessory.serialNumber // iOS使用序列号作为唯一标识 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)
}
return result
} }
// 返回结果 // 检查是否为蓝牙输出
completion(devices, nil) 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[..<range.lowerBound])
}
return address
} }
/// 开始监听蓝牙状态变化 /// 开始监听蓝牙状态变化
/// - Parameter callback: 状态变化回调
func startBluetoothStateMonitoring(callback: @escaping (BluetoothEventType, [String: Any]) -> Void) { func startBluetoothStateMonitoring(callback: @escaping (BluetoothEventType, [String: Any]) -> Void) {
self.stateCallback = callback self.stateCallback = callback
if !isMonitoring { if !isMonitoring {
isMonitoring = true isMonitoring = true
// 设置蓝牙中央管理器代理 // 初始化蓝牙管理器
centralManager?.delegate = self initializeCentralManagerIfNeeded()
// 注册蓝牙配件连接/断开通知 // 观察音频路由变化
let notificationCenter = NotificationCenter.default let notificationCenter = NotificationCenter.default
routeChangeObserver = notificationCenter.addObserver(
// 观察配件连接 forName: AVAudioSession.routeChangeNotification,
accessoryConnectionObserver = notificationCenter.addObserver(
forName: NSNotification.Name.EAAccessoryDidConnect,
object: nil, object: nil,
queue: nil queue: nil
) { [weak self] notification in ) { [weak self] notification in
guard let self = self, self?.handleAudioRouteChange(notification)
let accessory = notification.userInfo?[EAAccessoryKey] as? EAAccessory else {
return
} }
let device: [String: String] = [ // 激活音频会话
"name": accessory.name, activateAudioSession()
"address": accessory.serialNumber }
]
self.stateCallback?(.deviceConnected, ["device": device])
} }
// 观察配件断开 // 处理音频路由变化
accessoryDisconnectionObserver = notificationCenter.addObserver( private func handleAudioRouteChange(_ notification: Notification) {
forName: NSNotification.Name.EAAccessoryDidDisconnect, guard let userInfo = notification.userInfo,
object: nil, let reasonValue = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt,
queue: nil let reason = AVAudioSession.RouteChangeReason(rawValue: reasonValue) else {
) { [weak self] notification in
guard let self = self,
let accessory = notification.userInfo?[EAAccessoryKey] as? EAAccessory else {
return 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] = [ let device: [String: String] = [
"name": accessory.name, "name": output.portName,
"address": accessory.serialNumber "address": cleanDeviceAddress(output.uid)
] ]
stateCallback?(.deviceDisconnected, ["device": device])
}
}
self.stateCallback?(.deviceDisconnected, ["device": device]) default:
break
}
} }
// 开始监视配件变化 // 激活音频会话
EAAccessoryManager.shared().registerForLocalNotifications() 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 { if isMonitoring {
isMonitoring = false isMonitoring = false
// 移除蓝牙中央管理器代理 // 移除音频路由观察者
centralManager?.delegate = nil if let observer = routeChangeObserver {
// 注销配件通知观察者
if let observer = accessoryConnectionObserver {
NotificationCenter.default.removeObserver(observer) NotificationCenter.default.removeObserver(observer)
accessoryConnectionObserver = nil routeChangeObserver = nil
} }
if let observer = accessoryDisconnectionObserver { // 关闭音频会话
NotificationCenter.default.removeObserver(observer) deactivateAudioSession()
accessoryDisconnectionObserver = nil
}
// 停止监视配件变化
EAAccessoryManager.shared().unregisterForLocalNotifications()
} }
// 清除回调 // 清除回调
stateCallback = nil stateCallback = nil
} }
// 关闭音频会话
private func deactivateAudioSession() {
do {
try AVAudioSession.sharedInstance().setActive(false)
} catch {
print("关闭音频会话失败: \(error.localizedDescription)")
}
}
/// 报告当前蓝牙状态 /// 报告当前蓝牙状态
func reportCurrentBluetoothState() { func reportCurrentBluetoothState() {
// 如果centralManager未初始化,不触发状态报告
guard let centralManager = centralManager else { return } guard let centralManager = centralManager else { return }
let state: BluetoothState // 直接调用delegate方法保持状态处理一致性
centralManagerDidUpdateState(centralManager)
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])
} }
} }
@ -179,19 +259,47 @@ extension ClassicBluetoothHelper: CBCentralManagerDelegate {
func centralManagerDidUpdateState(_ central: CBCentralManager) { func centralManagerDidUpdateState(_ central: CBCentralManager) {
let state: BluetoothState let state: BluetoothState
// 添加详细日志输出
print("iOS蓝牙状态原始值: \(central.state.rawValue)")
switch central.state { switch central.state {
case .poweredOn: case .poweredOn:
print("蓝牙状态: 已开启(.poweredOn)")
state = .on state = .on
case .poweredOff: case .poweredOff:
print("蓝牙状态: 已关闭(.poweredOff)")
state = .off state = .off
case .unauthorized:
print("蓝牙状态: 未授权(.unauthorized)")
state = .unauthorized
case .unsupported:
print("蓝牙状态: 不支持(.unsupported)")
state = .unsupported
case .resetting: case .resetting:
state = .turningOff print("蓝牙状态: 重置中(.resetting)")
case .unauthorized, .unsupported: state = .resetting
state = .off case .unknown:
default: print("蓝牙状态: 未知(.unknown)")
state = .unknown
@unknown default:
print("蓝牙状态: 未处理的新状态(\(central.state.rawValue))")
state = .unknown 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])
} }
} }

136
local_plugins/classic_bluetooth/ios/Classes/FileLogger.swift

@ -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
}
}

42
local_plugins/classic_bluetooth/ios/Classes/SwiftClassicBluetoothPlugin.swift

@ -1,6 +1,7 @@
import Flutter import Flutter
import UIKit import UIKit
import ExternalAccessory import ExternalAccessory
import AVFoundation
// 这个文件是为了满足Flutter插件的命名约定 // 这个文件是为了满足Flutter插件的命名约定
// 实际实现在ClassicBluetoothPlugin.swift中 // 实际实现在ClassicBluetoothPlugin.swift中
@ -22,6 +23,9 @@ public class SwiftClassicBluetoothPlugin: NSObject, FlutterPlugin {
instance.methodChannel = channel instance.methodChannel = channel
instance.eventChannel = eventChannel instance.eventChannel = eventChannel
// 记录插件初始化
print("经典蓝牙插件初始化成功")
} }
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
@ -36,18 +40,37 @@ public class SwiftClassicBluetoothPlugin: NSObject, FlutterPlugin {
} }
private func getConnectedDevices(result: @escaping FlutterResult) { private func getConnectedDevices(result: @escaping FlutterResult) {
// 先检查蓝牙状态
if !bluetoothHelper.isBluetoothEnabled() { if !bluetoothHelper.isBluetoothEnabled() {
print("蓝牙未启用,无法获取设备")
result([]) result([])
return return
} }
bluetoothHelper.getConnectedDevices { devices, error in bluetoothHelper.getConnectedDevices { devices, error in
if let error = error { if let error = error {
print("获取设备失败: \(error.localizedDescription)")
result(FlutterError(code: "BLUETOOTH_ERROR", message: error.localizedDescription, details: nil)) result(FlutterError(code: "BLUETOOTH_ERROR", message: error.localizedDescription, details: nil))
return 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 { switch eventType {
case .stateChanged: case .stateChanged:
if let state = data["state"] as? String { if let state = data["state"] as? String {
print("蓝牙状态变化: \(state)")
self.sendEvent([ self.sendEvent([
"type": "stateChanged", "type": "stateChanged",
"state": state, "state": state,
@ -79,17 +103,29 @@ extension SwiftClassicBluetoothPlugin: FlutterStreamHandler {
} }
case .deviceConnected: case .deviceConnected:
if let device = data["device"] as? [String: String] { 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([ self.sendEvent([
"type": "deviceConnected", "type": "deviceConnected",
"device": device, "device": deviceData,
"timestamp": Int(Date().timeIntervalSince1970 * 1000) "timestamp": Int(Date().timeIntervalSince1970 * 1000)
]) ])
} }
case .deviceDisconnected: case .deviceDisconnected:
if let device = data["device"] as? [String: String] { 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([ self.sendEvent([
"type": "deviceDisconnected", "type": "deviceDisconnected",
"device": device, "device": deviceData,
"timestamp": Int(Date().timeIntervalSince1970 * 1000) "timestamp": Int(Date().timeIntervalSince1970 * 1000)
]) ])
} }

BIN
local_plugins/opus/.gradle/8.10/fileHashes/fileHashes.lock

Binary file not shown.

BIN
local_plugins/opus/.gradle/buildOutputCleanup/buildOutputCleanup.lock

Binary file not shown.

2
local_plugins/opus/.gradle/buildOutputCleanup/cache.properties

@ -1,2 +1,2 @@
#Sat May 17 17:27:45 IST 2025 #Sun May 18 21:39:38 IST 2025
gradle.version=8.10 gradle.version=8.10

45
test.md

@ -0,0 +1,45 @@
请帮我生成一个基于 GetX 最佳实践的 Flutter 应用项目骨架。要求如下:
1. 目录结构
- lib/
- main.dart
- app/
- data/
- providers/ # 网络请求、API 客户端(使用 GetConnect)
- repositories/ # Repository 层,封装业务数据源
- services/ # 全局服务(如 ThemeService、AuthService)
- modules/
- home/ # 示例模块
- bindings/ # HomeBinding,注册 Controller、Service
- controllers/ # HomeController
- models/ # HomeModel 数据类
- views/ # HomePage 页面
- routes/
- app_routes.dart # 路由常量
- app_pages.dart # GetPage 列表,包括中间件示例
- translations/ # 多语言文件(en_US、zh_CN)和 Translations 实现
- widgets/ # 公共 Widget(Loading、ErrorView)
- utils/ # 日志 Logger、网络拦截器等工具函数
2. main.dart
- 使用 GetMaterialApp
- 配置 initialRoute、getPages、theme、darkTheme、locale、translations
3. 依赖注入(Bindings)
- 在 app/modules/**/bindings 中,用 Get.lazyPut、Get.putAsync、Get.create 分别示例单例、异步初始化和工厂模式
4. 状态管理
- 在 Controller 中使用 Rx<T> + .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” 分层
- 各层代码注释职责
- 代码风格统一、注释清晰
Loading…
Cancel
Save