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

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?) {
// 通知状态更新回调,不需要特殊处理
}
}