Browse Source

上传iOS 代码修复

weicu
liwei1dao 1 year ago
parent
commit
74df1fdd81
  1. 8
      ios/Podfile.lock
  2. 1
      ios/Runner.xcodeproj/project.pbxproj
  3. 64
      ios/Runner/Info.plist
  4. 9
      lib/modules/ble/bindings/ble_binding.dart
  5. 281
      lib/modules/ble/controllers/ble_controller.dart
  6. 355
      lib/modules/ble/views/ble_view.dart
  7. 14
      lib/modules/login/views/login_view.dart
  8. 54
      lib/modules/pairing/controllers/pairing_controller.dart
  9. 8
      lib/routes/app_pages.dart
  10. 3
      lib/routes/app_routes.dart
  11. 506
      local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleAccessorySetup.swift
  12. 1144
      local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleScanManager.swift
  13. 191
      local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleService.swift
  14. 216
      local_plugins/ble_service/ios/ble_service/Sources/ble_service/SwiftBleServicePlugin.swift
  15. 29
      local_plugins/user_portrait/ios/user_portrait.podspec
  16. 24
      local_plugins/user_portrait/ios/user_portrait/Package.swift
  17. 14
      local_plugins/user_portrait/ios/user_portrait/Sources/user_portrait/PrivacyInfo.xcprivacy
  18. 4
      local_plugins/user_portrait/ios/user_portrait/Sources/user_portrait/UserPortraitPlugin.swift
  19. 46
      local_plugins/user_portrait/pubspec.yaml
  20. 27
      local_plugins/user_portrait/test/user_portrait_method_channel_test.dart
  21. 29
      local_plugins/user_portrait/test/user_portrait_test.dart
  22. 1
      pubspec.yaml

8
ios/Podfile.lock

@ -77,9 +77,9 @@ PODS:
- QCloudTrack/Beacon (6.4.7)
- record_darwin (1.0.0):
- Flutter
- SDWebImage (5.21.0):
- SDWebImage/Core (= 5.21.0)
- SDWebImage/Core (5.21.0)
- SDWebImage (5.21.1):
- SDWebImage/Core (= 5.21.1)
- SDWebImage/Core (5.21.1)
- SDWebImageWebPCoder (0.14.6):
- libwebp (~> 1.0)
- SDWebImage/Core (~> 5.17)
@ -200,7 +200,7 @@ SPEC CHECKSUMS:
QCloudCOSXML: 7205e76aa9cf613468222615483c9d7c074e4298
QCloudTrack: 3b53a7fc4fe3920e407f2aa73f2452992a61f7f3
record_darwin: fb1f375f1d9603714f55b8708a903bbb91ffdb0a
SDWebImage: f84b0feeb08d2d11e6a9b843cb06d75ebf5b8868
SDWebImage: f29024626962457f3470184232766516dee8dfea
SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380
spotify_sdk: a48400bb29f70c4fe251ebfdc9135c37097ac5ca
tencentcloud_cos_sdk_plugin: 7bed564dbe72df23e7b5cacb1e51b1a20cb1639c

1
ios/Runner.xcodeproj/project.pbxproj

@ -549,6 +549,7 @@
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 537SFY28XS;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
MARKETING_VERSION = 1.0;

64
ios/Runner/Info.plist

@ -72,66 +72,10 @@
</array>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSAccessorySetupBluetoothCompanyIdentifiers</key>
<array>
<string>EFAB</string>
</array>
<key>NSAccessorySetupDescriptions</key>
<array>
<dict>
<key>AccessoryMediaType</key>
<string>Headphones</string>
<key>LocalizedAccessoryName</key>
<string>语音助理耳机</string>
</dict>
</array>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
<key>NSAllowsArbitraryLoadsInWebContent</key>
<true/>
<key>NSExceptionDomains</key>
<dict>
<key>mcp.amap.com</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>NSExceptionMinimumTLSVersion</key>
<string>TLSv1.0</string>
<key>NSIncludesSubdomains</key>
<true/>
</dict>
<key>mcp.ideapsound.com</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>NSExceptionMinimumTLSVersion</key>
<string>TLSv1.0</string>
<key>NSIncludesSubdomains</key>
<true/>
</dict>
</dict>
</dict>
<!-- 蓝牙权限描述 -->
<key>NSBluetoothAlwaysUsageDescription</key>
<string>需要蓝牙权限用于连接和管理音频设备</string>
<key>NSBluetoothPeripheralUsageDescription</key>
<string>需要蓝牙权限用于连接和管理音频设备</string>
<!-- 后台模式支持 -->
<key>UIBackgroundModes</key>
<array>
<string>bluetooth-central</string>
<string>audio</string>
<string>location</string>
</array>
<!-- AccessorySetup支持(用于配对) -->
<key>NSAccessorySetupKitSupports</key>
<array>
<string>Bluetooth</string>
</array>
<key>NSCalendarsUsageDescription</key>
<string>需要日历权限用于安排和管理您的音频会议和录音任务</string>
<key>NSCameraUsageDescription</key>
@ -148,6 +92,12 @@
<string>需要相册权限用于保存录音文件和导入音频文件</string>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UIBackgroundModes</key>
<array>
<string>bluetooth-central</string>
<string>audio</string>
<string>location</string>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
@ -165,5 +115,7 @@
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
</dict>
</plist>

9
lib/modules/ble/bindings/ble_binding.dart

@ -0,0 +1,9 @@
import 'package:get/get.dart';
import '../controllers/ble_controller.dart';
class BleBinding extends Bindings {
@override
void dependencies() {
Get.put<BleController>(BleController());
}
}

281
lib/modules/ble/controllers/ble_controller.dart

