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
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)
|
|
}
|
|
}
|
|
}
|