import Foundation import CoreBluetooth import os.log import AVFoundation /// BLE服务类 - 专为AccessorySetupKit设备设计,支持自动重连和状态恢复 /// /// 核心特性: /// 1. 外设掉电→上电后自动重连(通过pendingReconnectUUIDs队列) /// 2. App被系统终止后,外设广播或特征通知时自动恢复连接 /// 3. 使用CBCentralManagerOptionRestoreIdentifierKey实现系统级状态保留 /// 4. 支持bluetooth-central后台模式,允许后台BLE操作 /// 5. willRestoreState恢复系统保留的外设、扫描条件 /// 6. 统一的重连队列管理,适配AccessorySetupKit工作流程 @available(iOS 13.0, *) public class BleService: NSObject { // MARK: - 单例实现 public static let shared = BleService() // MARK: - 回调代理协议 public protocol Callback: AnyObject { // 扫描相关回调 func onScanResult(devices: [[String: Any]]) // 连接状态相关回调 func onConnectionStateChanged(state: Int) // 数据相关回调 func onAudioDataReceived(data: Data) // 唤醒信号回调 func onWakeupSignalReceived() // 设备信息相关回调 func onDeviceInfoReceived(infoType: Int, infoData: [String: Any]) } // MARK: - 属性 private var centralManager: CBCentralManager? private var peripheralDevice: CBPeripheral? private var writeCharacteristic: CBCharacteristic? private var notifyCharacteristic: CBCharacteristic? private var audioCharacteristic: CBCharacteristic? private var isScanning = false private var callbacks = [Callback]() private var connectionState = BleConst.STATE_DISCONNECTED // 状态恢复相关 private var restoredPeripherals: [CBPeripheral] = [] // 自动重连队列(统一管理所有待连接的设备) private var pendingReconnectUUIDs = Set() private let logger = OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleService") // MARK: - Opus音频处理器 private var opusProcessor: SwiftOpusAudioProcessor? // 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) return } os_log("🔧 开始初始化CBCentralManager", log: logger, type: .info) let options: [String: Any] = [ CBCentralManagerOptionRestoreIdentifierKey: "BLEAssistantCentral", CBCentralManagerOptionShowPowerAlertKey: true ] centralManager = CBCentralManager(delegate: self, queue: nil, options: options) os_log("🔧 CBCentralManager初始化完成,等待状态更新", log: logger, type: .debug) } // 初始化Opus处理器 private func initializeOpusProcessor() { opusProcessor = SwiftOpusAudioProcessor() opusProcessor?.setCallback(self) // 立即启动Opus解码流,保持整个生命周期内活跃 startOpusDecoding() } // 启动Opus解码流 private func startOpusDecoding() { let opusStarted = opusProcessor?.startDecoding( sampleRate: 16000, // 设备端Opus编码采样率 channels: 1, // 单声道 frameSize: 640 // 40ms at 16kHz = 640 samples ) ?? false if !opusStarted { os_log("Opus解码流启动失败", log: logger, type: .error) } } // MARK: - 回调管理 /// 添加回调代理 public func addCallback(_ callback: Callback) -> Bool { if !callbacks.contains(where: { $0 === callback }) { callbacks.append(callback) os_log("已添加BLE回调, 当前回调数: %d", log: logger, type: .debug, callbacks.count) return true } return false } /// 移除回调代理 public func removeCallback(_ callback: Callback) -> Bool { if let index = callbacks.firstIndex(where: { $0 === callback }) { callbacks.remove(at: index) os_log("已移除BLE回调, 当前回调数: %d", log: logger, type: .debug, callbacks.count) return true } return false } /// 清除所有回调 public func clearCallbacks() { callbacks.removeAll() os_log("已清除所有BLE回调", log: logger, type: .debug) } /// 设置代理(单一代理模式,用于替代addCallback) public func setDelegate(_ delegate: Callback?) { callbacks.removeAll() if let delegate = delegate { callbacks.append(delegate) os_log("设置BLE单一代理", log: logger, type: .debug) } else { os_log("清除BLE单一代理", log: logger, type: .debug) } } // MARK: - 公共方法 /// 获取中央管理器,如果未初始化则先初始化 public func getCentralManager() -> CBCentralManager { if centralManager == nil { initializeCentralManager() } return centralManager! } /// 开始扫描BLE设备 public func startScan() { #if canImport(AccessorySetupKit) && swift(>=5.9) if #available(iOS 18.0, *) { // iOS 18+不使用传统扫描 return } #endif if centralManager == nil { initializeCentralManager() } guard let manager = centralManager, manager.state == .poweredOn else { return } if isScanning { stopScan() } isScanning = true let serviceUUIDs = [BleConst.PRIMARY_SERVICE_UUID, BleConst.AUDIO_SERVICE_UUID] manager.scanForPeripherals(withServices: serviceUUIDs, options: [ CBCentralManagerScanOptionAllowDuplicatesKey: false ]) os_log("开始扫描BLE设备...", log: logger, type: .info) } /// 停止扫描 public func stopScan() { if isScanning && centralManager != nil { centralManager?.stopScan() isScanning = false os_log("停止BLE扫描", log: logger, type: .info) } } /// 连接到指定设备 public func connect(to peripheral: CBPeripheral) { connectPeripheral(peripheral) } /// 统一的连接方法,支持恢复和重连 private func connectPeripheral(_ peripheral: CBPeripheral) { guard let manager = centralManager else { return } if peripheral.state == .connected { 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) return } if connectionState == BleConst.STATE_CONNECTED && peripheralDevice?.identifier != peripheral.identifier { os_log("断开当前设备,连接新设备", log: logger, type: .info) disconnect() } updateConnectionState(BleConst.STATE_CONNECTING) peripheralDevice = peripheral peripheralDevice?.delegate = self manager.connect(peripheral, options: [ CBConnectPeripheralOptionNotifyOnConnectionKey: true, CBConnectPeripheralOptionNotifyOnDisconnectionKey: true, CBConnectPeripheralOptionNotifyOnNotificationKey: true ]) os_log("开始连接到设备: %@", log: logger, type: .info, peripheral.identifier.uuidString) } /// 恢复已连接设备的会话 private func resumeSession(for peripheral: CBPeripheral) { peripheralDevice = peripheral peripheralDevice?.delegate = self updateConnectionState(BleConst.STATE_CONNECTED) if opusProcessor?.isDecoding() != true { startOpusDecoding() } // 清空特征引用,强制重新发现 writeCharacteristic = nil notifyCharacteristic = nil audioCharacteristic = nil 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) } /// 断开当前连接 public func disconnect() { guard let peripheral = peripheralDevice, connectionState == BleConst.STATE_CONNECTED else { return } if centralManager != nil { updateConnectionState(BleConst.STATE_DISCONNECTING) centralManager?.cancelPeripheralConnection(peripheral) os_log("开始断开设备连接", log: logger, type: .info) } } /// 检查是否已连接 public func isConnected() -> Bool { return connectionState == BleConst.STATE_CONNECTED } // MARK: - 回调通知方法 /// 通知所有回调扫描结果 private func notifyScanResult(devices: [[String: Any]]) { for callback in callbacks { callback.onScanResult(devices: devices) } } /// 通知所有回调连接状态变化 private func notifyConnectionStateChanged(state: Int) { for callback in callbacks { callback.onConnectionStateChanged(state: state) } } /// 通知所有回调接收到音频数据 private func notifyAudioDataReceived(data: Data) { for callback in callbacks { callback.onAudioDataReceived(data: data) } } /// 通知所有回调接收到唤醒信号 private func notifyWakeupSignalReceived() { for callback in callbacks { callback.onWakeupSignalReceived() } } /// 通知所有回调接收到设备信息 private func notifyDeviceInfoReceived(infoType: Int, infoData: [String: Any]) { for callback in callbacks { callback.onDeviceInfoReceived(infoType: infoType, infoData: infoData) } } // MARK: - 设备信息和控制命令 /// 获取设备版本号 func getDeviceVersion() -> Bool { os_log("获取耳机版本号...", log: logger, type: .info) let paramData = Data([0xFF]) return sendCommand(BleConst.CMD_GET_VERSION, data: paramData) } /// 获取产品ID func getProductId() -> Bool { os_log("获取耳机产品ID...", log: logger, type: .info) let paramData = Data([0xFF]) return sendCommand(BleConst.CMD_GET_PRODUCT_ID, data: paramData) } /// 获取颜色ID func getColorId() -> Bool { os_log("获取耳机颜色ID...", log: logger, type: .info) let paramData = Data([0xFF]) return sendCommand(BleConst.CMD_GET_COLOR_ID, data: paramData) } /// 获取电池信息 func getBatteryInfo() -> Bool { os_log("获取耳机电量信息...", log: logger, type: .info) let paramData = Data([0xFF]) return sendCommand(BleConst.CMD_GET_BATTERY_INFO, data: paramData) } // MARK: - 编解码控制 /// 打开编码器 func openEncoder() -> Bool { os_log("🎙️ 准备打开编码(设备侧)...", log: logger, type: .info) let paramData = Data([BleConst.CODEC_CONTROL_ENCODE_ON, BleConst.AUDIO_CHANNEL_RIGHT]) let result = sendCommand(BleConst.CMD_CONTROL_CODEC, data: paramData) if result { os_log("✅ 编码器命令发送成功", log: logger, type: .info) } else { os_log("❌ 编码器命令发送失败", log: logger, type: .error) } return result } /// 打开解码器 func openDecoder() -> Bool { os_log("打开解码...", log: logger, type: .info) // 确保Opus解码器处于活跃状态(如果不是则启动) if opusProcessor?.isDecoding() != true { startOpusDecoding() } // 发送命令通知设备开始解码 let paramData = Data([BleConst.CODEC_CONTROL_DECODE_ON, BleConst.AUDIO_CHANNEL_RIGHT]) return sendCommand(BleConst.CMD_CONTROL_CODEC, data: paramData) } /// 打开A2DP解码器 func openA2DPDecoder() -> Bool { os_log("打开A2DP解码...", log: logger, type: .info) let paramData = Data([BleConst.CODEC_CONTROL_A2DP_PLAY, BleConst.AUDIO_CHANNEL_RIGHT]) return sendCommand(BleConst.CMD_CONTROL_CODEC, data: paramData) } /// 关闭编解码器 func closeCodec() -> Bool { os_log("关闭编解码...", log: logger, type: .info) // 不停止Opus解码流,只发送命令通知设备关闭编解码 let paramData = Data([BleConst.CODEC_CONTROL_CLOSE, BleConst.AUDIO_CHANNEL_RIGHT]) return sendCommand(BleConst.CMD_CONTROL_CODEC, data: paramData) } /// 播放A2DP func playA2DP() -> Bool { os_log("切换至A2DP播放模式...", log: logger, type: .info) return sendCommand(BleConst.CMD_PLAY_A2DP) } /// 音量增大 func volumeUp() -> Bool { os_log("设备音量增加...", log: logger, type: .info) return sendCommand(BleConst.CMD_VOLUME_UP) } /// 音量减小 func volumeDown() -> Bool { os_log("设备音量减小...", log: logger, type: .info) return sendCommand(BleConst.CMD_VOLUME_DOWN) } /// 设置声道模式 func setChannelMode(_ channelMode: UInt8) -> Bool { let modeDesc: String switch channelMode { case BleConst.AUDIO_CHANNEL_LEFT: modeDesc = "左声道" case BleConst.AUDIO_CHANNEL_RIGHT: modeDesc = "右声道" case BleConst.AUDIO_CHANNEL_STEREO: modeDesc = "立体声" default: os_log("无效的声道模式: %d", log: logger, type: .error, channelMode) return false } os_log("设置%@模式...", log: logger, type: .info, modeDesc) let paramData = Data([0xA1, channelMode]) return sendCommand(BleConst.CMD_CONTROL_CODEC, data: paramData) } // MARK: - 私有方法 /// 更新连接状态并通知回调 private func updateConnectionState(_ state: Int) { let oldState = connectionState connectionState = state let stateDesc = { (state: Int) -> String in switch state { case BleConst.STATE_DISCONNECTED: return "DISCONNECTED" case BleConst.STATE_CONNECTING: return "CONNECTING" case BleConst.STATE_CONNECTED: return "CONNECTED" case BleConst.STATE_DISCONNECTING: return "DISCONNECTING" case BleConst.STATE_ERROR: return "ERROR" default: return "UNKNOWN(\(state))" } } if oldState != state { os_log("📱 BLE连接状态变化: %@ → %@", log: logger, type: .info, stateDesc(oldState), stateDesc(state)) } notifyConnectionStateChanged(state: state) } /// 向设备发送命令 private func sendCommand(_ cmd: UInt8, data: Data = Data()) -> Bool { guard let characteristic = writeCharacteristic, connectionState == BleConst.STATE_CONNECTED else { os_log("发送命令失败: 设备未连接", log: logger, type: .error) return false } do { // 命令数据长度 let cmdLength = UInt8(data.count) // 准备完整的命令包:帧头 + 命令ID + 长度 + 数据 + CRC var cmdPacket = Data() cmdPacket.append(BleConst.APP_REQUEST_HEADER) // 帧头 cmdPacket.append(cmd) // 命令类型 cmdPacket.append(cmdLength) // 命令长度 // 添加命令数据(如果有) if !data.isEmpty { cmdPacket.append(data) } // 计算CRC校验 - 对除CRC外的所有字节 let crc = calculateCRC8(data: cmdPacket) cmdPacket.append(crc) // 记录命令详情 var dataHexString = "" if !data.isEmpty { dataHexString = data.map { String(format: "0x%02X", $0) }.joined(separator: ", ") } os_log("发送命令: CMD=0x%02X, 长度=%d, 数据=[%@], CRC=0x%02X", log: logger, type: .debug, cmd, cmdLength, dataHexString, crc) // 发送数据(使用无响应写入,与Android端保持一致) peripheralDevice?.writeValue(cmdPacket, for: characteristic, type: .withoutResponse) return true } catch { os_log("发送命令异常: %@", log: logger, type: .error, error.localizedDescription) return false } } /// 计算CRC8/MAXIM校验值(与Android端保持一致) private func calculateCRC8(data: Data) -> UInt8 { var crc: Int = 0 let polynomial: Int = 0x31 for byte in data { crc = crc ^ (Int(byte) & 0xFF) for _ in 0..<8 { if (crc & 0x80) != 0 { crc = ((crc << 1) ^ polynomial) & 0xFF } else { crc = (crc << 1) & 0xFF } } } return UInt8(crc & 0xFF) } /// 处理收到的音频数据 private func processAudioData(_ data: Data) { // 简单过滤:只处理合理大小的音频包 guard data.count > 0 && data.count <= 1275 else { return } // 使用Opus处理器处理音频数据 opusProcessor?.processAudioData(data) } /// 处理接收到的响应数据 private func handleResponseData(_ data: Data) { guard data.count >= 4 else { os_log("响应数据格式错误:数据长度过短", log: logger, type: .error) return } // 提取帧信息 let header = data[0] let cmd = data[1] let length = data[2] // 检查帧头,确保是设备响应 guard header == BleConst.DEVICE_RESPONSE_HEADER else { // 如果是设备主动上报 if header == BleConst.DEVICE_NOTIFY_HEADER { handleNotificationData(data) } return } // 记录响应详情 var hexString = data.map { String(format: "0x%02X", $0) }.joined(separator: ", ") os_log("收到设备响应: %@", log: logger, type: .debug, hexString) // 检查数据长度 if data.count != Int(length) + 4 { // 帧头 + 命令 + 长度 + 数据 + CRC os_log("设备响应数据长度不匹配: 期望=%d, 实际=%d", log: logger, type: .error, Int(length) + 4, data.count) return } // 计算CRC校验 - 不包括最后一个字节(CRC) let payload = data.prefix(data.count - 1) let calculatedCrc = calculateCRC8(data: payload) let receivedCrc = data[data.count - 1] if calculatedCrc != receivedCrc { os_log("设备响应CRC校验失败: 计算=0x%02X, 接收=0x%02X", log: logger, type: .error, calculatedCrc, receivedCrc) return } // 提取响应数据 let responseData = length > 0 ? data.subdata(in: 3..<(3 + Int(length))) : Data() // 根据命令类型处理设备响应 switch cmd { case BleConst.CMD_GET_VERSION: var resultMap: [String: Any] = [ "success": true, "command": "versionInfo", "rawData": responseData ] if !responseData.isEmpty { if let versionStr = String(data: responseData, encoding: .utf8) { resultMap["version"] = versionStr os_log("接收到目标版本号: %@", log: logger, type: .info, versionStr) } else { // 如果无法解析为字符串,以点分隔的数字表示版本 let versionNums = responseData .map { $0 } .filter { $0 != 0xFF } // 过滤掉无效字节 .map { String($0) } .joined(separator: ".") resultMap["version"] = versionNums os_log("接收到目标版本号(数字格式): %@", log: logger, type: .info, versionNums) } } notifyDeviceInfoReceived(infoType: Int(cmd), infoData: resultMap) case BleConst.CMD_GET_PRODUCT_ID: var resultMap: [String: Any] = [ "success": true, "command": "productId", "rawData": responseData ] if responseData.count >= 2 { let productId = UInt16(responseData[0]) << 8 | UInt16(responseData[1]) resultMap["productId"] = productId os_log("接收到产品ID: %d", log: logger, type: .info, productId) } else if !responseData.isEmpty { // 单字节产品ID let productId = UInt16(responseData[0]) resultMap["productId"] = productId os_log("接收到产品ID: %d", log: logger, type: .info, productId) } notifyDeviceInfoReceived(infoType: Int(cmd), infoData: resultMap) case BleConst.CMD_GET_COLOR_ID: var resultMap: [String: Any] = [ "success": true, "command": "colorInfo", "rawData": responseData ] if !responseData.isEmpty { let colorId = Int(responseData[0]) resultMap["colorId"] = colorId os_log("接收到耳机颜色ID: %d", log: logger, type: .info, colorId) } notifyDeviceInfoReceived(infoType: Int(cmd), infoData: resultMap) case BleConst.CMD_GET_BATTERY_INFO: var resultMap: [String: Any] = [ "success": true, "command": "batteryInfo", "rawData": responseData ] if responseData.count >= 3 { // 处理电池信息 - 左耳、右耳、充电仓 let leftEarBattery = Int(responseData[0] & 0x7F) let leftEarCharging = (responseData[0] & 0x80) != 0 let rightEarBattery = Int(responseData[1] & 0x7F) let rightEarCharging = (responseData[1] & 0x80) != 0 let caseBattery = Int(responseData[2] & 0x7F) let caseCharging = (responseData[2] & 0x80) != 0 // 添加到结果Map resultMap["leftEarBattery"] = leftEarBattery resultMap["leftEarCharging"] = leftEarCharging resultMap["rightEarBattery"] = rightEarBattery resultMap["rightEarCharging"] = rightEarCharging resultMap["caseBattery"] = caseBattery resultMap["caseCharging"] = caseCharging resultMap["batteryLevel"] = leftEarBattery // 整体电量用左耳表示 os_log("接收到电量信息: 左耳=%d%% (%@), 右耳=%d%% (%@), 充电仓=%d%% (%@)", log: logger, type: .info, leftEarBattery, leftEarCharging ? "充电中" : "未充电", rightEarBattery, rightEarCharging ? "充电中" : "未充电", caseBattery, caseCharging ? "充电中" : "未充电") } else if !responseData.isEmpty { // 兼容单字节电量 let batteryLevel = Int(responseData[0]) resultMap["batteryLevel"] = batteryLevel os_log("接收到电量信息(简化): %d%%", log: logger, type: .info, batteryLevel) } notifyDeviceInfoReceived(infoType: Int(cmd), infoData: resultMap) case BleConst.CMD_CONTROL_CODEC: var resultMap: [String: Any] = [ "success": true, "command": Int(cmd), "rawData": responseData ] if responseData.count >= 2 { let codecStatus = Int(responseData[0]) let channelMode = Int(responseData[1]) // 解码状态描述 let statusDesc: String switch codecStatus { case Int(BleConst.CODEC_CONTROL_CLOSE): statusDesc = "已关闭编解码" case Int(BleConst.CODEC_CONTROL_DECODE_ON): statusDesc = "已打开解码" case Int(BleConst.CODEC_CONTROL_A2DP_PLAY): statusDesc = "A2DP播放模式" case Int(BleConst.CODEC_CONTROL_ENCODE_ON): statusDesc = "已打开编码" default: statusDesc = "未知状态(\(codecStatus))" } // 声道模式描述 let channelDesc: String switch channelMode { case Int(BleConst.AUDIO_CHANNEL_LEFT): channelDesc = "左声道" case Int(BleConst.AUDIO_CHANNEL_RIGHT): channelDesc = "右声道" case Int(BleConst.AUDIO_CHANNEL_STEREO): channelDesc = "立体声" default: channelDesc = "未知声道(\(channelMode))" } resultMap["codecStatus"] = codecStatus resultMap["channelMode"] = channelMode resultMap["statusDesc"] = statusDesc resultMap["channelDesc"] = channelDesc os_log("编解码控制响应: %@, %@", log: logger, type: .info, statusDesc, channelDesc) } notifyDeviceInfoReceived(infoType: Int(cmd), infoData: resultMap) default: // 对于未知命令,返回原始数据 let resultMap: [String: Any] = [ "success": true, "command": Int(cmd), "rawData": responseData, "isUnknown": true ] os_log("收到未知命令响应: 0x%02X", log: logger, type: .debug, cmd) notifyDeviceInfoReceived(infoType: Int(cmd), infoData: resultMap) } } /// 处理设备主动上报的通知数据 private func handleNotificationData(_ data: Data) { guard data.count >= 4 else { os_log("设备主动上报数据格式错误:数据长度过短", log: logger, type: .error) return } let header = data[0] // 0xCC let infoType = data[1] let length = data[2] var hexString = data.map { String(format: "0x%02X", $0) }.joined(separator: ", ") os_log("收到设备主动上报: %@", log: logger, type: .debug, hexString) // 检查数据长度 if data.count != Int(length) + 4 { // 帧头 + 类型 + 长度 + 数据 + CRC os_log("设备主动上报数据长度不匹配: 期望=%d, 实际=%d", log: logger, type: .error, Int(length) + 4, data.count) return } // 计算CRC校验 - 不包括最后一个字节(CRC) let payload = data.prefix(data.count - 1) let calculatedCrc = calculateCRC8(data: payload) let receivedCrc = data[data.count - 1] if calculatedCrc != receivedCrc { os_log("设备主动上报CRC校验失败: 计算=0x%02X, 接收=0x%02X", log: logger, type: .error, calculatedCrc, receivedCrc) return } // 提取上报数据 let notifyData = length > 0 ? data.subdata(in: 3..<(3 + Int(length))) : Data() // 根据上报类型处理 switch infoType { case BleConst.CMD_GET_BATTERY_INFO: var resultMap: [String: Any] = [ "success": true, "command": Int(infoType), "rawData": notifyData, "isNotification": true // 标记为主动上报 ] if notifyData.count >= 3 { // 处理电池信息 - 左耳、右耳、充电仓 let leftEarBattery = Int(notifyData[0] & 0x7F) let leftEarCharging = (notifyData[0] & 0x80) != 0 let rightEarBattery = Int(notifyData[1] & 0x7F) let rightEarCharging = (notifyData[1] & 0x80) != 0 let caseBattery = Int(notifyData[2] & 0x7F) let caseCharging = (notifyData[2] & 0x80) != 0 // 添加到结果Map resultMap["leftEarBattery"] = leftEarBattery resultMap["leftEarCharging"] = leftEarCharging resultMap["rightEarBattery"] = rightEarBattery resultMap["rightEarCharging"] = rightEarCharging resultMap["caseBattery"] = caseBattery resultMap["caseCharging"] = caseCharging resultMap["batteryLevel"] = leftEarBattery // 整体电量用左耳表示 os_log("收到主动上报电量信息: 左耳=%d%% (%@), 右耳=%d%% (%@), 充电仓=%d%% (%@)", log: logger, type: .info, leftEarBattery, leftEarCharging ? "充电中" : "未充电", rightEarBattery, rightEarCharging ? "充电中" : "未充电", caseBattery, caseCharging ? "充电中" : "未充电") } notifyDeviceInfoReceived(infoType: Int(infoType), infoData: resultMap) case BleConst.CMD_WAKE_UP: os_log("📢 收到唤醒指令", log: logger, type: .info) os_log("🔍 当前状态检查: 连接状态=%d, writeCharacteristic=%@, notifyCharacteristic=%@, audioCharacteristic=%@", log: logger, type: .debug, connectionState, writeCharacteristic != nil ? "✅" : "❌", notifyCharacteristic != nil ? "✅" : "❌", audioCharacteristic != nil ? "✅" : "❌") // 接收到唤醒信号,打开编码器 (设备侧) let openResult = openEncoder() os_log("🎙️ 尝试打开编码器结果: %@", log: logger, type: .info, openResult ? "成功" : "失败") notifyWakeupSignalReceived() default: // 对于未知上报类型,返回原始数据 let resultMap: [String: Any] = [ "success": true, "command": Int(infoType), "rawData": notifyData, "isUnknown": true, "isNotification": true // 标记为主动上报 ] os_log("收到未知主动上报类型: 0x%02X", log: logger, type: .debug, infoType) notifyDeviceInfoReceived(infoType: Int(infoType), infoData: resultMap) } } // MARK: - 通知配置 /// 设置特征的通知功能(与Android端行为一致) private func setupNotification(for peripheral: CBPeripheral, characteristic: CBCharacteristic) { // 检查特征是否支持通知 guard characteristic.properties.contains(.notify) else { os_log("⚠️ 特征不支持通知功能: %{public}@", log: logger, type: .debug, characteristic.uuid.uuidString) return } os_log("🔧 开始配置特征通知: %{public}@", log: logger, type: .debug, characteristic.uuid.uuidString) // iOS CoreBluetooth会自动处理CLIENT_CHAR_CONFIG_UUID描述符 // 直接使用setNotifyValue即可,不需要手动写入描述符 peripheral.setNotifyValue(true, for: characteristic) os_log("🔧 特征通知配置完成: %{public}@", log: logger, type: .debug, characteristic.uuid.uuidString) } /// 清理所有资源 func cleanup() { stopScan() disconnect() // 停止Opus解码 opusProcessor?.cleanup() opusProcessor = nil // 清理状态恢复和重连队列 restoredPeripherals.removeAll() pendingReconnectUUIDs.removeAll() os_log("BleService资源已清理", log: logger, type: .debug) } /// 清除重连队列 public func clearReconnectQueue() { pendingReconnectUUIDs.removeAll() os_log("已清除重连队列", log: logger, type: .info) } /// 获取当前重连队列状态(用于调试) public func getReconnectQueueStatus() -> [String: Any] { return [ "reconnectQueueCount": pendingReconnectUUIDs.count, "reconnectQueueUUIDs": Array(pendingReconnectUUIDs).map { $0.uuidString }, "restoredPeripheralsCount": restoredPeripherals.count, "connectionState": connectionState, "centralManagerState": centralManager?.state.rawValue ?? -1 ] } // 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 @available(iOS 13.0, *) extension BleService: SwiftOpusAudioProcessor.AudioDataCallback { /// 接收到解码后的PCM音频数据 func onAudioDataReceived(data: Data) { notifyAudioDataReceived(data: data) } } // MARK: - CBCentralManagerDelegate @available(iOS 13.0, *) extension BleService: CBCentralManagerDelegate { public func centralManagerDidUpdateState(_ central: CBCentralManager) { os_log("🔧 CBCentralManager状态更新: %d", log: logger, type: .info, central.state.rawValue) switch central.state { case .poweredOn: os_log("🔧 蓝牙已开启", log: logger, type: .info) // 注:AccessorySetupKit应用不需要恢复传统BLE扫描 // 恢复外设连接 if !restoredPeripherals.isEmpty { os_log("🔄 恢复%d个外设连接", log: logger, type: .info, restoredPeripherals.count) for peripheral in restoredPeripherals { peripheral.delegate = self // 从重连队列中移除(避免重复处理) pendingReconnectUUIDs.remove(peripheral.identifier) if peripheral.state == .connected { os_log("🔄 外设已连接,恢复会话: %{public}@", log: logger, type: .info, peripheral.identifier.uuidString) resumeSession(for: peripheral) } else { os_log("🔄 外设未连接,开始重连: %{public}@, 状态=%d", log: logger, type: .info, peripheral.identifier.uuidString, peripheral.state.rawValue) connectPeripheral(peripheral) } } // 清空已处理的恢复外设 restoredPeripherals.removeAll() } // 恢复待重连的设备(只处理不在restoredPeripherals中的设备) if !pendingReconnectUUIDs.isEmpty { os_log("🔄 尝试重连%d个待重连设备", log: logger, type: .info, pendingReconnectUUIDs.count) let peripherals = central.retrievePeripherals(withIdentifiers: Array(pendingReconnectUUIDs)) for peripheral in peripherals { os_log("🔄 重连设备: %{public}@", log: logger, type: .info, peripheral.identifier.uuidString) connectPeripheral(peripheral) pendingReconnectUUIDs.remove(peripheral.identifier) } } case .poweredOff: os_log("蓝牙已关闭", log: logger, type: .info) updateConnectionState(BleConst.STATE_DISCONNECTED) case .resetting: os_log("蓝牙正在重置", log: logger, type: .info) // 无需额外处理,等待状态变更 case .unauthorized: os_log("蓝牙权限未授权", log: logger, type: .error) updateConnectionState(BleConst.STATE_ERROR) case .unsupported: os_log("设备不支持蓝牙", log: logger, type: .error) updateConnectionState(BleConst.STATE_ERROR) case .unknown: os_log("蓝牙状态未知", log: logger, type: .info) // 等待状态变更 @unknown default: os_log("蓝牙未知状态: %d", log: logger, type: .info, central.state.rawValue) break } } // MARK: - 重连管理 /// 添加设备到重连队列 public func addToReconnectQueue(uuid: UUID) { pendingReconnectUUIDs.insert(uuid) os_log("🔄 设备已加入重连队列: %{public}@", log: logger, type: .info, uuid.uuidString) // 立即尝试连接(如果蓝牙已就绪) tryReconnectDevices() } /// 尝试重连队列中的设备 private func tryReconnectDevices() { guard let manager = centralManager, manager.state == .poweredOn else { os_log("🔄 蓝牙未就绪,延迟重连", log: logger, type: .debug) return } guard !pendingReconnectUUIDs.isEmpty else { return } os_log("🔄 尝试重连%d个设备", log: logger, type: .info, pendingReconnectUUIDs.count) let peripherals = manager.retrievePeripherals(withIdentifiers: Array(pendingReconnectUUIDs)) for peripheral in peripherals { os_log("🔄 重连设备: %{public}@", log: logger, type: .info, peripheral.identifier.uuidString) connectPeripheral(peripheral) // 注意:不在这里移除UUID,让didConnect回调来移除 } } // 原有方法 public func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) { // 将扫描结果转换为字典格式 var deviceInfo: [String: Any] = [ "name": peripheral.name ?? "Unknown", "id": peripheral.identifier.uuidString, "rssi": RSSI.intValue ] // 添加广播数据 if let manufacturerData = advertisementData[CBAdvertisementDataManufacturerDataKey] as? Data { deviceInfo["manufacturerData"] = manufacturerData } if let serviceUUIDs = advertisementData[CBAdvertisementDataServiceUUIDsKey] as? [CBUUID] { deviceInfo["serviceUUIDs"] = serviceUUIDs.map { $0.uuidString } } // 报告给回调 notifyScanResult(devices: [deviceInfo]) } public func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) { os_log("🔗 BLE设备连接成功: %{public}@", log: logger, type: .info, peripheral.name ?? peripheral.identifier.uuidString) os_log("🔗 连接详情: UUID=%{public}@, 服务数=%d", log: logger, type: .debug, peripheral.identifier.uuidString, peripheral.services?.count ?? 0) // 从重连队列中移除(如果存在) pendingReconnectUUIDs.remove(peripheral.identifier) updateConnectionState(BleConst.STATE_CONNECTED) os_log("🔍 开始发现服务...", log: logger, type: .debug) // 查找服务 peripheral.discoverServices([BleConst.PRIMARY_SERVICE_UUID, BleConst.AUDIO_SERVICE_UUID]) // 确保Opus解码器处于活跃状态 if opusProcessor?.isDecoding() != true { startOpusDecoding() } } public func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) { let errorMsg = error?.localizedDescription ?? "未知错误" os_log("❌ BLE设备连接失败: %{public}@, 错误: %{public}@", log: logger, type: .error, peripheral.name ?? peripheral.identifier.uuidString, errorMsg) os_log("❌ 失败详情: UUID=%{public}@", log: logger, type: .debug, peripheral.identifier.uuidString) updateConnectionState(BleConst.STATE_ERROR) // 连接失败时,立即发起新的连接 os_log("🔄 连接失败,立即重新连接: %{public}@", log: logger, type: .info, peripheral.identifier.uuidString) connectPeripheral(peripheral) } public func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) { if let error = error { os_log("⚠️ BLE设备意外断开: %{public}@, 错误: %{public}@", log: logger, type: .info, peripheral.name ?? peripheral.identifier.uuidString, error.localizedDescription) } else { os_log("🔌 BLE设备正常断开: %{public}@", log: logger, type: .info, peripheral.name ?? peripheral.identifier.uuidString) } updateConnectionState(BleConst.STATE_DISCONNECTED) // 无论是否意外断开,都立即发起新的连接 if central.state == .poweredOn { os_log("🔄 立即重新连接设备: %{public}@", log: logger, type: .info, peripheral.identifier.uuidString) connectPeripheral(peripheral) } else { os_log("🔄 蓝牙未就绪,添加到重连队列: %{public}@", log: logger, type: .info, peripheral.identifier.uuidString) pendingReconnectUUIDs.insert(peripheral.identifier) } } public func centralManager(_ central: CBCentralManager, willRestoreState dict: [String : Any]) { os_log("🔄 ===== iOS系统正在恢复CBCentralManager状态 =====", log: logger, type: .info) os_log("🔄 恢复数据: %{public}@", log: logger, type: .debug, dict) // 恢复外设连接 if let peripherals = dict[CBCentralManagerRestoredStatePeripheralsKey] as? [CBPeripheral] { restoredPeripherals = peripherals os_log("🔄 恢复%d个外设连接", log: logger, type: .info, peripherals.count) for peripheral in peripherals { os_log("🔄 恢复外设: %{public}@, 状态=%d", log: logger, type: .debug, peripheral.name ?? peripheral.identifier.uuidString, peripheral.state.rawValue) } } } } // MARK: - CBPeripheralDelegate @available(iOS 13.0, *) extension BleService: CBPeripheralDelegate { public func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) { if let error = error { os_log("❌ 服务发现失败: %{public}@", log: logger, type: .error, error.localizedDescription) return } guard let services = peripheral.services else { os_log("❌ 未发现任何服务", log: logger, type: .error) return } os_log("✅ 发现%d个服务", log: logger, type: .info, services.count) for service in services { if service.uuid == BleConst.PRIMARY_SERVICE_UUID { os_log("🔍 发现主服务,开始发现写入和通知特征...", log: logger, type: .debug) // 主服务,发现写入和通知特征 peripheral.discoverCharacteristics([BleConst.WRITE_CHAR_UUID, BleConst.NOTIFY_CHAR_UUID], for: service) } else if service.uuid == BleConst.AUDIO_SERVICE_UUID { os_log("🔍 发现音频服务,开始发现音频特征...", log: logger, type: .debug) // 音频服务,发现音频特征 peripheral.discoverCharacteristics([BleConst.RECEIVE_AUDIO_CHAR_UUID], for: service) } } } public func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) { if let error = error { os_log("❌ 特征发现失败: %{public}@", log: logger, type: .error, error.localizedDescription) return } guard let characteristics = service.characteristics else { os_log("❌ 服务%{public}@未发现任何特征", log: logger, type: .error, service.uuid.uuidString) return } os_log("✅ 服务%{public}@发现%d个特征", log: logger, type: .info, service.uuid.uuidString, characteristics.count) for characteristic in characteristics { if characteristic.uuid == BleConst.WRITE_CHAR_UUID { writeCharacteristic = characteristic os_log("📝 写入特征已设置: %{public}@", log: logger, type: .info, characteristic.uuid.uuidString) } else if characteristic.uuid == BleConst.NOTIFY_CHAR_UUID { notifyCharacteristic = characteristic os_log("🔔 通知特征已设置,开始发现描述符...", log: logger, type: .info) // 先发现描述符,再配置通知 peripheral.discoverDescriptors(for: characteristic) } else if characteristic.uuid == BleConst.RECEIVE_AUDIO_CHAR_UUID { audioCharacteristic = characteristic os_log("🎵 音频特征已设置,开始发现描述符...", log: logger, type: .info) // 先发现描述符,再配置通知 peripheral.discoverDescriptors(for: characteristic) } } // 检查所有必要特征是否已准备就绪 let isReady = writeCharacteristic != nil && notifyCharacteristic != nil && audioCharacteristic != nil if isReady { os_log("🚀 所有BLE特征已准备就绪,可以发送命令", log: logger, type: .info) } else { os_log("⏳ 等待更多特征准备: 写入=%@, 通知=%@, 音频=%@", log: logger, type: .debug, writeCharacteristic != nil ? "✅" : "❌", notifyCharacteristic != nil ? "✅" : "❌", audioCharacteristic != nil ? "✅" : "❌") } } public func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { if let error = error { return } guard let data = characteristic.value, !data.isEmpty else { return } if characteristic.uuid == BleConst.NOTIFY_CHAR_UUID { // 处理通知数据 handleResponseData(data) } else if characteristic.uuid == BleConst.RECEIVE_AUDIO_CHAR_UUID { // 处理音频数据 processAudioData(data) } } public func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) { // 写入完成回调,不需要特殊处理 } public func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) { // 通知状态更新回调,不需要特殊处理 } public func peripheral(_ peripheral: CBPeripheral, didDiscoverDescriptorsFor characteristic: CBCharacteristic, error: Error?) { if let error = error { os_log("❌ 描述符发现失败: %{public}@", log: logger, type: .error, error.localizedDescription) return } os_log("✅ 特征%{public}@发现%d个描述符", log: logger, type: .debug, characteristic.uuid.uuidString, characteristic.descriptors?.count ?? 0) // 对通知特征和音频特征配置通知 if characteristic.uuid == BleConst.NOTIFY_CHAR_UUID || characteristic.uuid == BleConst.RECEIVE_AUDIO_CHAR_UUID { setupNotification(for: peripheral, characteristic: characteristic) } } public func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor descriptor: CBDescriptor, error: Error?) { if let error = error { os_log("❌ 描述符读取失败: %{public}@", log: logger, type: .error, error.localizedDescription) return } os_log("📖 描述符值已更新: %{public}@", log: logger, type: .debug, descriptor.uuid.uuidString) } public func peripheral(_ peripheral: CBPeripheral, didWriteValueFor descriptor: CBDescriptor, error: Error?) { if let error = error { os_log("❌ 描述符写入失败: %{public}@", log: logger, type: .error, error.localizedDescription) return } if descriptor.uuid == BleConst.CLIENT_CHAR_CONFIG_UUID { os_log("✅ CLIENT_CHAR_CONFIG_UUID描述符写入成功: %{public}@", log: logger, type: .info, descriptor.characteristic?.uuid.uuidString ?? "未知特征") } else { os_log("✅ 描述符写入成功: %{public}@", log: logger, type: .debug, descriptor.uuid.uuidString) } } }