@ -0,0 +1,281 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_blue_plus/flutter_blue_plus.dart' as fbp;
import 'package:permission_handler/permission_handler.dart';
import 'package:app_settings/app_settings.dart';
class BleController extends GetxController {
// 扫描状态
final RxBool isScanning = false.obs;
final RxBool isBluetoothOn = false.obs;
final RxBool hasPermissions = false.obs;
// 发现的设备列表
final RxList<fbp.ScanResult> scanResults = <fbp.ScanResult>[].obs;
// 连接的设备
final Rx<fbp.BluetoothDevice?> connectedDevice =
Rx<fbp.BluetoothDevice?>(null);
// 错误信息
final RxString errorMessage = ''.obs;
@override
void onInit() {
super.onInit();
_initBluetooth();
}
@override
void onClose() {
stopScan();
super.onClose();
}
// 初始化蓝牙
Future<void> _initBluetooth() async {
try {
// 先检查权限
await _checkPermissions();
// 检查蓝牙适配器状态
fbp.FlutterBluePlus.adapterState.listen((state) {
isBluetoothOn.value = state == fbp.BluetoothAdapterState.on;
if (state == fbp.BluetoothAdapterState.on) {
errorMessage.value = '';
} else {
errorMessage.value = '请开启蓝牙';
stopScan();
}
});
// 监听扫描结果
fbp.FlutterBluePlus.scanResults.listen((results) {
scanResults.value = results;
});
// 监听扫描状态
fbp.FlutterBluePlus.isScanning.listen((scanning) {
isScanning.value = scanning;
});
} catch (e) {
errorMessage.value = '蓝牙初始化失败: $e';
}
}
// 检查权限(不请求)
Future<void> _checkPermissions() async {
try {
Map<Permission, PermissionStatus> statuses = await [
Permission.bluetoothScan,
Permission.bluetoothConnect,
Permission.location,
].request();
hasPermissions.value =
statuses.values.every((status) => status == PermissionStatus.granted);
if (!hasPermissions.value) {
errorMessage.value = '需要蓝牙和位置权限才能扫描设备';
}
} catch (e) {
print('检查权限失败: $e');
hasPermissions.value = false;
}
}
// 请求权限
Future<bool> _requestPermissions() async {
try {
Map<Permission, PermissionStatus> statuses = await [
Permission.bluetoothScan,
Permission.bluetoothConnect,
Permission.location,
].request();
bool allGranted =
statuses.values.every((status) => status == PermissionStatus.granted);
hasPermissions.value = allGranted;
if (!allGranted) {
// 检查是否有权限被永久拒绝
bool hasPermanentlyDenied = statuses.values
.any((status) => status == PermissionStatus.permanentlyDenied);
if (hasPermanentlyDenied) {
_showPermissionDialog();
} else {
errorMessage.value = '需要蓝牙和位置权限才能扫描设备';
}
}
return allGranted;
} catch (e) {
print('请求权限失败: $e');
errorMessage.value = '权限请求失败: $e';
return false;
}
}
// 显示权限对话框
void _showPermissionDialog() {
Get.dialog(
AlertDialog(
title: Text('需要权限'),
content: Text('应用需要蓝牙和位置权限才能扫描设备。请在设置中手动开启权限。'),
actions: [
TextButton(
onPressed: () => Get.back(),
child: Text('取消'),
),
TextButton(
onPressed: () {
Get.back();
AppSettings.openAppSettings();
},
child: Text('去设置'),
),
],
),
);
}
// 显示蓝牙开启对话框
void _showBluetoothDialog() {
Get.dialog(
AlertDialog(
title: Text('需要开启蓝牙'),
content: Text('请开启蓝牙以扫描和连接设备。'),
actions: [
TextButton(
onPressed: () => Get.back(),
child: Text('取消'),
),
TextButton(
onPressed: () {
Get.back();
// 尝试开启蓝牙
fbp.FlutterBluePlus.turnOn();
},
child: Text('开启蓝牙'),
),
],
),
);
}
// 开始扫描
Future<void> startScan() async {
try {
errorMessage.value = '';
// 检查权限
if (!hasPermissions.value) {
if (!await _requestPermissions()) {
return;
}
}
// 检查蓝牙状态
if (!isBluetoothOn.value) {
_showBluetoothDialog();
return;
}
// 清空之前的结果
scanResults.clear();
// 开始扫描
await fbp.FlutterBluePlus.startScan(
timeout: const Duration(seconds: 30),
androidUsesFineLocation: true,
);
} catch (e) {
errorMessage.value = '扫描失败: $e';
}
}
// 停止扫描
Future<void> stopScan() async {
try {
await fbp.FlutterBluePlus.stopScan();
} catch (e) {
print('停止扫描失败: $e');
}
}
// 连接设备
Future<void> connectToDevice(fbp.BluetoothDevice device) async {
try {
errorMessage.value = '';
// 停止扫描
await stopScan();
// 连接设备
await device.connect(timeout: const Duration(seconds: 15));
connectedDevice.value = device;
Get.snackbar(
'连接成功',
'已连接到 ${device.platformName.isNotEmpty ? device.platformName : '未知设备'}',
backgroundColor: Colors.green,
colorText: Colors.white,
);
} catch (e) {
errorMessage.value = '连接失败: $e';
Get.snackbar(
'连接失败',
e.toString(),
backgroundColor: Colors.red,
colorText: Colors.white,
);
}
}
// 断开连接
Future<void> disconnectDevice() async {
try {
if (connectedDevice.value != null) {
await connectedDevice.value!.disconnect();
connectedDevice.value = null;
Get.snackbar(
'已断开连接',
'设备已断开连接',
backgroundColor: Colors.orange,
colorText: Colors.white,
);
}
} catch (e) {
errorMessage.value = '断开连接失败: $e';
}
}
// 获取设备名称
String getDeviceName(fbp.ScanResult result) {
if (result.device.platformName.isNotEmpty) {
return result.device.platformName;
}
if (result.advertisementData.localName.isNotEmpty) {
return result.advertisementData.localName;
}
return '未知设备';
}
// 获取信号强度描述
String getSignalStrengthText(int rssi) {
if (rssi >= -50) return '信号强';
if (rssi >= -70) return '信号中等';
if (rssi >= -90) return '信号弱';
return '信号很弱';
}
// 获取信号强度图标
IconData getSignalStrengthIcon(int rssi) {
if (rssi >= -50) return Icons.signal_wifi_4_bar;
if (rssi >= -70) return Icons.signal_wifi_4_bar;
if (rssi >= -90) return Icons.signal_wifi_4_bar;
return Icons.signal_wifi_4_bar;
}
}

355
lib/modules/ble/views/ble_view.dart

@ -0,0 +1,355 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_blue_plus/flutter_blue_plus.dart' as fbp;
import '../controllers/ble_controller.dart';
class BleView extends GetView<BleController> {
const BleView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final bool isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Scaffold(
backgroundColor: isDarkMode ? Colors.black : Colors.white,
appBar: AppBar(
title: Text(
'BLE设备扫描',
style: TextStyle(
color: isDarkMode ? Colors.white : Colors.black,
fontSize: 18.sp,
fontWeight: FontWeight.bold,
),
),
backgroundColor: isDarkMode ? Colors.grey[900] : Colors.white,
elevation: 0,
iconTheme: IconThemeData(
color: isDarkMode ? Colors.white : Colors.black,
),
actions: [
Obx(() => IconButton(
onPressed: controller.isScanning.value
? controller.stopScan
: controller.startScan,
icon: Icon(
controller.isScanning.value ? Icons.stop : Icons.refresh,
color: controller.isScanning.value
? Colors.red
: (isDarkMode ? Colors.white : Colors.black),
),
)),
],
),
body: SafeArea(
child: Column(
children: [
// 状态栏
_buildStatusBar(isDarkMode),
// 连接的设备信息
_buildConnectedDevice(isDarkMode),
// 设备列表
Expanded(
child: _buildDeviceList(isDarkMode),
),
],
),
),
floatingActionButton: Obx(() => FloatingActionButton(
onPressed: controller.isScanning.value
? controller.stopScan
: controller.startScan,
backgroundColor:
controller.isScanning.value ? Colors.red : Colors.blue,
child: Icon(
controller.isScanning.value
? Icons.stop
: Icons.bluetooth_searching,
color: Colors.white,
),
)),
);
}
// 状态栏
Widget _buildStatusBar(bool isDarkMode) {
return Obx(() => Container(
width: double.infinity,
padding: EdgeInsets.all(16.w),
margin: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[800] : Colors.grey[100],
borderRadius: BorderRadius.circular(12.r),
),
child: Column(
children: [
Row(
children: [
Icon(
controller.isBluetoothOn.value
? Icons.bluetooth
: Icons.bluetooth_disabled,
color: controller.isBluetoothOn.value
? Colors.blue
: Colors.red,
size: 24.w,
),
SizedBox(width: 8.w),
Text(
controller.isBluetoothOn.value ? '蓝牙已开启' : '蓝牙已关闭',
style: TextStyle(
color: isDarkMode ? Colors.white : Colors.black,
fontSize: 16.sp,
fontWeight: FontWeight.w500,
),
),
Spacer(),
if (controller.isScanning.value)
SizedBox(
width: 20.w,
height: 20.w,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.blue),
),
),
],
),
if (controller.errorMessage.value.isNotEmpty) ...[
SizedBox(height: 8.h),
Text(
controller.errorMessage.value,
style: TextStyle(
color: Colors.red,
fontSize: 14.sp,
),
),
],
if (controller.isScanning.value) ...[
SizedBox(height: 8.h),
Text(
'正在扫描设备...',
style: TextStyle(
color: Colors.blue,
fontSize: 14.sp,
),
),
],
],
),
));
}
// 已连接设备信息
Widget _buildConnectedDevice(bool isDarkMode) {
return Obx(() {
if (controller.connectedDevice.value == null) {
return SizedBox.shrink();
}
final device = controller.connectedDevice.value!;
return Container(
width: double.infinity,
padding: EdgeInsets.all(16.w),
margin: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h),
decoration: BoxDecoration(
color: Colors.green.withOpacity(0.1),
border: Border.all(color: Colors.green, width: 1),
borderRadius: BorderRadius.circular(12.r),
),
child: Row(
children: [
Icon(
Icons.bluetooth_connected,
color: Colors.green,
size: 24.w,
),
SizedBox(width: 12.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'已连接设备',
style: TextStyle(
color: Colors.green,
fontSize: 12.sp,
fontWeight: FontWeight.w500,
),
),
Text(
device.platformName.isNotEmpty
? device.platformName
: '未知设备',
style: TextStyle(
color: isDarkMode ? Colors.white : Colors.black,
fontSize: 16.sp,
fontWeight: FontWeight.bold,
),
),
Text(
device.remoteId.toString(),
style: TextStyle(
color: isDarkMode ? Colors.white70 : Colors.black54,
fontSize: 12.sp,
),
),
],
),
),
IconButton(
onPressed: controller.disconnectDevice,
icon: Icon(
Icons.close,
color: Colors.red,
size: 20.w,
),
),
],
),
);
});
}
// 设备列表
Widget _buildDeviceList(bool isDarkMode) {
return Obx(() {
if (controller.scanResults.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.bluetooth_searching,
size: 64.w,
color: isDarkMode ? Colors.white54 : Colors.black54,
),
SizedBox(height: 16.h),
Text(
controller.isScanning.value ? '正在搜索设备...' : '点击扫描按钮开始搜索BLE设备',
style: TextStyle(
color: isDarkMode ? Colors.white54 : Colors.black54,
fontSize: 16.sp,
),
textAlign: TextAlign.center,
),
],
),
);
}
return ListView.builder(
padding: EdgeInsets.symmetric(horizontal: 16.w),
itemCount: controller.scanResults.length,
itemBuilder: (context, index) {
final result = controller.scanResults[index];
return _buildDeviceItem(result, isDarkMode);
},
);
});
}
// 设备项
Widget _buildDeviceItem(fbp.ScanResult result, bool isDarkMode) {
final deviceName = controller.getDeviceName(result);
final isConnected =
controller.connectedDevice.value?.remoteId == result.device.remoteId;
return Container(
margin: EdgeInsets.only(bottom: 12.h),
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[800] : Colors.white,
borderRadius: BorderRadius.circular(12.r),
border: Border.all(
color: isConnected
? Colors.green
: (isDarkMode ? Colors.grey[700]! : Colors.grey[300]!),
width: isConnected ? 2 : 1,
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 8,
offset: Offset(0, 2),
),
],
),
child: ListTile(
contentPadding: EdgeInsets.all(16.w),
leading: Container(
width: 48.w,
height: 48.w,
decoration: BoxDecoration(
color: isConnected
? Colors.green.withOpacity(0.1)
: Colors.blue.withOpacity(0.1),
borderRadius: BorderRadius.circular(24.r),
),
child: Icon(
isConnected ? Icons.bluetooth_connected : Icons.bluetooth,
color: isConnected ? Colors.green : Colors.blue,
size: 24.w,
),
),
title: Text(
deviceName,
style: TextStyle(
color: isDarkMode ? Colors.white : Colors.black,
fontSize: 16.sp,
fontWeight: FontWeight.bold,
),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 4.h),
Text(
result.device.remoteId.toString(),
style: TextStyle(
color: isDarkMode ? Colors.white70 : Colors.black54,
fontSize: 12.sp,
),
),
SizedBox(height: 4.h),
Row(
children: [
Icon(
controller.getSignalStrengthIcon(result.rssi),
size: 16.w,
color: isDarkMode ? Colors.white70 : Colors.black54,
),
SizedBox(width: 4.w),
Text(
'${result.rssi} dBm (${controller.getSignalStrengthText(result.rssi)})',
style: TextStyle(
color: isDarkMode ? Colors.white70 : Colors.black54,
fontSize: 12.sp,
),
),
],
),
],
),
trailing: isConnected
? Icon(
Icons.check_circle,
color: Colors.green,
size: 24.w,
)
: IconButton(
onPressed: () => controller.connectToDevice(result.device),
icon: Icon(
Icons.connect_without_contact,
color: Colors.blue,
size: 24.w,
),
),
onTap: isConnected
? null
: () => controller.connectToDevice(result.device),
),
);
}
}

