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.
945 lines
35 KiB
945 lines
35 KiB
import Foundation
|
|
import CoreBluetooth
|
|
import os.log
|
|
import SpeexKit // 使用杰理的SpeexKit库替代OpusSwift
|
|
import AVFoundation
|
|
|
|
@available(iOS 13.0, *)
|
|
class BleService: NSObject {
|
|
// MARK: - 单例实现
|
|
static let shared = BleService()
|
|
|
|
// MARK: - 回调代理协议
|
|
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 let logger = OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleService")
|
|
|
|
// MARK: - Opus音频处理器
|
|
private var opusProcessor: OpusAudioProcessor?
|
|
|
|
// 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 {
|
|
return
|
|
}
|
|
|
|
let options: [String: Any] = [
|
|
CBCentralManagerOptionRestoreIdentifierKey: "BLEAssistantCentral",
|
|
CBCentralManagerOptionShowPowerAlertKey: true
|
|
]
|
|
|
|
centralManager = CBCentralManager(delegate: self, queue: nil, options: options)
|
|
}
|
|
|
|
// 初始化Opus处理器
|
|
private func initializeOpusProcessor() {
|
|
opusProcessor = OpusAudioProcessor()
|
|
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解码流启动成功,将在整个BLE生命周期内保持活跃", log: logger, type: .info)
|
|
} else {
|
|
os_log("Opus解码流启动失败", log: logger, type: .error)
|
|
}
|
|
}
|
|
|
|
// MARK: - 回调管理
|
|
|
|
/// 添加回调代理
|
|
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
|
|
}
|
|
|
|
/// 移除回调代理
|
|
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
|
|
}
|
|
|
|
/// 清除所有回调
|
|
func clearCallbacks() {
|
|
callbacks.removeAll()
|
|
os_log("已清除所有BLE回调", log: logger, type: .debug)
|
|
}
|
|
|
|
// MARK: - 公共方法
|
|
|
|
/// 获取中央管理器,如果未初始化则先初始化
|
|
func getCentralManager() -> CBCentralManager {
|
|
if centralManager == nil {
|
|
initializeCentralManager()
|
|
}
|
|
return centralManager!
|
|
}
|
|
|
|
/// 开始扫描BLE设备
|
|
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)
|
|
}
|
|
|
|
/// 停止扫描
|
|
func stopScan() {
|
|
if isScanning && centralManager != nil {
|
|
centralManager?.stopScan()
|
|
isScanning = false
|
|
os_log("停止BLE扫描", log: logger, type: .info)
|
|
}
|
|
}
|
|
|
|
/// 连接到指定设备
|
|
func connect(to peripheral: CBPeripheral) {
|
|
if connectionState == BleConst.STATE_CONNECTED || connectionState == BleConst.STATE_CONNECTING {
|
|
return
|
|
}
|
|
|
|
if centralManager == nil {
|
|
initializeCentralManager()
|
|
}
|
|
|
|
guard let manager = centralManager else {
|
|
return
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
/// 断开当前连接
|
|
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)
|
|
}
|
|
}
|
|
|
|
/// 检查是否已连接
|
|
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])
|
|
return sendCommand(BleConst.CMD_CONTROL_CODEC, data: paramData)
|
|
}
|
|
|
|
/// 打开解码器
|
|
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) {
|
|
connectionState = 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)
|
|
|
|
// 发送数据
|
|
peripheralDevice?.writeValue(cmdPacket, for: characteristic, type: .withResponse)
|
|
return true
|
|
} catch {
|
|
os_log("发送命令异常: %@", log: logger, type: .error, error.localizedDescription)
|
|
return false
|
|
}
|
|
}
|
|
|
|
/// 计算CRC8/MAXIM校验值
|
|
private func calculateCRC8(data: Data) -> UInt8 {
|
|
var crc: UInt8 = 0
|
|
let polynomial: UInt8 = 0x31
|
|
|
|
for byte in data {
|
|
crc ^= byte
|
|
for _ in 0..<8 {
|
|
if (crc & 0x80) != 0 {
|
|
crc = ((crc << 1) ^ polynomial)
|
|
} else {
|
|
crc = (crc << 1)
|
|
}
|
|
}
|
|
}
|
|
|
|
return crc
|
|
}
|
|
|
|
/// 处理收到的音频数据
|
|
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)
|
|
// 接收到唤醒信号,打开编码器 (设备侧)
|
|
openEncoder()
|
|
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: - 资源清理
|
|
|
|
/// 清理所有资源
|
|
func cleanup() {
|
|
stopScan()
|
|
disconnect()
|
|
|
|
// 停止Opus解码
|
|
opusProcessor?.cleanup()
|
|
opusProcessor = nil
|
|
|
|
os_log("BleService资源已清理", log: logger, type: .debug)
|
|
}
|
|
|
|
// MARK: - AccessorySetupKit
|
|
|
|
/// 在接收到AccessorySetupKit的accessoryAdded事件后调用
|
|
func connectToAccessory(uuid: UUID) {
|
|
#if canImport(AccessorySetupKit) && swift(>=5.9)
|
|
if #available(iOS 18.0, *) {
|
|
// 确保CBCentralManager已初始化
|
|
if centralManager == nil {
|
|
initializeCentralManager()
|
|
}
|
|
|
|
guard let manager = centralManager, manager.state == .poweredOn else {
|
|
return
|
|
}
|
|
|
|
// 使用retrievePeripherals而不是扫描
|
|
let peripherals = manager.retrievePeripherals(withIdentifiers: [uuid])
|
|
|
|
if let peripheral = peripherals.first {
|
|
connect(to: peripheral)
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
|
|
// MARK: - OpusAudioProcessor.AudioDataCallback
|
|
@available(iOS 13.0, *)
|
|
extension BleService: OpusAudioProcessor.AudioDataCallback {
|
|
/// 接收到解码后的PCM音频数据
|
|
func onAudioDataReceived(data: Data) {
|
|
notifyAudioDataReceived(data: data)
|
|
}
|
|
}
|
|
|
|
// MARK: - CBCentralManagerDelegate
|
|
@available(iOS 13.0, *)
|
|
extension BleService: CBCentralManagerDelegate {
|
|
func centralManagerDidUpdateState(_ central: CBCentralManager) {
|
|
switch central.state {
|
|
case .poweredOn:
|
|
// 蓝牙已开启,可以开始扫描
|
|
break
|
|
|
|
case .poweredOff:
|
|
updateConnectionState(BleConst.STATE_DISCONNECTED)
|
|
|
|
case .resetting, .unauthorized, .unsupported, .unknown:
|
|
// 其他状态不处理
|
|
break
|
|
|
|
@unknown default:
|
|
break
|
|
}
|
|
}
|
|
|
|
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])
|
|
}
|
|
|
|
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
|
|
updateConnectionState(BleConst.STATE_CONNECTED)
|
|
|
|
// 查找服务
|
|
peripheral.discoverServices([BleConst.PRIMARY_SERVICE_UUID, BleConst.AUDIO_SERVICE_UUID])
|
|
|
|
// 确保Opus解码器处于活跃状态
|
|
if opusProcessor?.isDecoding() != true {
|
|
startOpusDecoding()
|
|
}
|
|
}
|
|
|
|
func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
|
|
updateConnectionState(BleConst.STATE_ERROR)
|
|
}
|
|
|
|
func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
|
|
updateConnectionState(BleConst.STATE_DISCONNECTED)
|
|
}
|
|
|
|
func centralManager(_ central: CBCentralManager, willRestoreState dict: [String : Any]) {
|
|
// 从恢复状态中获取之前连接的外设
|
|
if let peripherals = dict[CBCentralManagerRestoredStatePeripheralsKey] as? [CBPeripheral],
|
|
let peripheral = peripherals.first {
|
|
peripheralDevice = peripheral
|
|
peripheralDevice?.delegate = self
|
|
|
|
// 如果之前已连接,更新状态
|
|
if peripheral.state == .connected {
|
|
updateConnectionState(BleConst.STATE_CONNECTED)
|
|
// 重新发现服务
|
|
peripheral.discoverServices([BleConst.PRIMARY_SERVICE_UUID, BleConst.AUDIO_SERVICE_UUID])
|
|
} else {
|
|
// 如果之前未连接,尝试重新连接
|
|
central.connect(peripheral, options: nil)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - CBPeripheralDelegate
|
|
@available(iOS 13.0, *)
|
|
extension BleService: CBPeripheralDelegate {
|
|
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
|
|
if let error = error {
|
|
return
|
|
}
|
|
|
|
guard let services = peripheral.services else { return }
|
|
|
|
for service in services {
|
|
if service.uuid == BleConst.PRIMARY_SERVICE_UUID {
|
|
// 主服务,发现写入和通知特征
|
|
peripheral.discoverCharacteristics([BleConst.WRITE_CHAR_UUID, BleConst.NOTIFY_CHAR_UUID], for: service)
|
|
} else if service.uuid == BleConst.AUDIO_SERVICE_UUID {
|
|
// 音频服务,发现音频特征
|
|
peripheral.discoverCharacteristics([BleConst.RECEIVE_AUDIO_CHAR_UUID], for: service)
|
|
}
|
|
}
|
|
}
|
|
|
|
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
|
|
if let error = error {
|
|
return
|
|
}
|
|
|
|
guard let characteristics = service.characteristics else { return }
|
|
|
|
for characteristic in characteristics {
|
|
if characteristic.uuid == BleConst.WRITE_CHAR_UUID {
|
|
writeCharacteristic = characteristic
|
|
} else if characteristic.uuid == BleConst.NOTIFY_CHAR_UUID {
|
|
notifyCharacteristic = characteristic
|
|
// 订阅通知
|
|
peripheral.setNotifyValue(true, for: characteristic)
|
|
} else if characteristic.uuid == BleConst.RECEIVE_AUDIO_CHAR_UUID {
|
|
audioCharacteristic = characteristic
|
|
// 订阅音频通知
|
|
peripheral.setNotifyValue(true, for: characteristic)
|
|
}
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) {
|
|
// 写入完成回调,不需要特殊处理
|
|
}
|
|
|
|
func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) {
|
|
// 通知状态更新回调,不需要特殊处理
|
|
}
|
|
}
|