You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

562 lines
27 KiB

import Foundation
import CoreBluetooth
import os.log
import ObjectiveC // 添加ObjC运行时支持
import AVFoundation
// 条件编译只导入在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
var isActivated = false
init() throws {
do {
os_log("尝试初始化ASAccessorySession...", log: BleAccessorySetup.shared.logger, type: .debug)
// 根据WWDC24示例,使用无参初始化
self.session = try ASAccessorySession()
os_log("ASAccessorySession初始化成功", log: BleAccessorySetup.shared.logger, type: .info)
} catch {
os_log("ASAccessorySession初始化错误: %{public}@", log: BleAccessorySetup.shared.logger, type: .error, error.localizedDescription)
// 检查Info.plist配置
if let plistSupports = Bundle.main.object(forInfoDictionaryKey: "NSAccessorySetupKitSupports") {
os_log("NSAccessorySetupKitSupports: %{public}@", log: BleAccessorySetup.shared.logger, type: .debug, String(describing: plistSupports))
} else {
os_log("NSAccessorySetupKitSupports未配置", log: BleAccessorySetup.shared.logger, type: .error)
}
if let services = Bundle.main.object(forInfoDictionaryKey: "NSAccessorySetupBluetoothServices") {
os_log("NSAccessorySetupBluetoothServices: %{public}@", log: BleAccessorySetup.shared.logger, type: .debug, String(describing: services))
} else {
os_log("NSAccessorySetupBluetoothServices未配置", log: BleAccessorySetup.shared.logger, type: .error)
}
if let companyIds = Bundle.main.object(forInfoDictionaryKey: "NSAccessorySetupBluetoothCompanyIdentifiers") {
os_log("NSAccessorySetupBluetoothCompanyIdentifiers: %{public}@", log: BleAccessorySetup.shared.logger, type: .debug, String(describing: companyIds))
} else {
os_log("NSAccessorySetupBluetoothCompanyIdentifiers未配置", log: BleAccessorySetup.shared.logger, type: .error)
}
throw error
}
}
}
var accessorySessionWrapper: Any?
// 添加初始化完成回调
private var onSessionActivated: (() -> Void)?
typealias DeviceDiscoveredCallback = (UUID) -> Void
private var onDeviceDiscovered: DeviceDiscoveredCallback?
#endif
private var isInitializing = false
private override init() {
super.init()
}
// 设置BLE服务实例
func setBleService(_ service: Any) {
self.bleService = service
os_log("BLE服务实例已设置", log: logger, type: .info)
}
// 设置设备发现回调
func setDeviceDiscoveredCallback(_ callback: @escaping (UUID) -> Void) {
#if canImport(AccessorySetupKit) && swift(>=5.9)
if #available(iOS 18.0, *) {
self.onDeviceDiscovered = callback
os_log("设备发现回调已设置", log: logger, type: .debug)
}
#endif
}
// 初始化设备发现会话
func initializeDiscoverySession() {
#if canImport(AccessorySetupKit) && swift(>=5.9)
if #available(iOS 18.0, *) {
// 避免重复初始化
if isInitializing {
os_log("初始化正在进行中,请等待...", log: logger, type: .debug)
return
}
// 如果会话已经存在且已激活,无需重新初始化
if let wrapper = accessorySessionWrapper as? AccessorySessionWrapper, wrapper.isActivated {
os_log("会话已经初始化并激活", log: logger, type: .info)
return
}
isInitializing = true
os_log("初始化iOS 18 AccessorySetupKit会话...", log: logger, type: .debug)
// 检查Info.plist配置并详细打印
if let plistCheck = Bundle.main.object(forInfoDictionaryKey: "NSAccessorySetupKitSupports") {
os_log("NSAccessorySetupKitSupports配置: %{public}@", log: logger, type: .debug, String(describing: plistCheck))
} else {
os_log("NSAccessorySetupKitSupports未配置", log: logger, type: .error)
}
if let services = Bundle.main.object(forInfoDictionaryKey: "NSAccessorySetupBluetoothServices") {
os_log("NSAccessorySetupBluetoothServices配置: %{public}@", log: logger, type: .debug, String(describing: services))
} else {
os_log("NSAccessorySetupBluetoothServices未配置", log: logger, type: .error)
}
if let companyIds = Bundle.main.object(forInfoDictionaryKey: "NSAccessorySetupBluetoothCompanyIdentifiers") {
os_log("NSAccessorySetupBluetoothCompanyIdentifiers配置: %{public}@", log: logger, type: .debug, String(describing: companyIds))
} else {
os_log("NSAccessorySetupBluetoothCompanyIdentifiers未配置", log: logger, type: .error)
}
// 创建会话 (使用try-catch)
do {
os_log("尝试创建AccessorySessionWrapper...", log: logger, type: .debug)
accessorySessionWrapper = try AccessorySessionWrapper()
os_log("AccessorySessionWrapper创建成功", log: logger, type: .info)
// 激活会话并设置事件处理
if let wrapper = accessorySessionWrapper as? AccessorySessionWrapper {
os_log("尝试激活ASAccessorySession...", log: logger, type: .debug)
wrapper.session.activate(on: .main) { [weak self] event in
guard let self = self else {
os_log("self已被释放,无法处理事件", log: BleAccessorySetup.shared.logger, type: .error)
return
}
os_log("收到ASAccessorySession事件: %{public}d", log: self.logger, type: .debug, event.eventType.rawValue)
switch event.eventType {
case .activated:
os_log("AccessorySetupKit会话已激活", log: self.logger, type: .info)
// 标记会话已激活
if let wrapper = self.accessorySessionWrapper as? AccessorySessionWrapper {
wrapper.isActivated = true
// 触发激活回调
DispatchQueue.main.async {
self.onSessionActivated?()
self.onSessionActivated = nil // 防止重复调用
}
}
case .accessoryAdded:
if let accessory = event.accessory, let uuid = accessory.bluetoothIdentifier {
os_log("发现新设备: %{public}@", log: self.logger, type: .info, accessory.displayName)
self.onDeviceDiscovered?(uuid)
} else {
os_log("设备添加事件但缺少设备信息或UUID", log: self.logger, type: .debug)
}
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)
} else {
os_log("会话出错但无错误详情", log: self.logger, type: .debug)
}
default:
os_log("收到其他会话事件: %{public}d", log: self.logger, type: .debug, event.eventType.rawValue)
}
}
// 检查现有授权设备
let accessoriesCount = wrapper.session.accessories.count
os_log("发现%{public}d个已授权设备", log: logger, type: .info, accessoriesCount)
if accessoriesCount > 0 {
// 如果有现有设备,触发回调
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("已授权设备缺少UUID: %{public}@", log: logger, type: .debug, accessory.displayName)
}
}
}
} else {
os_log("accessorySessionWrapper类型转换失败", log: logger, type: .error)
}
} catch {
os_log("致命错误:AccessorySetupKit初始化失败: %{public}@", log: logger, type: .error, error.localizedDescription)
// 重置初始化状态
isInitializing = false
// 释放失败的会话
accessorySessionWrapper = nil
// 尝试通知可能正在等待的回调
DispatchQueue.main.async {
self.onSessionActivated?()
self.onSessionActivated = nil
}
}
} else {
os_log("当前iOS版本不支持AccessorySetupKit", log: logger, type: .info)
// 即使在不支持的系统上,也要调用回调
DispatchQueue.main.async {
self.onSessionActivated?()
self.onSessionActivated = nil
}
}
#else
os_log("构建环境不支持AccessorySetupKit", log: logger, type: .info)
// 即使在不支持的环境中,也要调用回调
DispatchQueue.main.async {
self.onSessionActivated?()
self.onSessionActivated = nil
}
#endif
// 完成初始化过程
isInitializing = false
}
// 初始化会话并在完成后执行回调
func initializeSessionAndRun(completion: @escaping () -> Void) {
#if canImport(AccessorySetupKit) && swift(>=5.9)
if #available(iOS 18.0, *) {
os_log("安全初始化会话...", log: logger, type: .debug)
// 如果会话已存在且已激活
if let wrapper = accessorySessionWrapper as? AccessorySessionWrapper, wrapper.isActivated {
os_log("会话已初始化并激活,直接执行回调", log: logger, type: .info)
completion()
return
}
// 如果会话存在但未激活,设置回调
if let wrapper = accessorySessionWrapper as? AccessorySessionWrapper, !wrapper.isActivated {
os_log("会话已创建但未激活,等待激活...", log: logger, type: .debug)
self.onSessionActivated = completion
return
}
// 需要从头创建会话
os_log("需要创建新会话...", log: logger, type: .debug)
self.onSessionActivated = completion
initializeDiscoverySession()
} else {
// 不支持iOS 18,直接执行回调
completion()
}
#else
// 不支持AccessorySetupKit,直接执行回调
completion()
#endif
}
// 重置会话状态
func resetSession() {
#if canImport(AccessorySetupKit) && swift(>=5.9)
if #available(iOS 18.0, *) {
os_log("重置AccessorySetupKit会话...", log: logger, type: .debug)
// 取消可能挂起的回调
onSessionActivated = nil
// 清理现有会话
if let wrapper = accessorySessionWrapper as? AccessorySessionWrapper {
os_log("清理现有会话", log: logger, type: .debug)
wrapper.session.invalidate()
}
// 重置状态
accessorySessionWrapper = nil
isInitializing = false
os_log("会话已重置", log: logger, type: .info)
}
#endif
}
// 安全显示设备选择器
func safeShowDevicePicker() {
// 重置会话状态,确保干净的重新初始化
resetSession()
initializeSessionAndRun { [weak self] in
guard let self = self else { return }
self.showDevicePicker()
}
}
// 显示设备选择器
func showDevicePicker() {
#if canImport(AccessorySetupKit) && swift(>=5.9)
if #available(iOS 18.0, *) {
os_log("尝试显示设备选择器...", log: logger, type: .debug)
// 检查会话是否已初始化
if accessorySessionWrapper == nil {
os_log("会话未初始化,尝试自动初始化...", log: logger, type: .debug)
safeShowDevicePicker()
return
}
if let wrapper = accessorySessionWrapper as? AccessorySessionWrapper {
// 检查会话是否已激活
if !wrapper.isActivated {
os_log("会话尚未激活,等待激活后再显示...", log: logger, type: .debug)
onSessionActivated = { [weak self] in
self?.showDevicePicker()
}
return
}
os_log("找到会话包装器,准备显示选择器", log: logger, type: .info)
// 根据WWDC24示例代码创建设备描述符和显示项
os_log("创建设备描述符...", log: logger, type: .debug)
let descriptor = ASDiscoveryDescriptor()
// 设置蓝牙服务UUID
// let serviceUUID = "0000abc0-0000-1111-2222-123456789abc" // BleConst.PRIMARY_SERVICE_UUID
// os_log("📝 设置服务UUID: %{public}@", log: logger, type: .debug, serviceUUID)
// descriptor.bluetoothServiceUUID = CBUUID(string: serviceUUID)
// 设置制造商ID
os_log("设置公司ID: 0xABEF", log: logger, type: .debug)
// descriptor.setValue(UInt16(0xABEF), forKey: "bluetoothCompanyIdentifier")
descriptor.bluetoothCompanyIdentifier = ASBluetoothCompanyIdentifier(0xEFAB)
// 创建默认图像
os_log("获取设备图标...", log: logger, type: .debug)
let deviceImage = UIImage(systemName: "headphones") ?? UIImage()
// 创建显示项
os_log("创建选择器显示项...", log: logger, type: .debug)
let displayItem = ASPickerDisplayItem(
name: "语音助理耳机",
productImage: deviceImage,
descriptor: descriptor
)
// 确保在主线程上调用
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
os_log("调用showPicker方法...", log: self.logger, type: .debug)
// 再次检查Info.plist配置
let bundle = Bundle.main
os_log("Info.plist检查:", log: self.logger, type: .debug)
if let supports = bundle.object(forInfoDictionaryKey: "NSAccessorySetupKitSupports") as? [String] {
os_log("NSAccessorySetupKitSupports: %{public}@", log: self.logger, type: .debug, supports.description)
}
if let services = bundle.object(forInfoDictionaryKey: "NSAccessorySetupBluetoothServices") as? [String] {
os_log("NSAccessorySetupBluetoothServices: %{public}@", log: self.logger, type: .debug, services.description)
}
if let companyIds = bundle.object(forInfoDictionaryKey: "NSAccessorySetupBluetoothCompanyIdentifiers") as? [String] {
os_log("NSAccessorySetupBluetoothCompanyIdentifiers: %{public}@", log: self.logger, type: .debug, companyIds.description)
}
os_log("iOS 18环境下不检查蓝牙状态,避免创建CBCentralManager", log: self.logger, type: .debug)
// 使用标准方法显示选择器
wrapper.session.showPicker(for: [displayItem]) { [weak self] error in
guard let self = self else { return }
if let error = error {
os_log("设备选择器显示错误: %{public}@", log: self.logger, type: .error, error.localizedDescription)
// 错误码解析
if let nsError = error as? NSError {
os_log("错误域: %{public}@, 错误码: %{public}d", log: self.logger, type: .error, nsError.domain, nsError.code)
// 解析常见错误
switch nsError.code {
case 550:
os_log("错误550: CBCentralManager冲突 - 进程中已存在使用全局权限的CBCentralManager", log: self.logger, type: .error)
case 500...599:
os_log("错误%{public}d: 可能是AccessorySetupKit内部错误", log: self.logger, type: .error, nsError.code)
default:
os_log("未知错误码", log: self.logger, type: .error)
}
if let info = nsError.userInfo as? [String: Any] {
os_log("错误信息: %{public}@", log: self.logger, type: .error, info.description)
}
}
} else {
os_log("设备选择器显示已触发", log: self.logger, type: .info)
}
}
}
} else {
os_log("会话未初始化,无法显示设备选择器", log: logger, type: .error)
}
} else {
os_log("当前iOS版本不支持AccessorySetupKit", log: logger, type: .info)
}
#else
os_log("构建环境不支持AccessorySetupKit", log: logger, type: .info)
#endif
}
// 处理发现的设备
func handleDiscoveredAccessory(uuid: UUID) {
#if canImport(AccessorySetupKit) && swift(>=5.9)
if #available(iOS 18.0, *) {
os_log("处理UUID为%{public}@的发现设备", log: logger, type: .debug, uuid.uuidString)
guard let service = bleService as? BleService else {
os_log("BLE服务未设置或类型转换失败", log: logger, type: .error)
return
}
os_log("按照iOS 18规范,直接使用UUID调用特定连接方法", log: logger, type: .debug)
service.connectToAccessory(uuid: uuid)
}
#endif
}
// 从指定的视图控制器显示设备选择器
func showDevicePickerFromViewController(_ viewController: UIViewController) {
#if canImport(AccessorySetupKit) && swift(>=5.9)
if #available(iOS 18.0, *) {
os_log("从视图控制器显示设备选择器...", log: logger, type: .debug)
safeShowDevicePickerWithUI(presentingVC: viewController)
} else {
os_log("当前iOS版本不支持AccessorySetupKit", log: logger, type: .info)
}
#else
os_log("构建环境不支持AccessorySetupKit", log: logger, type: .info)
#endif
}
// 带UI上下文的安全显示选择器
private func safeShowDevicePickerWithUI(presentingVC: UIViewController) {
#if canImport(AccessorySetupKit) && swift(>=5.9)
if #available(iOS 18.0, *) {
resetSession()
initializeSessionAndRun { [weak self, weak presentingVC] in
guard let self = self, let presentingVC = presentingVC else { return }
if let wrapper = self.accessorySessionWrapper as? AccessorySessionWrapper {
os_log("会话已激活,准备显示选择器", log: self.logger, type: .info)
// 从主线程创建和显示
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
os_log("创建设备描述符...", log: self.logger, type: .debug)
let descriptor = ASDiscoveryDescriptor()
// 设置服务UUID和制造商ID
// let serviceUUID = "0000abc0-0000-1111-2222-123456789abc"
// os_log("设置服务UUID: %{public}@", log: self.logger, type: .debug, serviceUUID)
// descriptor.bluetoothServiceUUID = CBUUID(string: serviceUUID)
os_log("设置公司ID: 0xABEF", log: self.logger, type: .debug)
// descriptor.setValue(UInt16(0xABEF), forKey: "bluetoothCompanyIdentifier")
descriptor.bluetoothCompanyIdentifier = ASBluetoothCompanyIdentifier(0xEFAB)
// 创建默认图像
let deviceImage = UIImage(systemName: "headphones") ?? UIImage()
// 创建显示项
let displayItem = ASPickerDisplayItem(
name: "语音助理耳机",
productImage: deviceImage,
descriptor: descriptor
)
// 检查权限,但不创建CBCentralManager
os_log("iOS 18环境下不检查蓝牙状态,避免创建CBCentralManager", log: self.logger, type: .debug)
// 显示选择器
wrapper.session.showPicker(for: [displayItem]) { [weak self] error in
guard let self = self else { return }
if let error = error {
os_log("设备选择器显示错误: %{public}@", log: self.logger, type: .error, error.localizedDescription)
// 错误码解析
if let nsError = error as? NSError {
os_log("错误域: %{public}@, 错误码: %{public}d", log: self.logger, type: .error, nsError.domain, nsError.code)
// 解析常见错误
switch nsError.code {
case 550:
os_log("错误550: CBCentralManager冲突 - 进程中已存在使用全局权限的CBCentralManager", log: self.logger, type: .error)
case 500...599:
os_log("错误%{public}d: 可能是AccessorySetupKit内部错误", log: self.logger, type: .error, nsError.code)
default:
os_log("未知错误码", log: self.logger, type: .error)
}
if let info = nsError.userInfo as? [String: Any] {
os_log("错误信息: %{public}@", log: self.logger, type: .error, info.description)
}
}
} else {
os_log("设备选择器显示已触发", log: self.logger, type: .info)
}
}
}
}
}
}
#endif
}
// 检查蓝牙是否可用,不使用CBCentralManager
func isBluetoothAvailable() -> Bool {
#if canImport(AccessorySetupKit) && swift(>=5.9)
if #available(iOS 18.0, *) {
// iOS 18+环境下不进行蓝牙状态检查
os_log("iOS 18环境下,跳过蓝牙状态检查", log: logger, type: .debug)
return true
}
#endif
// 使用AVAudioSession检测是否有蓝牙设备连接
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
}
}
// 没有检测到蓝牙设备,但不确定蓝牙是否已打开
return false
}
}