14
lib/modules/login/views/login_view.dart

@ -39,13 +39,13 @@ class LoginView extends GetView<LoginController> {
SizedBox(height: 32.h),
_buildWelcomeText(isDarkMode),
// 输入框和继续按钮
SizedBox(height: 24.h),
_buildInputField(isDarkMode),
SizedBox(height: 16.h),
_buildVerificationInputField(isDarkMode),
SizedBox(height: 16.h),
_buildContinueButton(isDarkMode),
// // 输入框和继续按钮
// SizedBox(height: 24.h),
// _buildInputField(isDarkMode),
// SizedBox(height: 16.h),
// _buildVerificationInputField(isDarkMode),
// SizedBox(height: 16.h),
// _buildContinueButton(isDarkMode),
// 社交媒体登录
SizedBox(height: 32.h),

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

@ -1,6 +1,3 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:app_settings/app_settings.dart';
import '/data/models/appconfig.dart';
import '/data/models/user_Info.dart';
@ -14,7 +11,6 @@ import '../../../core/utils/logger.dart';
import 'dart:io';
import '../../../data/services/bluetooth_manager.dart';
import '../../../data/services/ble_manager.dart';
import '../../../core/utils/permission_util.dart';
/// 配对状态枚举 - 简化版本
enum PairingState {
@ -418,46 +414,16 @@ class PairingController extends GetxController {
/// 检查权限
Future<bool> _checkPermissions() async {
if (Platform.isAndroid) {
// Android 平台权限检查
Map<Permission, PermissionStatus> statuses = await [
Permission.bluetoothScan,
Permission.bluetoothConnect,
Permission.locationWhenInUse,
].request();
return statuses[Permission.bluetoothScan]!.isGranted &&
statuses[Permission.bluetoothConnect]!.isGranted &&
statuses[Permission.locationWhenInUse]!.isGranted;
} else if (Platform.isIOS) {
// iOS 平台权限检查
Map<Permission, PermissionStatus> statuses = await [
Permission.locationWhenInUse,
Permission.bluetooth, // 添加蓝牙权限检查
].request();
// 检查位置权限
bool locationGranted = statuses[Permission.locationWhenInUse]!.isGranted;
// 检查蓝牙权限
bool bluetoothGranted = statuses[Permission.bluetooth]!.isGranted;
// 如果蓝牙权限未确定,强制触发权限申请
if (!bluetoothGranted) {
try {
await _bleManager.requestBluetoothPermission();
// 重新检查蓝牙权限状态
final newBluetoothStatus = await Permission.bluetooth.status;
bluetoothGranted = newBluetoothStatus.isGranted;
} catch (e) {
Logger.e(_tag, '蓝牙权限申请失败: $e');
}
}
return locationGranted && bluetoothGranted;
}
return true;
// Android 平台权限检查
Map<Permission, PermissionStatus> statuses = await [
Permission.bluetoothScan,
Permission.bluetoothConnect,
Permission.locationWhenInUse,
].request();
return statuses[Permission.bluetoothScan]!.isGranted &&
statuses[Permission.bluetoothConnect]!.isGranted &&
statuses[Permission.locationWhenInUse]!.isGranted;
}
/// 验证设备

8
lib/routes/app_pages.dart

@ -1,3 +1,6 @@
import 'package:deep_voice/modules/ble/bindings/ble_binding.dart';
import 'package:deep_voice/modules/ble/views/ble_view.dart';
import '../../modules/devices/bindings/devices_binding.dart';
import '../../modules/devices/views/devices_view.dart';
import 'package:get/get.dart';
@ -249,5 +252,10 @@ abstract class AppPages {
page: () => const DevicesView(),
binding: DevicesBinding(),
),
GetPage(
name: Routes.ble,
page: () => const BleView(),
binding: BleBinding(),
),
];
}

3
lib/routes/app_routes.dart

@ -56,4 +56,7 @@ abstract class Routes {
// 设备管理
static const devices = '/devices';
// ble设备管理
static const ble = '/ble';
}

506
local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleAccessorySetup.swift

@ -1,306 +1,306 @@
import Foundation
import CoreBluetooth
import os.log
import AVFoundation
import AccessorySetupKit
import UIKit
// import Foundation
// import CoreBluetooth
// import os.log
// import AVFoundation
// import AccessorySetupKit
// import UIKit
@available(iOS 18.0, *)
class BleAccessorySetup: NSObject {
// MARK: - 属性
static let shared = BleAccessorySetup()
// @available(iOS 18.0, *)
// class BleAccessorySetup: NSObject {
// // MARK: - 属性
// static let shared = BleAccessorySetup()
private var bleService: BleService?
private var session: ASAccessorySession?
private var isSessionActivated = false
private var isInitializing = false
private let logger = OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleAccessorySetup")
// private var bleService: BleService?
// private var session: ASAccessorySession?
// private var isSessionActivated = false
// private var isInitializing = false
// private let logger = OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleAccessorySetup")
// 杰理蓝牙设备的公司标识符
private let deviceCompanyIdentifier: UInt16 = 0xEFAB
private let deviceDisplayName = "语音助理耳机"
// // 杰理蓝牙设备的公司标识符
// private let deviceCompanyIdentifier: UInt16 = 0xEFAB
// private let deviceDisplayName = "语音助理耳机"
// 用于存储配对设备UUID的UserDefaults键
private let pairedDeviceUUIDKey = "com.yunqiinnovation.ble_service.paired_device_uuid"
// // 用于存储配对设备UUID的UserDefaults键
// private let pairedDeviceUUIDKey = "com.yunqiinnovation.ble_service.paired_device_uuid"
typealias DeviceDiscoveredCallback = (UUID) -> Void
private var onDeviceDiscovered: DeviceDiscoveredCallback?
// typealias DeviceDiscoveredCallback = (UUID) -> Void
// private var onDeviceDiscovered: DeviceDiscoveredCallback?
// MARK: - 初始化
private override init() { super.init() }
// // MARK: - 初始化
// private override init() { super.init() }
// MARK: - 公共方法
func setBleService(_ service: Any) {
self.bleService = service as? BleService
}
// // MARK: - 公共方法
// func setBleService(_ service: Any) {
// self.bleService = service as? BleService
// }
func setDeviceDiscoveredCallback(_ callback: @escaping (UUID) -> Void) {
self.onDeviceDiscovered = callback
}
// func setDeviceDiscoveredCallback(_ callback: @escaping (UUID) -> Void) {
// self.onDeviceDiscovered = callback
// }
// 处理发现的设备并连接
func connectToAccessory(uuid: UUID) {
guard let service = bleService else {
return
}
// // 处理发现的设备并连接
// func connectToAccessory(uuid: UUID) {
// guard let service = bleService else {
// return
// }
service.connectToAccessory(uuid: uuid)
}
// service.connectToAccessory(uuid: uuid)
// }
func addToReconnectQueue(uuid: UUID) {
guard let service = bleService else {
return
}
// func addToReconnectQueue(uuid: UUID) {
// guard let service = bleService else {
// return
// }
service.addToReconnectQueue(uuid: uuid)
}
// service.addToReconnectQueue(uuid: uuid)
// }
// 从视图控制器显示设备选择器 - 主要入口点
func showDevicePickerFromViewController(_ viewController: UIViewController) {
initializeSession()
}
// // 从视图控制器显示设备选择器 - 主要入口点
// func showDevicePickerFromViewController(_ viewController: UIViewController) {
// initializeSession()
// }
// 清除蓝牙状态(仅用于测试或重置,不会清除系统授权)
func resetBluetoothState() {
if let service = bleService {
service.disconnect()
}
}
// // 清除蓝牙状态(仅用于测试或重置,不会清除系统授权)
// func resetBluetoothState() {
// if let service = bleService {
// service.disconnect()
// }
// }
// MARK: - 会话管理
func initializeSession() {
if isInitializing {
os_log("会话正在初始化中,跳过", log: logger, type: .info)
return
}
// // MARK: - 会话管理
// func initializeSession() {
// if isInitializing {
// os_log("会话正在初始化中,跳过", log: logger, type: .info)
// return
// }
// 如果会话已激活,检查是否有配对设备
if session != nil && isSessionActivated {
os_log("会话已激活,检查配对设备", log: logger, type: .info)
// 会话已激活,检查是否有配对设备
if let accessories = session?.accessories, !accessories.isEmpty {
os_log("发现 %d 个已配对设备,尝试连接", log: logger, type: .info, accessories.count)
// 有配对设备,尝试连接
if let service = bleService {
let _ = service.getCentralManager()
// // 如果会话已激活,检查是否有配对设备
// if session != nil && isSessionActivated {
// os_log("会话已激活,检查配对设备", log: logger, type: .info)
// // 会话已激活,检查是否有配对设备
// if let accessories = session?.accessories, !accessories.isEmpty {
// os_log("发现 %d 个已配对设备,尝试连接", log: logger, type: .info, accessories.count)
// // 有配对设备,尝试连接
// if let service = bleService {
// let _ = service.getCentralManager()
for accessory in accessories {
if let uuid = accessory.bluetoothIdentifier {
onDeviceDiscovered?(uuid)
connectToAccessory(uuid: uuid)
}
}
}
} else {
os_log("没有已配对设备,显示配对框", log: logger, type: .info)
// 没有配对设备,直接显示配对框
showDevicePicker()
}
return
}
// for accessory in accessories {
// if let uuid = accessory.bluetoothIdentifier {
// onDeviceDiscovered?(uuid)
// connectToAccessory(uuid: uuid)
// }
// }
// }
// } else {
// os_log("没有已配对设备,显示配对框", log: logger, type: .info)
// // 没有配对设备,直接显示配对框
// showDevicePicker()
// }
// return
// }
os_log("开始初始化新会话", log: logger, type: .info)
isInitializing = true
// os_log("开始初始化新会话", log: logger, type: .info)
// isInitializing = true
do {
session = try ASAccessorySession()
session?.activate(on: .main) { [weak self] event in
guard let self = self else { return }
self.handleSessionEvent(event)
}
} catch {
os_log("会话初始化失败: %{public}@", log: logger, type: .error, error.localizedDescription)
handleSessionInitError(error)
}
}
// do {
// session = try ASAccessorySession()
// session?.activate(on: .main) { [weak self] event in
// guard let self = self else { return }
// self.handleSessionEvent(event)
// }
// } catch {
// os_log("会话初始化失败: %{public}@", log: logger, type: .error, error.localizedDescription)
// handleSessionInitError(error)
// }
// }
func resetSession() {
session?.invalidate()
session = nil
isSessionActivated = false
isInitializing = false
}
// func resetSession() {
// session?.invalidate()
// session = nil
// isSessionActivated = false
// isInitializing = false
// }
// MARK: - 设备选择器显示
private func showDevicePicker() {
guard let session = session, isSessionActivated else {
os_log("无法显示配对框:会话未激活", log: logger, type: .error)
return
}
// // MARK: - 设备选择器显示
// private func showDevicePicker() {
// guard let session = session, isSessionActivated else {
// os_log("无法显示配对框:会话未激活", log: logger, type: .error)
// return
// }
os_log("准备显示设备配对框", log: logger, type: .info)
// os_log("准备显示设备配对框", log: logger, type: .info)
let descriptor = ASDiscoveryDescriptor()
descriptor.bluetoothCompanyIdentifier = ASBluetoothCompanyIdentifier(deviceCompanyIdentifier)
// let descriptor = ASDiscoveryDescriptor()
// descriptor.bluetoothCompanyIdentifier = ASBluetoothCompanyIdentifier(deviceCompanyIdentifier)
let deviceImage = UIImage(systemName: "headphones") ?? UIImage()
let displayItem = ASPickerDisplayItem(
name: deviceDisplayName,
productImage: deviceImage,
descriptor: descriptor
)
// let deviceImage = UIImage(systemName: "headphones") ?? UIImage()
// let displayItem = ASPickerDisplayItem(
// name: deviceDisplayName,
// productImage: deviceImage,
// descriptor: descriptor
// )
DispatchQueue.main.async {
os_log("正在显示设备配对框", log: self.logger, type: .info)
session.showPicker(for: [displayItem]) { [weak self] error in
guard let self = self else { return }
// DispatchQueue.main.async {
// os_log("正在显示设备配对框", log: self.logger, type: .info)
// session.showPicker(for: [displayItem]) { [weak self] error in
// guard let self = self else { return }
if let error = error {
let nsError = error as NSError
os_log("设备选择器错误: %{public}@", log: self.logger, type: .error, error.localizedDescription)
} else {
os_log("设备选择器完成", log: self.logger, type: .info)
}
}
}
}
// if let error = error {
// let nsError = error as NSError
// os_log("设备选择器错误: %{public}@", log: self.logger, type: .error, error.localizedDescription)
// } else {
// os_log("设备选择器完成", log: self.logger, type: .info)
// }
// }
// }
// }
// MARK: - 私有辅助方法
private func handleSessionEvent(_ event: ASAccessoryEvent) {
switch event.eventType {
case .activated:
os_log("会话已激活", log: logger, type: .info)
handleSessionActivated()
// // MARK: - 私有辅助方法
// private func handleSessionEvent(_ event: ASAccessoryEvent) {
// switch event.eventType {
// case .activated:
// os_log("会话已激活", log: logger, type: .info)
// handleSessionActivated()
case .accessoryAdded:
os_log("发现新设备: %{public}@", log: logger, type: .info, event.accessory?.bluetoothIdentifier?.uuidString ?? "Unknown")
handleAccessoryAdded(event.accessory)
// case .accessoryAdded:
// os_log("发现新设备: %{public}@", log: logger, type: .info, event.accessory?.bluetoothIdentifier?.uuidString ?? "Unknown")
// handleAccessoryAdded(event.accessory)
case .accessoryRemoved:
os_log("设备已移除: %{public}@", log: logger, type: .info, event.accessory?.bluetoothIdentifier?.uuidString ?? "Unknown")
handleAccessoryRemoved(event.accessory)
// case .accessoryRemoved:
// os_log("设备已移除: %{public}@", log: logger, type: .info, event.accessory?.bluetoothIdentifier?.uuidString ?? "Unknown")
// handleAccessoryRemoved(event.accessory)
case .accessoryChanged:
os_log("设备状态已改变", log: logger, type: .info)
break
// case .accessoryChanged:
// os_log("设备状态已改变", log: logger, type: .info)
// break
case .pickerDidPresent:
os_log("配对框已显示", log: logger, type: .info)
break
// case .pickerDidPresent:
// os_log("配对框已显示", log: logger, type: .info)
// break
case .pickerDidDismiss:
os_log("配对框已关闭", log: logger, type: .info)
break
// case .pickerDidDismiss:
// os_log("配对框已关闭", log: logger, type: .info)
// break
case .pickerSetupFailed:
os_log("选择器设置失败: %{public}@", log: logger, type: .error,
event.error?.localizedDescription ?? "未知错误")
// case .pickerSetupFailed:
// os_log("选择器设置失败: %{public}@", log: logger, type: .error,
// event.error?.localizedDescription ?? "未知错误")
default:
os_log("未处理的会话事件: %d", log: logger, type: .info, event.eventType.rawValue)
break
}
}
// default:
// os_log("未处理的会话事件: %d", log: logger, type: .info, event.eventType.rawValue)
// break
// }
// }
private func handleSessionActivated() {
isSessionActivated = true
isInitializing = false
// private func handleSessionActivated() {
// isSessionActivated = true
// isInitializing = false
if let accessories = session?.accessories, !accessories.isEmpty {
if let service = bleService {
let _ = service.getCentralManager()
// if let accessories = session?.accessories, !accessories.isEmpty {
// if let service = bleService {
// let _ = service.getCentralManager()
for accessory in accessories {
if let uuid = accessory.bluetoothIdentifier {
onDeviceDiscovered?(uuid)
connectToAccessory(uuid: uuid)
}
}
}
} else {
showDevicePicker()
}
}
// for accessory in accessories {
// if let uuid = accessory.bluetoothIdentifier {
// onDeviceDiscovered?(uuid)
// connectToAccessory(uuid: uuid)
// }
// }
// }
// } else {
// showDevicePicker()
// }
// }
private func handleAccessoryAdded(_ accessory: ASAccessory?) {
guard let accessory = accessory, let uuid = accessory.bluetoothIdentifier else {
return
}
// private func handleAccessoryAdded(_ accessory: ASAccessory?) {
// guard let accessory = accessory, let uuid = accessory.bluetoothIdentifier else {
// return
// }
// 保存配对设备的UUID
savePairedMacAddress(uuid)
// // 保存配对设备的UUID
// savePairedMacAddress(uuid)
onDeviceDiscovered?(uuid)
connectToAccessory(uuid: uuid)
}
// onDeviceDiscovered?(uuid)
// connectToAccessory(uuid: uuid)
// }
private func handleAccessoryRemoved(_ accessory: ASAccessory?) {
guard let accessory = accessory, let uuid = accessory.bluetoothIdentifier else {
return
}
// private func handleAccessoryRemoved(_ accessory: ASAccessory?) {
// guard let accessory = accessory, let uuid = accessory.bluetoothIdentifier else {
// return
// }
if let service = bleService {
service.disconnect()
}
}
// if let service = bleService {
// service.disconnect()
// }
// }
private func handleSessionInitError(_ error: Error) {
isInitializing = false
session = nil
}
// private func handleSessionInitError(_ error: Error) {
// isInitializing = false
// session = nil
// }
// MARK: - 蓝牙状态检查
func isBluetoothAvailable() -> Bool {
do {
let audioSession = AVAudioSession.sharedInstance()
let outputs = audioSession.currentRoute.outputs
// // MARK: - 蓝牙状态检查
// func isBluetoothAvailable() -> Bool {
// do {
// let audioSession = AVAudioSession.sharedInstance()
// let outputs = audioSession.currentRoute.outputs
for output in outputs {
if output.portType == .bluetoothA2DP ||
output.portType == .bluetoothHFP ||
output.portType == .bluetoothLE {
return true
}
}
} catch {
os_log("检查蓝牙状态出错: %{public}@", log: logger, type: .error, error.localizedDescription)
}
// for output in outputs {
// if output.portType == .bluetoothA2DP ||
// output.portType == .bluetoothHFP ||
// output.portType == .bluetoothLE {
// return true
// }
// }
// } catch {
// os_log("检查蓝牙状态出错: %{public}@", log: logger, type: .error, error.localizedDescription)
// }
return true
}
// return true
// }
// MARK: - 清除配对关系
func clearAssociations() {
resetBluetoothState()
// // MARK: - 清除配对关系
// func clearAssociations() {
// resetBluetoothState()
// 清除保存的配对设备UUID
clearPairedMacAddress()
// // 清除保存的配对设备UUID
// clearPairedMacAddress()
#if canImport(AccessorySetupKit) && swift(>=5.9)
if #available(iOS 18.0, *) {
resetSession()
initializeSession()
}
#endif
}
// #if canImport(AccessorySetupKit) && swift(>=5.9)
// if #available(iOS 18.0, *) {
// resetSession()
// initializeSession()
// }
// #endif
// }
// MARK: - 配对设备UUID管理
// // MARK: - 配对设备UUID管理
/// 获取已配对设备的UUID字符串
/// 返回存储的配对设备UUID,如果没有则返回nil
func getPairedMacAddress() -> String? {
let uuidString = UserDefaults.standard.string(forKey: pairedDeviceUUIDKey)
if let uuidString = uuidString, !uuidString.isEmpty {
os_log("获取已配对设备UUID: %{public}@", log: logger, type: .info, uuidString)
return uuidString
}
os_log("未找到已配对设备UUID", log: logger, type: .info)
return nil
}
// /// 获取已配对设备的UUID字符串
// /// 返回存储的配对设备UUID,如果没有则返回nil
// func getPairedMacAddress() -> String? {
// let uuidString = UserDefaults.standard.string(forKey: pairedDeviceUUIDKey)
// if let uuidString = uuidString, !uuidString.isEmpty {
// os_log("获取已配对设备UUID: %{public}@", log: logger, type: .info, uuidString)
// return uuidString
// }
// os_log("未找到已配对设备UUID", log: logger, type: .info)
// return nil
// }
/// 保存配对设备的UUID
/// - Parameter uuid: 要保存的设备UUID
func savePairedMacAddress(_ uuid: UUID) {
let uuidString = uuid.uuidString
UserDefaults.standard.set(uuidString, forKey: pairedDeviceUUIDKey)
UserDefaults.standard.synchronize()
os_log("已保存配对设备UUID: %{public}@", log: logger, type: .info, uuidString)
}
// /// 保存配对设备的UUID
// /// - Parameter uuid: 要保存的设备UUID
// func savePairedMacAddress(_ uuid: UUID) {
// let uuidString = uuid.uuidString
// UserDefaults.standard.set(uuidString, forKey: pairedDeviceUUIDKey)
// UserDefaults.standard.synchronize()
// os_log("已保存配对设备UUID: %{public}@", log: logger, type: .info, uuidString)
// }
/// 清除已保存的配对设备UUID
private func clearPairedMacAddress() {
UserDefaults.standard.removeObject(forKey: pairedDeviceUUIDKey)
UserDefaults.standard.synchronize()
os_log("已清除配对设备UUID", log: logger, type: .info)
}
}
// /// 清除已保存的配对设备UUID
// private func clearPairedMacAddress() {
// UserDefaults.standard.removeObject(forKey: pairedDeviceUUIDKey)
// UserDefaults.standard.synchronize()
// os_log("已清除配对设备UUID", log: logger, type: .info)
// }
// }

