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.

456 lines
17 KiB

import Foundation
import CoreBluetooth
import os.log
@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 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 delegate: Callback?
private var connectionState = BleConst.STATE_DISCONNECTED
private let logger = OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleService")
// MARK: - 初始化
private override init() {
super.init()
// 初始化中央管理器,提供恢复标识以支持后台恢复
let options: [String: Any] = [
CBCentralManagerOptionRestoreIdentifierKey: "BLEAssistantCentral",
CBCentralManagerOptionShowPowerAlertKey: true
]
centralManager = CBCentralManager(delegate: self, queue: nil, options: options)
}
// MARK: - 公共方法
/// 设置回调代理
func setDelegate(_ delegate: Callback) {
self.delegate = delegate
}
/// 获取中央管理器
func getCentralManager() -> CBCentralManager {
return centralManager
}
/// 开始扫描BLE设备
func startScan() {
guard centralManager.state == .poweredOn else {
os_log("蓝牙未打开", log: logger, type: .error)
return
}
if isScanning {
stopScan()
}
isScanning = true
// 扫描特定服务的设备
let serviceUUIDs = [BleConst.PRIMARY_SERVICE_UUID, BleConst.AUDIO_SERVICE_UUID]
centralManager.scanForPeripherals(withServices: serviceUUIDs, options: [
CBCentralManagerScanOptionAllowDuplicatesKey: false
])
os_log("开始扫描BLE设备", log: logger, type: .info)
}
/// 停止扫描
func stopScan() {
if isScanning {
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 {
os_log("已连接或正在连接中", log: logger, type: .info)
return
}
updateConnectionState(BleConst.STATE_CONNECTING)
peripheralDevice = peripheral
peripheralDevice?.delegate = self
centralManager.connect(peripheral, options: [
CBConnectPeripheralOptionNotifyOnConnectionKey: true,
CBConnectPeripheralOptionNotifyOnDisconnectionKey: true,
CBConnectPeripheralOptionNotifyOnNotificationKey: true
])
}
/// 断开当前连接
func disconnect() {
guard let peripheral = peripheralDevice,
connectionState == BleConst.STATE_CONNECTED else {
os_log("未连接设备", log: logger, type: .info)
return
}
updateConnectionState(BleConst.STATE_DISCONNECTING)
centralManager.cancelPeripheralConnection(peripheral)
}
/// 检查是否已连接
func isConnected() -> Bool {
return connectionState == BleConst.STATE_CONNECTED
}
// MARK: - 获取设备信息
/// 获取设备版本号
func getDeviceVersion() -> Bool {
return sendCommand(BleConst.CMD_GET_VERSION)
}
/// 获取产品ID
func getProductId() -> Bool {
return sendCommand(BleConst.CMD_GET_PRODUCT_ID)
}
/// 获取电池信息
func getBatteryInfo() -> Bool {
return sendCommand(BleConst.CMD_GET_BATTERY_INFO)
}
// MARK: - 编解码控制
/// 打开编码器
func openEncoder() -> Bool {
return sendCommandWithParam(BleConst.CMD_CONTROL_CODEC, param: BleConst.CODEC_CONTROL_ENCODE_ON)
}
/// 打开解码器
func openDecoder() -> Bool {
return sendCommandWithParam(BleConst.CMD_CONTROL_CODEC, param: BleConst.CODEC_CONTROL_DECODE_ON)
}
/// 打开A2DP解码器
func openA2DPDecoder() -> Bool {
return sendCommandWithParam(BleConst.CMD_CONTROL_CODEC, param: BleConst.CODEC_CONTROL_A2DP_PLAY)
}
/// 关闭编解码器
func closeCodec() -> Bool {
return sendCommandWithParam(BleConst.CMD_CONTROL_CODEC, param: BleConst.CODEC_CONTROL_CLOSE)
}
/// 播放A2DP
func playA2DP() -> Bool {
return sendCommand(BleConst.CMD_PLAY_A2DP)
}
// MARK: - 私有方法
/// 更新连接状态并通知代理
private func updateConnectionState(_ state: Int) {
connectionState = state
delegate?.onConnectionStateChanged(state: state)
}
/// 向设备发送命令(无参数)
private func sendCommand(_ cmd: UInt8) -> Bool {
guard let characteristic = writeCharacteristic,
connectionState == BleConst.STATE_CONNECTED else {
os_log("设备未连接或写特征未找到", log: logger, type: .error)
return false
}
// 构造帧: 帧头 + 命令ID + 长度(0) + 校验和
let frameData = Data([BleConst.APP_REQUEST_HEADER, cmd, 0x00, BleConst.APP_REQUEST_HEADER ^ cmd ^ 0x00])
peripheralDevice?.writeValue(frameData, for: characteristic, type: .withResponse)
return true
}
/// 向设备发送带参数的命令
private func sendCommandWithParam(_ cmd: UInt8, param: UInt8) -> Bool {
guard let characteristic = writeCharacteristic,
connectionState == BleConst.STATE_CONNECTED else {
os_log("设备未连接或写特征未找到", log: logger, type: .error)
return false
}
// 构造帧: 帧头 + 命令ID + 长度(1) + 参数 + 校验和
let frameData = Data([BleConst.APP_REQUEST_HEADER, cmd, 0x01, param, BleConst.APP_REQUEST_HEADER ^ cmd ^ 0x01 ^ param])
peripheralDevice?.writeValue(frameData, for: characteristic, type: .withResponse)
return true
}
/// 处理接收到的响应数据
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
}
// 根据命令类型处理设备响应
switch cmd {
case BleConst.CMD_GET_VERSION:
if length > 0 && data.count >= 4 + Int(length) {
let versionData = data.subdata(in: 3..<(3 + Int(length)))
if let versionStr = String(data: versionData, encoding: .utf8) {
delegate?.onDeviceInfoReceived(infoType: Int(BleConst.CMD_GET_VERSION),
infoData: ["version": versionStr])
}
}
case BleConst.CMD_GET_PRODUCT_ID:
if length >= 2 && data.count >= 5 {
let productId = UInt16(data[3]) << 8 | UInt16(data[4])
delegate?.onDeviceInfoReceived(infoType: Int(BleConst.CMD_GET_PRODUCT_ID),
infoData: ["productId": productId])
}
case BleConst.CMD_GET_BATTERY_INFO:
if length >= 1 && data.count >= 4 {
let batteryLevel = Int(data[3])
delegate?.onDeviceInfoReceived(infoType: Int(BleConst.CMD_GET_BATTERY_INFO),
infoData: ["batteryLevel": batteryLevel])
}
default:
os_log("未知的响应命令: %{public}d", log: logger, type: .debug, cmd)
}
}
/// 处理设备主动上报的通知数据
private func handleNotificationData(_ data: Data) {
guard data.count >= 4 else { return }
let cmd = data[1]
let length = data[2]
switch cmd {
case BleConst.CMD_WAKE_UP:
// 处理唤醒指令
os_log("收到设备唤醒指令", log: logger, type: .info)
// 可以在这里添加唤醒回调
default:
os_log("未知的通知命令: %{public}d", log: logger, type: .debug, cmd)
}
}
}
// MARK: - CBCentralManagerDelegate
@available(iOS 13.0, *)
extension BleService: CBCentralManagerDelegate {
func centralManagerDidUpdateState(_ central: CBCentralManager) {
switch central.state {
case .poweredOn:
os_log("蓝牙已开启", log: logger, type: .info)
// 蓝牙已开启,可以开始扫描
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)
case .unsupported:
os_log("设备不支持蓝牙", log: logger, type: .error)
case .unknown:
os_log("蓝牙状态未知", log: logger, type: .info)
@unknown default:
os_log("蓝牙状态未定义", log: logger, type: .info)
}
}
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 }
}
// 报告给回调
delegate?.onScanResult(devices: [deviceInfo])
}
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
os_log("已连接到设备: %{public}@", log: logger, type: .info, peripheral.name ?? "Unknown")
updateConnectionState(BleConst.STATE_CONNECTED)
// 查找服务
peripheral.discoverServices([BleConst.PRIMARY_SERVICE_UUID, BleConst.AUDIO_SERVICE_UUID])
}
func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
os_log("连接失败: %{public}@", log: logger, type: .error, error?.localizedDescription ?? "Unknown error")
updateConnectionState(BleConst.STATE_ERROR)
}
func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
os_log("设备断开连接: %{public}@", log: logger, type: .info, error?.localizedDescription ?? "Normal disconnection")
updateConnectionState(BleConst.STATE_DISCONNECTED)
}
func centralManager(_ central: CBCentralManager, willRestoreState dict: [String : Any]) {
os_log("恢复先前的连接状态", log: logger, type: .info)
// 从恢复状态中获取之前连接的外设
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 {
os_log("服务发现错误: %{public}@", log: logger, type: .error, error.localizedDescription)
return
}
guard let services = peripheral.services else { return }
for service in services {
os_log("发现服务: %{public}@", log: logger, type: .debug, service.uuid.uuidString)
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 {
os_log("特征发现错误: %{public}@", log: logger, type: .error, error.localizedDescription)
return
}
guard let characteristics = service.characteristics else { return }
for characteristic in characteristics {
os_log("发现特征: %{public}@", log: logger, type: .debug, characteristic.uuid.uuidString)
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 {
os_log("接收数据错误: %{public}@", log: logger, type: .error, error.localizedDescription)
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 {
// 处理音频数据
delegate?.onAudioDataReceived(data: data)
}
}
func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) {
if let error = error {
os_log("写入数据错误: %{public}@", log: logger, type: .error, error.localizedDescription)
}
}
func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) {
if let error = error {
os_log("订阅通知错误: %{public}@", log: logger, type: .error, error.localizedDescription)
return
}
os_log("特征 %{public}@ 的通知状态更新为 %{public}d", log: logger, type: .debug,
characteristic.uuid.uuidString, characteristic.isNotifying ? 1 : 0)
}
}