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.

1252 lines
50 KiB

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<UUID>()
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()
}
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) {
// 使用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
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()
}
// 恢复待重连的设备
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)
}
}
// 尝试重连队列中的设备
tryReconnectDevices()
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)
}
}
// 原有方法
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)
}
}
}