1144
local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleScanManager.swift

File diff suppressed because it is too large

191
local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleService.swift

@ -3,15 +3,16 @@ import CoreBluetooth
import os.log
import AVFoundation
/// BLE服务类 - 专为AccessorySetupKit设备设计,支持自动重连和状态恢复
/// BLE服务类 - 传统BLE扫描和连接实现
///
/// 核心特性:
/// 1. 外设掉电→上电后自动重连(通过pendingReconnectUUIDs队列)
/// 2. App被系统终止后,外设广播或特征通知时自动恢复连接
/// 3. 使用CBCentralManagerOptionRestoreIdentifierKey实现系统级状态保留
/// 4. 支持bluetooth-central后台模式,允许后台BLE操作
/// 5. willRestoreState恢复系统保留的外设、扫描条件
/// 6. 统一的重连队列管理,适配AccessorySetupKit工作流程
/// 1. 传统BLE扫描和连接逻辑
/// 2. 外设掉电→上电后自动重连(通过pendingReconnectUUIDs队列)
/// 3. App被系统终止后,外设广播或特征通知时自动恢复连接
/// 4. 使用CBCentralManagerOptionRestoreIdentifierKey实现系统级状态保留
/// 5. 支持bluetooth-central后台模式,允许后台BLE操作
/// 6. willRestoreState恢复系统保留的外设、扫描条件
/// 7. 统一的重连队列管理
@available(iOS 13.0, *)
public class BleService: NSObject {
// MARK: - 单例实现
@ -43,6 +44,13 @@ public class BleService: NSObject {
private var audioCharacteristic: CBCharacteristic?
private var isScanning = false
private var scanTimer: Timer?
// 新增:设备缓存和广播数据记录
private var discoveredDevices: [String: [String: Any]] = [:] // 以设备ID为key的设备信息缓存
private var deviceAdvertisementData: [String: [String: Any]] = [:] // 设备广播数据缓存
private var scanStartTime: Date?
private let maxCacheTime: TimeInterval = 300 // 5分钟缓存时间
// 是否是ota模式
var isEnterOta = false
@ -50,8 +58,6 @@ public class BleService: NSObject {
private var callbacks = [Callback]()
private var connectionState = BleConst.STATE_DISCONNECTED
// 状态恢复相关
private var restoredPeripherals: [CBPeripheral] = []
@ -66,22 +72,11 @@ public class BleService: NSObject {
// MARK: - 初始化
private override init() {
super.init()
#if canImport(AccessorySetupKit) && swift(>=5.9)
if #available(iOS 18.0, *) {
// iOS 18+下不立即初始化,等待AccessorySetupKit
} else {
initializeCentralManager()
}
#else
initializeCentralManager()
#endif
// 初始化Opus音频处理器
initializeOpusProcessor()
}
// 延迟初始化中央管理器
// 初始化中央管理器
private func initializeCentralManager() {
if centralManager != nil {
os_log("🔧 CBCentralManager已存在,跳过初始化", log: logger, type: .debug)
@ -170,19 +165,13 @@ public class BleService: NSObject {
}
/// 开始扫描BLE设备
public func startScan() {
#if canImport(AccessorySetupKit) && swift(>=5.9)
if #available(iOS 18.0, *) {
// iOS 18+不使用传统扫描
return
}
#endif
public func startScan(timeout: TimeInterval = 30.0) {
if centralManager == nil {
initializeCentralManager()
}
guard let manager = centralManager, manager.state == .poweredOn else {
os_log("❌ 蓝牙未就绪,无法开始扫描", log: logger, type: .error)
return
}
@ -190,6 +179,9 @@ public class BleService: NSObject {
stopScan()
}
// 清空之前的扫描结果
discoveredDevices.removeAll()
isScanning = true
let serviceUUIDs = [BleConst.PRIMARY_SERVICE_UUID, BleConst.AUDIO_SERVICE_UUID]
@ -197,7 +189,14 @@ public class BleService: NSObject {
CBCentralManagerScanOptionAllowDuplicatesKey: false
])
os_log("开始扫描BLE设备...", log: logger, type: .info)
os_log("🔍 开始扫描BLE设备,超时时间: %.1f秒", log: logger, type: .info, timeout)
// 设置扫描超时
scanTimer?.invalidate()
scanTimer = Timer.scheduledTimer(withTimeInterval: timeout, repeats: false) { [weak self] _ in
self?.stopScan()
os_log("⏰ 扫描超时,自动停止", log: self?.logger ?? OSLog.default, type: .info)
}
}
/// 停止扫描
@ -205,10 +204,37 @@ public class BleService: NSObject {
if isScanning && centralManager != nil {
centralManager?.stopScan()
isScanning = false
os_log("停止BLE扫描", log: logger, type: .info)
scanTimer?.invalidate()
scanTimer = nil
os_log("⏹️ 停止BLE扫描", log: logger, type: .info)
}
}
/// 连接到指定UUID的设备
public func connectToDevice(uuid: String) -> Bool {
guard let deviceUUID = UUID(uuidString: uuid) else {
os_log("❌ 无效的设备UUID: %@", log: logger, type: .error, uuid)
return false
}
guard let manager = centralManager else {
os_log("❌ CBCentralManager未初始化", log: logger, type: .error)
return false
}
// 先尝试从已知外设中获取
let knownPeripherals = manager.retrievePeripherals(withIdentifiers: [deviceUUID])
if let peripheral = knownPeripherals.first {
connectPeripheral(peripheral)
return true
}
// 如果没有找到,添加到重连队列
addToReconnectQueue(uuid: deviceUUID)
os_log("📱 设备未找到,已添加到重连队列: %@", log: logger, type: .info, uuid)
return false
}
/// 连接到指定设备
public func connect(to peripheral: CBPeripheral) {
connectPeripheral(peripheral)
@ -231,18 +257,18 @@ public class BleService: NSObject {
}
if peripheral.state == .connected {
os_log("设备已连接,恢复会话: %@", log: logger, type: .info, peripheral.identifier.uuidString)
os_log("📱 设备已连接,恢复会话: %@", log: logger, type: .info, peripheral.identifier.uuidString)
resumeSession(for: peripheral)
return
}
if peripheral.state == .connecting {
os_log("设备正在连接中: %@", log: logger, type: .info, peripheral.identifier.uuidString)
os_log("📱 设备正在连接中: %@", log: logger, type: .info, peripheral.identifier.uuidString)
return
}
if connectionState == BleConst.STATE_CONNECTED && peripheralDevice?.identifier != peripheral.identifier {
os_log("断开当前设备,连接新设备", log: logger, type: .info)
os_log("📱 断开当前设备,连接新设备", log: logger, type: .info)
disconnect()
}
@ -257,7 +283,7 @@ public class BleService: NSObject {
CBConnectPeripheralOptionNotifyOnNotificationKey: true
])
os_log("开始连接到设备: %@", log: logger, type: .info, peripheral.identifier.uuidString)
os_log("🔗 开始连接到设备: %@", log: logger, type: .info, peripheral.identifier.uuidString)
}
/// 恢复已连接设备的会话
@ -278,7 +304,7 @@ public class BleService: NSObject {
os_log("🔍 恢复会话时重新发现服务和特征", log: logger, type: .info)
peripheral.discoverServices([BleConst.PRIMARY_SERVICE_UUID, BleConst.AUDIO_SERVICE_UUID])
os_log("已恢复设备会话: %@", log: logger, type: .info, peripheral.identifier.uuidString)
os_log("✅ 已恢复设备会话: %@", log: logger, type: .info, peripheral.identifier.uuidString)
}
/// 断开当前连接
@ -291,7 +317,7 @@ public class BleService: NSObject {
if centralManager != nil {
updateConnectionState(BleConst.STATE_DISCONNECTING)
centralManager?.cancelPeripheralConnection(peripheral)
os_log("开始断开设备连接", log: logger, type: .info)
os_log("🔌 开始断开设备连接", log: logger, type: .info)
}
}
@ -300,6 +326,11 @@ public class BleService: NSObject {
return connectionState == BleConst.STATE_CONNECTED
}
/// 获取扫描到的设备列表
public func getDiscoveredDevices() -> [[String: Any]] {
return Array(discoveredDevices.values)
}
// MARK: - 回调通知方法
/// 通知所有回调扫描结果
@ -903,25 +934,7 @@ public class BleService: NSObject {
]
}
// MARK: - AccessorySetupKit
/// 在接收到AccessorySetupKit的accessoryAdded事件后调用
func connectToAccessory(uuid: UUID) {
#if canImport(AccessorySetupKit) && swift(>=5.9)
if #available(iOS 18.0, *) {
os_log("开始连接设备: %{public}@", log: logger, type: .info, uuid.uuidString)
// 确保CBCentralManager已初始化
if centralManager == nil {
os_log("CBCentralManager未初始化,开始初始化", log: logger, type: .debug)
initializeCentralManager()
}
// 添加到重连队列
addToReconnectQueue(uuid: uuid)
}
#endif
}
}
// MARK: - SwiftOpusAudioProcessor.AudioDataCallback
@ -943,7 +956,7 @@ extension BleService: CBCentralManagerDelegate {
case .poweredOn:
os_log("🔧 蓝牙已开启", log: logger, type: .info)
// 注:AccessorySetupKit应用不需要恢复传统BLE扫描
// 蓝牙就绪后可以开始扫描
// 恢复外设连接
if !restoredPeripherals.isEmpty {
@ -1042,22 +1055,72 @@ extension BleService: CBCentralManagerDelegate {
// 原有方法
public func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
// 将扫描结果转换为字典格式
let deviceId = peripheral.identifier.uuidString
let deviceName = peripheral.name ?? "Unknown"
// 构建设备基本信息
var deviceInfo: [String: Any] = [
"name": peripheral.name ?? "Unknown",
"id": peripheral.identifier.uuidString,
"rssi": RSSI.intValue
"name": deviceName,
"id": deviceId,
"rssi": RSSI.intValue,
"discoveredAt": Date().timeIntervalSince1970
]
// 添加广播数据
// 详细的广播数据记录
var advData: [String: Any] = [:]
// 制造商数据
if let manufacturerData = advertisementData[CBAdvertisementDataManufacturerDataKey] as? Data {
deviceInfo["manufacturerData"] = manufacturerData
advData["manufacturerData"] = manufacturerData.map { String(format: "%02x", $0) }.joined(separator: " ")
advData["manufacturerDataRaw"] = manufacturerData
}
// 服务UUID列表
if let serviceUUIDs = advertisementData[CBAdvertisementDataServiceUUIDsKey] as? [CBUUID] {
deviceInfo["serviceUUIDs"] = serviceUUIDs.map { $0.uuidString }
let serviceUUIDStrings = serviceUUIDs.map { $0.uuidString }
deviceInfo["serviceUUIDs"] = serviceUUIDStrings
advData["serviceUUIDs"] = serviceUUIDStrings
}
// 本地名称
if let localName = advertisementData[CBAdvertisementDataLocalNameKey] as? String {
advData["localName"] = localName
}
// 发射功率
if let txPowerLevel = advertisementData[CBAdvertisementDataTxPowerLevelKey] as? NSNumber {
advData["txPowerLevel"] = txPowerLevel.intValue
}
// 服务数据
if let serviceData = advertisementData[CBAdvertisementDataServiceDataKey] as? [CBUUID: Data] {
var serviceDataDict: [String: String] = [:]
for (uuid, data) in serviceData {
serviceDataDict[uuid.uuidString] = data.map { String(format: "%02x", $0) }.joined(separator: " ")
}
advData["serviceData"] = serviceDataDict
}
// 可连接性
if let isConnectable = advertisementData[CBAdvertisementDataIsConnectable] as? NSNumber {
advData["isConnectable"] = isConnectable.boolValue
}
// 完整的广播数据
advData["rssi"] = RSSI.intValue
advData["timestamp"] = Date().timeIntervalSince1970
// 缓存设备信息和广播数据
discoveredDevices[deviceId] = deviceInfo
deviceAdvertisementData[deviceId] = advData
// 添加广播数据到设备信息中
deviceInfo["advertisementData"] = advData
os_log("📡 发现设备: %{public}@ (ID: %{public}@), RSSI: %d", log: logger, type: .info, deviceName, deviceId, RSSI.intValue)
os_log("📡 广播数据: %{public}@", log: logger, type: .debug, String(describing: advData))
// 报告给回调
notifyScanResult(devices: [deviceInfo])
}
@ -1267,4 +1330,4 @@ extension BleService: CBPeripheralDelegate {
os_log("✅ 描述符写入成功: %{public}@", log: logger, type: .debug, descriptor.uuid.uuidString)
}
}
}
}

