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.

305 lines
10 KiB

import Foundation
import CoreBluetooth
import os.log
import AVFoundation
import AccessorySetupKit
import UIKit
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 let deviceCompanyIdentifier: UInt16 = 0xEFAB
private let deviceDisplayName = "语音助理耳机"
// 用于存储配对设备UUID的UserDefaults键
private let pairedDeviceUUIDKey = "com.yunqiinnovation.ble_service.paired_device_uuid"
typealias DeviceDiscoveredCallback = (UUID) -> Void
private var onDeviceDiscovered: DeviceDiscoveredCallback?
// MARK: - 初始化
private override init() { super.init() }
// MARK: - 公共方法
func setBleService(_ service: Any) {
self.bleService = service as? BleService
}
func setDeviceDiscoveredCallback(_ callback: @escaping (UUID) -> Void) {
self.onDeviceDiscovered = callback
}
// 处理发现的设备并连接
func connectToAccessory(uuid: UUID) {
guard let service = bleService else {
return
}
service.connectToAccessory(uuid: uuid)
}
func addToReconnectQueue(uuid: UUID) {
guard let service = bleService else {
return
}
service.addToReconnectQueue(uuid: uuid)
}
// 从视图控制器显示设备选择器 - 主要入口点
func showDevicePickerFromViewController(_ viewController: UIViewController) {
initializeSession()
}
// 清除蓝牙状态(仅用于测试或重置,不会清除系统授权)
func resetBluetoothState() {
if let service = bleService {
service.disconnect()
}
}
// 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()
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
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
}
// MARK: - 设备选择器显示
private func showDevicePicker() {
guard let session = session, isSessionActivated else {
os_log("无法显示配对框:会话未激活", log: logger, type: .error)
return
}
os_log("准备显示设备配对框", log: logger, type: .info)
let descriptor = ASDiscoveryDescriptor()
descriptor.bluetoothCompanyIdentifier = ASBluetoothCompanyIdentifier(deviceCompanyIdentifier)
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 }
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()
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 .accessoryChanged:
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 .pickerSetupFailed:
os_log("选择器设置失败: %{public}@", log: logger, type: .error,
event.error?.localizedDescription ?? "未知错误")
default:
os_log("未处理的会话事件: %d", log: logger, type: .info, event.eventType.rawValue)
break
}
}
private func handleSessionActivated() {
isSessionActivated = true
isInitializing = false
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()
}
}
private func handleAccessoryAdded(_ accessory: ASAccessory?) {
guard let accessory = accessory, let uuid = accessory.bluetoothIdentifier else {
return
}
// 保存配对设备的UUID
savePairedMacAddress(uuid)
onDeviceDiscovered?(uuid)
connectToAccessory(uuid: uuid)
}
private func handleAccessoryRemoved(_ accessory: ASAccessory?) {
guard let accessory = accessory, let uuid = accessory.bluetoothIdentifier else {
return
}
if let service = bleService {
service.disconnect()
}
}
private func handleSessionInitError(_ error: Error) {
isInitializing = false
session = nil
}
// 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)
}
return true
}
// MARK: - 清除配对关系
func clearAssociations() {
resetBluetoothState()
// 清除保存的配对设备UUID
clearPairedMacAddress()
#if canImport(AccessorySetupKit) && swift(>=5.9)
if #available(iOS 18.0, *) {
resetSession()
initializeSession()
}
#endif
}
// 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
/// - 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)
}
}