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.
537 lines
20 KiB
537 lines
20 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()
|
|
|
|
// iOS 18+ 下不在初始化阶段创建 CBCentralManager
|
|
#if canImport(AccessorySetupKit) && swift(>=5.9)
|
|
if #available(iOS 18.0, *) {
|
|
os_log("iOS 18+: 延迟初始化CBCentralManager,等待AccessorySetupKit完成", log: logger, type: .info)
|
|
} else {
|
|
initializeCentralManager()
|
|
}
|
|
#else
|
|
initializeCentralManager()
|
|
#endif
|
|
}
|
|
|
|
// 延迟初始化中央管理器
|
|
private func initializeCentralManager() {
|
|
// 防止重复初始化
|
|
if centralManager != nil {
|
|
return
|
|
}
|
|
|
|
os_log("正在初始化CBCentralManager", log: logger, type: .info)
|
|
|
|
// 初始化中央管理器,提供恢复标识以支持后台恢复
|
|
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 {
|
|
if centralManager == nil {
|
|
initializeCentralManager()
|
|
}
|
|
return centralManager!
|
|
}
|
|
|
|
/// 开始扫描BLE设备
|
|
func startScan() {
|
|
// iOS 18+ 需要处理AccessorySetupKit兼容性
|
|
#if canImport(AccessorySetupKit) && swift(>=5.9)
|
|
if #available(iOS 18.0, *) {
|
|
os_log("iOS 18+环境: 不进行传统扫描,请使用AccessorySetupKit配对", log: logger, type: .info)
|
|
return
|
|
}
|
|
#endif
|
|
|
|
// iOS 17及以下版本继续使用传统扫描
|
|
if centralManager == nil {
|
|
initializeCentralManager()
|
|
}
|
|
|
|
guard let manager = centralManager, manager.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]
|
|
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 {
|
|
os_log("已连接或正在连接中", log: logger, type: .info)
|
|
return
|
|
}
|
|
|
|
// 确保中央管理器已初始化
|
|
if centralManager == nil {
|
|
initializeCentralManager()
|
|
}
|
|
|
|
guard let manager = centralManager else {
|
|
os_log("蓝牙管理器未初始化", log: logger, type: .error)
|
|
return
|
|
}
|
|
|
|
updateConnectionState(BleConst.STATE_CONNECTING)
|
|
|
|
peripheralDevice = peripheral
|
|
peripheralDevice?.delegate = self
|
|
manager.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
|
|
}
|
|
|
|
if centralManager != nil {
|
|
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: - AccessorySetupKit
|
|
|
|
/// 在接收到AccessorySetupKit的accessoryAdded事件后调用
|
|
func connectToAccessory(uuid: UUID) {
|
|
#if canImport(AccessorySetupKit) && swift(>=5.9)
|
|
if #available(iOS 18.0, *) {
|
|
// 确保CBCentralManager已初始化
|
|
if centralManager == nil {
|
|
initializeCentralManager()
|
|
}
|
|
|
|
os_log("通过AccessorySetupKit连接设备: %{public}@", log: logger, type: .info, uuid.uuidString)
|
|
|
|
// 检查centralManager状态
|
|
guard let manager = centralManager, manager.state == .poweredOn else {
|
|
os_log("CBCentralManager尚未准备好或状态不是poweredOn: %d", log: logger, type: .error, centralManager?.state.rawValue ?? -1)
|
|
return
|
|
}
|
|
|
|
// 使用retrievePeripherals而不是扫描
|
|
let peripherals = manager.retrievePeripherals(withIdentifiers: [uuid])
|
|
|
|
if let peripheral = peripherals.first {
|
|
os_log("找到AccessorySetupKit配对的设备,尝试连接", log: logger, type: .info)
|
|
connect(to: peripheral)
|
|
} else {
|
|
os_log("未找到对应UUID的设备: %{public}@", log: logger, type: .error, uuid.uuidString)
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
}
|