216
local_plugins/ble_service/ios/ble_service/Sources/ble_service/SwiftBleServicePlugin.swift

@ -12,79 +12,20 @@ public class SwiftBleServicePlugin: NSObject, FlutterPlugin {
internal var statusEventSink: FlutterEventSink?
private let logger = OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleServicePlugin")
// 标记是否已初始化所有服务
private var servicesInitialized = false
// MARK: - 初始化方法
init(_ channel: FlutterMethodChannel) {
self.methodChannel = channel
super.init()
#if canImport(AccessorySetupKit) && swift(>=5.9)
if #available(iOS 18.0, *) {
// iOS 18环境下,只设置回调但不主动初始化会话
let bleAccessorySetup = BleAccessorySetup.shared
// 在accessoryAdded回调中延迟设置BleService
bleAccessorySetup.setDeviceDiscoveredCallback { [weak self] uuid in
// 确保服务已初始化
self?.ensureServicesInitialized()
// 然后处理发现的设备
self?.handleDiscoveredDevice(uuid: uuid)
}
// 注释掉自动初始化,改为等待用户主动调用startDeviceAssociation
// bleAccessorySetup.initializeSession()
os_log("iOS 18环境:已设置设备发现回调,等待用户主动调用关联", log: logger, type: .info)
} else {
// 低于iOS 18,直接初始化所有服务
initializeAllServices()
}
#else
// 非AccessorySetupKit环境,直接初始化所有服务
initializeAllServices()
#endif
// 直接初始化BLE服务
initializeBleService()
}
// 确保所有服务都已初始化
private func ensureServicesInitialized() {
if !servicesInitialized {
// 设置BLE服务回调
BleService.shared.addCallback(self)
// 设置BleService到AccessorySetup
#if canImport(AccessorySetupKit) && swift(>=5.9)
if #available(iOS 18.0, *) {
BleAccessorySetup.shared.setBleService(BleService.shared)
}
#endif
servicesInitialized = true
}
}
// 初始化所有服务(iOS 17及以下版本的标准行为)
private func initializeAllServices() {
// 初始化BLE服务
private func initializeBleService() {
// 设置BLE服务回调
BleService.shared.addCallback(self)
// 初始化AccessorySetup,但不自动开始会话
#if canImport(AccessorySetupKit) && swift(>=5.9)
if #available(iOS 18.0, *) {
BleAccessorySetup.shared.setBleService(BleService.shared)
BleAccessorySetup.shared.setDeviceDiscoveredCallback { [weak self] uuid in
self?.handleDiscoveredDevice(uuid: uuid)
}
// 移除自动初始化,统一由startDeviceAssociation方法触发
// BleAccessorySetup.shared.initializeSession()
}
#endif
os_log("非iOS 18环境:已设置BLE服务但不自动初始化会话", log: logger, type: .info)
servicesInitialized = true
os_log("✅ BLE服务已初始化", log: logger, type: .info)
}
// MARK: - FlutterPlugin Protocol
@ -106,9 +47,6 @@ public class SwiftBleServicePlugin: NSObject, FlutterPlugin {
}
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
// 检查是否需要初始化服务
checkAndInitializeServices()
switch call.method {
case "isConnected":
result(BleService.shared.isConnected())
@ -123,31 +61,6 @@ public class SwiftBleServicePlugin: NSObject, FlutterPlugin {
details: nil))
}
case "isCompanionSupported":
#if canImport(AccessorySetupKit) && swift(>=5.9)
if #available(iOS 18.0, *) {
result(true)
} else {
result(false)
}
#else
result(false)
#endif
case "getPairedMacAddress":
#if canImport(AccessorySetupKit) && swift(>=5.9)
if #available(iOS 18.0, *) {
let pairedUUID = BleAccessorySetup.shared.getPairedMacAddress()
result(pairedUUID)
} else {
result(nil)
}
#else
result(nil)
#endif
case "isBleConnected":
result(BleService.shared.isConnected())
@ -193,90 +106,46 @@ public class SwiftBleServicePlugin: NSObject, FlutterPlugin {
case "playA2DP":
result(BleService.shared.playA2DP())
// iOS 18+ AccessorySetupKit相关方法
case "clearAssociations":
// 清除设备配对关系
#if canImport(AccessorySetupKit) && swift(>=5.9)
if #available(iOS 18.0, *) {
os_log("清除设备配对关系", log: logger, type: .info)
BleAccessorySetup.shared.clearAssociations()
result(true)
} else {
result(true)
}
#else
result(true)
#endif
case "startDeviceAssociation":
// 用户主动触发设备关联流程
#if canImport(AccessorySetupKit) && swift(>=5.9)
if #available(iOS 18.0, *) {
os_log("用户主动触发设备关联流程", log: logger, type: .info)
// 确保服务已初始化
ensureServicesInitialized()
// 重新初始化会话
BleAccessorySetup.shared.initializeSession()
// 返回成功,实际配对结果会通过回调通知
result(true)
} else {
os_log("当前iOS版本不支持AccessorySetupKit", log: logger, type: .info)
result(false)
}
#else
os_log("当前环境不支持AccessorySetupKit", log: logger, type: .info)
result(false)
#endif
case "startScan":
let arguments = call.arguments as? [String: Any]
let devicename = (arguments?["devicename"] as? String) ?? ""
let timeout = (arguments?["timeout"] as? Double) ?? 30.0
// 修复:支持所有iOS版本
if #available(iOS 18.0, *) {
// iOS 18+ 使用新的BleScanManager
os_log("iOS 18+ 使用 BleScanManager 进行扫描", log: logger, type: .info)
let succ = BleScanManager.shared.startScan(timeout: 15.0)
result(succ)
} else {
// iOS 17及以下使用传统BleService
os_log("iOS 17及以下使用传统 BleService 进行扫描", log: logger, type: .info)
BleService.shared.startScan()
os_log("🔍 开始传统BLE扫描,超时: %.1f秒", log: logger, type: .info, timeout)
BleService.shared.startScan(timeout: timeout)
result(true)
}
case "getDeviceInfo":
case "stopScan":
os_log("⏹️ 停止BLE扫描", log: logger, type: .info)
BleService.shared.stopScan()
result(true)
case "connectToDevice":
let arguments = call.arguments as? [String: Any]
let address = (arguments?["address"] as? String) ?? ""
let info = BleScanManager.shared.getDiscoveredDevices()
result(info)
guard let deviceId = arguments?["deviceId"] as? String else {
result(FlutterError(code: "INVALID_ARGUMENT",
message: "缺少设备ID参数",
details: nil))
return
}
os_log("🔗 连接到设备: %@", log: logger, type: .info, deviceId)
let success = BleService.shared.connectToDevice(uuid: deviceId)
result(success)
case "getDiscoveredDevices":
let devices = BleService.shared.getDiscoveredDevices()
result(devices)
case "requestBluetoothPermission":
BleScanManager.shared.requestBluetoothPermission();
result(true)
// 移除这个方法调用,让iOS系统自然处理权限
result(FlutterMethodNotImplemented)
default:
result(FlutterMethodNotImplemented)
}
}
// MARK: - 私有方法
// 检查并按需初始化服务
private func checkAndInitializeServices() {
#if canImport(AccessorySetupKit) && swift(>=5.9)
if #available(iOS 18.0, *) {
ensureServicesInitialized()
}
#endif
}
// AccessorySetupKit发现设备的处理
private func handleDiscoveredDevice(uuid: UUID) {
// BleAccessorySetup内部已进行连接,此处不再重复调用
// os_log("收到设备发现回调: %{public}@", log: logger, type: .info, uuid.uuidString)
// 可以在这里添加设备记录或其他操作
}
private func sendEvent<T>(_ eventSink: FlutterEventSink?, data: T, errorPrefix: String) {
if let eventSink = eventSink {
DispatchQueue.main.async {
@ -284,25 +153,6 @@ public class SwiftBleServicePlugin: NSObject, FlutterPlugin {
}
}
}
// 获取顶层视图控制器的辅助方法
private func getTopViewController(_ viewController: UIViewController) -> UIViewController {
if let presented = viewController.presentedViewController {
return getTopViewController(presented)
}
if let tabBarController = viewController as? UITabBarController,
let selected = tabBarController.selectedViewController {
return getTopViewController(selected)
}
if let navigationController = viewController as? UINavigationController,
let visibleViewController = navigationController.visibleViewController {
return getTopViewController(visibleViewController)
}
return viewController
}
}
// MARK: - BleService.Callback

29
local_plugins/user_portrait/ios/user_portrait.podspec

@ -1,29 +0,0 @@
#
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
# Run `pod lib lint user_portrait.podspec` to validate before publishing.
#
Pod::Spec.new do |s|
s.name = 'user_portrait'
s.version = '0.0.1'
s.summary = '用户画像'
s.description = <<-DESC
用户画像
DESC
s.homepage = 'http://example.com'
s.license = { :file => '../LICENSE' }
s.author = { 'Your Company' => 'email@example.com' }
s.source = { :path => '.' }
s.source_files = 'user_portrait/Sources/user_portrait/**/*'
s.dependency 'Flutter'
s.platform = :ios, '12.0'
# Flutter.framework does not contain a i386 slice.
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
s.swift_version = '5.0'
# If your plugin requires a privacy manifest, for example if it uses any
# required reason APIs, update the PrivacyInfo.xcprivacy file to describe your
# plugin's privacy impact, and then uncomment this line. For more information,
# see https://developer.apple.com/documentation/bundleresources/privacy_manifest_files
# s.resource_bundles = {'user_portrait_privacy' => ['user_portrait/Sources/user_portrait/PrivacyInfo.xcprivacy']}
end

24
local_plugins/user_portrait/ios/user_portrait/Package.swift

@ -13,20 +13,12 @@ let package = Package(
],
dependencies: [],
targets: [
.target(
name: "user_portrait",
dependencies: [],
resources: [
// If your plugin requires a privacy manifest, for example if it uses any required
// reason APIs, update the PrivacyInfo.xcprivacy file to describe your plugin's
// privacy impact, and then uncomment these lines. For more information, see
// https://developer.apple.com/documentation/bundleresources/privacy_manifest_files
// .process("PrivacyInfo.xcprivacy"),
// If you have other resources that need to be bundled with your plugin, refer to
// the following instructions to add them:
// https://developer.apple.com/documentation/xcode/bundling-resources-with-a-swift-package
]
)
]
.target(
name: "user_portrait",
dependencies: [
],
path: "Sources/user_portrait"
)
]
)

14
local_plugins/user_portrait/ios/user_portrait/Sources/user_portrait/PrivacyInfo.xcprivacy

@ -1,14 +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>NSPrivacyTrackingDomains</key>
<array/>
<key>NSPrivacyAccessedAPITypes</key>
<array/>
<key>NSPrivacyCollectedDataTypes</key>
<array/>
<key>NSPrivacyTracking</key>
<false/>
</dict>
</plist>

4
local_plugins/user_portrait/ios/user_portrait/Sources/user_portrait/UserPortraitPlugin.swift

@ -10,8 +10,8 @@ public class UserPortraitPlugin: NSObject, FlutterPlugin {
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method {
case "getPlatformVersion":
result("iOS " + UIDevice.current.systemVersion)
case "clean":
result(true)
default:
result(FlutterMethodNotImplemented)
}

46
local_plugins/user_portrait/pubspec.yaml

@ -17,21 +17,8 @@ dev_dependencies:
sdk: flutter
flutter_lints: ^5.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter:
# This section identifies this Flutter project as a plugin project.
# The 'pluginClass' specifies the class (in Java, Kotlin, Swift, Objective-C, etc.)
# which should be registered in the plugin registry. This is required for
# using method channels.
# The Android 'package' specifies package in which the registered class is.
# This is required for using method channels on Android.
# The 'ffiPlugin' specifies that native code should be built and bundled.
# This is required for using `dart:ffi`.
# All these are used by the tooling to maintain consistency when
# adding or updating assets for this project.
plugin:
platforms:
android:
@ -39,34 +26,5 @@ flutter:
pluginClass: UserPortraitPlugin
ios:
pluginClass: UserPortraitPlugin
# To add assets to your plugin package, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
#
# For details regarding assets in packages, see
# https://flutter.dev/to/asset-from-package
#
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/to/resolution-aware-images
# To add custom fonts to your plugin package, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts in packages, see
# https://flutter.dev/to/font-from-package
swiftPackage:
path: ios/user_portrait

27
local_plugins/user_portrait/test/user_portrait_method_channel_test.dart

@ -1,27 +0,0 @@
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:user_portrait/user_portrait_method_channel.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
MethodChannelUserPortrait platform = MethodChannelUserPortrait();
const MethodChannel channel = MethodChannel('user_portrait');
setUp(() {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
channel,
(MethodCall methodCall) async {
return '42';
},
);
});
tearDown(() {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(channel, null);
});
test('getPlatformVersion', () async {
expect(await platform.getPlatformVersion(), '42');
});
}

29
local_plugins/user_portrait/test/user_portrait_test.dart

@ -1,29 +0,0 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:user_portrait/user_portrait.dart';
import 'package:user_portrait/user_portrait_platform_interface.dart';
import 'package:user_portrait/user_portrait_method_channel.dart';
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
class MockUserPortraitPlatform
with MockPlatformInterfaceMixin
implements UserPortraitPlatform {
@override
Future<String?> getPlatformVersion() => Future.value('42');
}
void main() {
final UserPortraitPlatform initialPlatform = UserPortraitPlatform.instance;
test('$MethodChannelUserPortrait is the default instance', () {
expect(initialPlatform, isInstanceOf<MethodChannelUserPortrait>());
});
test('getPlatformVersion', () async {
UserPortrait userPortraitPlugin = UserPortrait();
MockUserPortraitPlatform fakePlatform = MockUserPortraitPlatform();
UserPortraitPlatform.instance = fakePlatform;
expect(await userPortraitPlugin.getPlatformVersion(), '42');
});
}

1
pubspec.yaml

@ -45,7 +45,6 @@ dependencies:
haptic_feedback: ^0.5.1+1
tencentcloud_cos_sdk_plugin: ^1.2.3
photo_view: ^0.15.0
window_size:
git:
url: https://github.com/google/flutter-desktop-embedding.git

Loading…
Cancel
Save