9 changed files with 689 additions and 24 deletions
@ -0,0 +1,586 @@ |
|||
import Foundation |
|||
import CoreBluetooth |
|||
import UIKit // 添加UIKit导入 |
|||
import os.log |
|||
|
|||
/// 简化的 BLE 扫描管理器 |
|||
/// 只提供扫描和获取设备列表功能 |
|||
@available(iOS 13.0, *) |
|||
public class BleScanManager: NSObject { |
|||
|
|||
// MARK: - 单例 |
|||
public static let shared = BleScanManager() |
|||
|
|||
// MARK: - 数据模型 |
|||
public struct BleDeviceInfo { |
|||
public let identifier: UUID |
|||
public let name: String? |
|||
public let rssi: NSNumber |
|||
public let advertisementData: [String: Any] |
|||
public let discoveryMethod: DiscoveryMethod |
|||
public let lastSeen: Date |
|||
|
|||
public enum DiscoveryMethod { |
|||
case traditional |
|||
case accessorySetupKit |
|||
} |
|||
} |
|||
|
|||
// MARK: - 属性 |
|||
private var centralManager: CBCentralManager! |
|||
private let logger = OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleScanManager") |
|||
|
|||
// 扫描状态 |
|||
private var isScanning = false |
|||
private var scanTimeout: Timer? |
|||
|
|||
// 设备列表 |
|||
private var discoveredDevices: [UUID: BleDeviceInfo] = [:] |
|||
|
|||
|
|||
// MARK: - 初始化 |
|||
private override init() { |
|||
super.init() |
|||
setupCentralManager() |
|||
} |
|||
|
|||
// 在setupCentralManager后添加权限检查 |
|||
private func setupCentralManager() { |
|||
let options: [String: Any] = [ |
|||
CBCentralManagerOptionShowPowerAlertKey: true, |
|||
CBCentralManagerOptionRestoreIdentifierKey: "BleScanManager" |
|||
] |
|||
|
|||
centralManager = CBCentralManager(delegate: self, queue: nil, options: options) |
|||
|
|||
// 添加权限检查和触发逻辑 |
|||
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in |
|||
self?.checkAndTriggerPermission() |
|||
} |
|||
} |
|||
|
|||
private func checkAndTriggerPermission() { |
|||
// 检查蓝牙权限状态,兼容不同iOS版本 |
|||
let authorization: CBManagerAuthorization |
|||
if #available(iOS 13.1, *) { |
|||
authorization = CBManager.authorization |
|||
} else if #available(iOS 13.0, *) { |
|||
// iOS 13.0 使用实例方法,需要确保centralManager已初始化 |
|||
authorization = centralManager?.authorization ?? .notDetermined |
|||
} else { |
|||
// iOS 13.0以下版本没有权限检查API,默认为未确定 |
|||
authorization = .notDetermined |
|||
} |
|||
|
|||
os_log("📊 蓝牙权限状态: %d", log: logger, type: .info, authorization.rawValue) |
|||
os_log("📊 CBCentralManager状态: %d", log: logger, type: .info, centralManager?.state.rawValue ?? -1) |
|||
|
|||
// 只在权限未确定且蓝牙状态为关闭时触发扫描 |
|||
if authorization == .notDetermined && centralManager?.state == .poweredOff { |
|||
os_log("📊 主动触发一次扫描来申请权限", log: logger, type: .info) |
|||
// 主动触发一次扫描来申请权限 |
|||
centralManager?.scanForPeripherals(withServices: nil, options: nil) |
|||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in |
|||
self?.centralManager?.stopScan() |
|||
} |
|||
} |
|||
} |
|||
|
|||
|
|||
|
|||
// MARK: - 公共接口 |
|||
|
|||
/// 开始扫描 BLE 设备 |
|||
/// - Parameter timeout: 扫描超时时间,默认 10 秒 |
|||
/// - Returns: 是否成功开始扫描 |
|||
public func startScan(timeout: TimeInterval = 30.0) -> Bool { |
|||
os_log("🔍 开始 BLE 扫描,超时时间: %.1f 秒", log: logger, type: .info, timeout) |
|||
|
|||
guard !isScanning else { |
|||
os_log("⚠️ 扫描已在进行中", log: logger, type: .info) |
|||
return false |
|||
} |
|||
|
|||
// 清空之前的设备列表 |
|||
discoveredDevices.removeAll() |
|||
|
|||
// iOS 18 兼容性检查 |
|||
if #available(iOS 18.0, *) { |
|||
// iOS 18 使用新的权限检查方式 |
|||
return startScanForIOS18(timeout: timeout) |
|||
} else { |
|||
// iOS 17 及以下使用传统方式 |
|||
return startScanLegacy(timeout: timeout) |
|||
} |
|||
} |
|||
|
|||
@available(iOS 18.0, *) |
|||
private func startScanForIOS18(timeout: TimeInterval) -> Bool { |
|||
// 对于 iOS 18,直接检查 CBCentralManager 状态 |
|||
guard let centralManager = centralManager else { |
|||
os_log("❌ CBCentralManager 未初始化", log: logger, type: .error) |
|||
return false |
|||
} |
|||
|
|||
let currentState = centralManager.state |
|||
os_log("📊 iOS 18 蓝牙状态: %d (%{public}@)", log: logger, type: .info, currentState.rawValue, getBluetoothStateString(currentState)) |
|||
|
|||
switch currentState { |
|||
case .poweredOn: |
|||
return performScanWithPermission(timeout: timeout) |
|||
case .unauthorized: |
|||
os_log("❌ 蓝牙权限被拒绝", log: logger, type: .error) |
|||
showPermissionAlert() |
|||
return false |
|||
case .unknown: |
|||
os_log("⚠️ 蓝牙状态未知,等待状态更新", log: logger, type: .info) |
|||
// 设置监听器等待状态更新 |
|||
waitForBluetoothState(timeout: timeout) |
|||
return true |
|||
default: |
|||
os_log("❌ 蓝牙不可用: %{public}@", log: logger, type: .error, getBluetoothStateString(currentState)) |
|||
return false |
|||
} |
|||
} |
|||
|
|||
private func startScanLegacy(timeout: TimeInterval) -> Bool { |
|||
// 检查并申请蓝牙权限 |
|||
let authorization: CBManagerAuthorization = CBManager.authorization |
|||
os_log("📊 当前蓝牙权限状态: %d", log: logger, type: .info, authorization.rawValue) |
|||
|
|||
switch authorization { |
|||
case .allowedAlways: |
|||
return performScanWithPermission(timeout: timeout) |
|||
case .denied, .restricted: |
|||
os_log("❌ 蓝牙权限被拒绝或受限", log: logger, type: .error) |
|||
showPermissionAlert() |
|||
return false |
|||
case .notDetermined: |
|||
// 权限未确定,需要主动触发蓝牙操作来显示权限弹窗 |
|||
if centralManager == nil { |
|||
setupCentralManager() |
|||
} |
|||
|
|||
// 等待CBCentralManager状态更新后,主动开始扫描来触发权限弹窗 |
|||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in |
|||
guard let self = self else { return } |
|||
if self.centralManager.state == .poweredOn { |
|||
// 开始一个短暂的扫描来触发权限申请 |
|||
self.centralManager.scanForPeripherals(withServices: nil, options: nil) |
|||
// 立即停止扫描 |
|||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { |
|||
self.centralManager.stopScan() |
|||
} |
|||
} else if self.centralManager.state == .unauthorized { |
|||
// 权限被拒绝,显示设置提示 |
|||
self.showPermissionAlert() |
|||
} |
|||
} |
|||
return true |
|||
@unknown default: |
|||
os_log("⚠️ 未知的蓝牙权限状态", log: logger, type: .info) |
|||
return false |
|||
} |
|||
} |
|||
|
|||
private func waitForBluetoothState(timeout: TimeInterval) { |
|||
var checkCount = 0 |
|||
let maxChecks = 20 |
|||
|
|||
let timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] timer in |
|||
guard let self = self else { |
|||
timer.invalidate() |
|||
return |
|||
} |
|||
|
|||
checkCount += 1 |
|||
let currentState = self.centralManager?.state ?? .unknown |
|||
|
|||
os_log("🔍 检查蓝牙状态 (%d/%d): %{public}@", log: self.logger, type: .info, checkCount, maxChecks, self.getBluetoothStateString(currentState)) |
|||
|
|||
switch currentState { |
|||
case .poweredOn: |
|||
timer.invalidate() |
|||
_ = self.performScanWithPermission(timeout: timeout) |
|||
case .unauthorized: |
|||
timer.invalidate() |
|||
self.showPermissionAlert() |
|||
case .poweredOff, .unsupported: |
|||
timer.invalidate() |
|||
os_log("❌ 蓝牙不可用", log: self.logger, type: .error) |
|||
default: |
|||
if checkCount >= maxChecks { |
|||
timer.invalidate() |
|||
os_log("⏰ 等待蓝牙状态超时", log: self.logger, type: .error) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
private func forceTriggerPermissionRequest(timeout: TimeInterval) { |
|||
// 完全清理旧的 centralManager |
|||
centralManager?.delegate = nil |
|||
centralManager = nil |
|||
|
|||
// 强制重新初始化,确保触发权限申请 |
|||
let options: [String: Any] = [ |
|||
CBCentralManagerOptionShowPowerAlertKey: true, |
|||
CBCentralManagerOptionRestoreIdentifierKey: "BleScanManager" |
|||
] |
|||
|
|||
centralManager = CBCentralManager(delegate: self, queue: nil, options: options) |
|||
|
|||
// 设置一个监听器来等待权限状态更新 |
|||
waitForPermissionResponse(timeout: timeout) |
|||
} |
|||
|
|||
private func waitForPermissionResponse(timeout: TimeInterval) { |
|||
// 设置一个定时器来检查权限状态变化 |
|||
var checkCount = 0 |
|||
let maxChecks = 20 // 最多检查20次,每次间隔0.5秒,总共10秒 |
|||
|
|||
let timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] timer in |
|||
guard let self = self else { |
|||
timer.invalidate() |
|||
return |
|||
} |
|||
|
|||
checkCount += 1 |
|||
let currentAuth = CBManager.authorization |
|||
|
|||
os_log("🔍 检查权限状态 (%d/%d): %d", log: self.logger, type: .info, checkCount, maxChecks, currentAuth.rawValue) |
|||
|
|||
switch currentAuth { |
|||
case .allowedAlways: |
|||
timer.invalidate() |
|||
os_log("✅ 蓝牙权限已获得,开始扫描", log: self.logger, type: .info) |
|||
_ = self.performScanWithPermission(timeout: timeout) |
|||
case .denied, .restricted: |
|||
timer.invalidate() |
|||
os_log("❌ 蓝牙权限被拒绝", log: self.logger, type: .error) |
|||
self.showPermissionAlert() |
|||
case .notDetermined: |
|||
if checkCount >= maxChecks { |
|||
timer.invalidate() |
|||
os_log("⏰ 等待权限响应超时", log: self.logger, type: .error) |
|||
self.showPermissionAlert() |
|||
} |
|||
// 继续等待 |
|||
@unknown default: |
|||
timer.invalidate() |
|||
os_log("⚠️ 未知权限状态", log: self.logger, type: .error) |
|||
} |
|||
} |
|||
} |
|||
|
|||
private func reinitializeCentralManager() { |
|||
// 清理旧的 centralManager |
|||
centralManager?.delegate = nil |
|||
centralManager = nil |
|||
|
|||
// 重新初始化 |
|||
setupCentralManager() |
|||
} |
|||
|
|||
/// 检查蓝牙权限状态 |
|||
private func checkBluetoothPermissionStatus() -> (hasPermission: Bool, needsRequest: Bool) { |
|||
// 检查权限状态 |
|||
let authorization: CBManagerAuthorization |
|||
if #available(iOS 13.1, *) { |
|||
authorization = CBManager.authorization |
|||
} else if #available(iOS 13.0, *) { |
|||
if centralManager == nil { |
|||
setupCentralManager() |
|||
} |
|||
authorization = centralManager?.authorization ?? .notDetermined |
|||
} else { |
|||
// iOS 13以下没有权限概念 |
|||
return (true, false) |
|||
} |
|||
|
|||
os_log("📊 蓝牙权限状态: %d, CBCentralManager状态: %d", log: logger, type: .info, authorization.rawValue, centralManager?.state.rawValue ?? -1) |
|||
|
|||
switch authorization { |
|||
case .allowedAlways: |
|||
return (true, false) |
|||
case .denied, .restricted: |
|||
return (false, false) |
|||
case .notDetermined: |
|||
return (false, true) |
|||
@unknown default: |
|||
return (false, true) |
|||
} |
|||
} |
|||
|
|||
private func performScanWithPermission(timeout: TimeInterval) -> Bool { |
|||
// 首先检查权限状态 |
|||
let permissionStatus = checkBluetoothPermissionStatus() |
|||
|
|||
if !permissionStatus.hasPermission { |
|||
if permissionStatus.needsRequest { |
|||
os_log("🔐 需要申请蓝牙权限", log: logger, type: .info) |
|||
_ = requestBluetoothPermission() |
|||
return false |
|||
} else { |
|||
os_log("❌ 蓝牙权限被拒绝", log: logger, type: .error) |
|||
showPermissionAlert() |
|||
return false |
|||
} |
|||
} |
|||
|
|||
// 权限已获得,检查蓝牙硬件状态 |
|||
let currentState = centralManager.state |
|||
os_log("📊 蓝牙硬件状态: %d (%{public}@)", log: logger, type: .info, currentState.rawValue, getBluetoothStateString(currentState)) |
|||
|
|||
switch currentState { |
|||
case .poweredOn: |
|||
// 蓝牙已开启,可以扫描 |
|||
break |
|||
case .unknown: |
|||
os_log("⚠️ 蓝牙状态未知,等待状态更新...", log: logger, type: .info) |
|||
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in |
|||
_ = self?.performScanWithPermission(timeout: timeout) |
|||
} |
|||
return false |
|||
case .poweredOff: |
|||
// 区分是权限问题还是硬件关闭 |
|||
if permissionStatus.hasPermission { |
|||
os_log("❌ 蓝牙硬件已关闭,请在设备设置中开启蓝牙", log: logger, type: .error) |
|||
} else { |
|||
os_log("❌ 可能是权限问题导致的蓝牙状态异常", log: logger, type: .error) |
|||
_ = requestBluetoothPermission() |
|||
} |
|||
return false |
|||
case .unauthorized: |
|||
os_log("❌ 应用未授权使用蓝牙", log: logger, type: .error) |
|||
showPermissionAlert() |
|||
return false |
|||
case .unsupported: |
|||
os_log("❌ 设备不支持蓝牙", log: logger, type: .error) |
|||
return false |
|||
default: |
|||
os_log("⚠️ 其他蓝牙状态: %d", log: logger, type: .error, currentState.rawValue) |
|||
return false |
|||
} |
|||
|
|||
// 开始BLE扫描 |
|||
centralManager.scanForPeripherals(withServices: nil, options: [ |
|||
CBCentralManagerScanOptionAllowDuplicatesKey: true |
|||
]) |
|||
|
|||
isScanning = true |
|||
|
|||
// 设置扫描超时 |
|||
scanTimeout = Timer.scheduledTimer(withTimeInterval: timeout, repeats: false) { [weak self] _ in |
|||
self?.stopScan() |
|||
} |
|||
|
|||
os_log("✅ 开始BLE扫描,超时时间: %.1f 秒", log: logger, type: .info, timeout) |
|||
return true |
|||
} |
|||
|
|||
private func showPermissionAlert() { |
|||
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, |
|||
let window = windowScene.windows.first else { |
|||
os_log("❌ 无法获取当前窗口", log: logger, type: .error) |
|||
return |
|||
} |
|||
|
|||
let alert = UIAlertController( |
|||
title: "需要蓝牙权限", |
|||
message: "请在设置中开启蓝牙权限以使用设备扫描功能", |
|||
preferredStyle: .alert |
|||
) |
|||
|
|||
alert.addAction(UIAlertAction(title: "去设置", style: .default) { _ in |
|||
if let settingsUrl = URL(string: UIApplication.openSettingsURLString) { |
|||
UIApplication.shared.open(settingsUrl) |
|||
} |
|||
}) |
|||
|
|||
alert.addAction(UIAlertAction(title: "取消", style: .cancel)) |
|||
|
|||
window.rootViewController?.present(alert, animated: true) |
|||
} |
|||
|
|||
/// 停止扫描 |
|||
public func stopScan() { |
|||
guard isScanning else { return } |
|||
|
|||
os_log("🛑 停止 BLE 扫描", log: logger, type: .info) |
|||
|
|||
centralManager.stopScan() // 直接使用,不需要可选绑定 |
|||
isScanning = false |
|||
|
|||
scanTimeout?.invalidate() |
|||
scanTimeout = nil |
|||
} |
|||
|
|||
/// 获取已发现的设备列表 |
|||
/// - Returns: 按最后发现时间排序的设备列表 |
|||
public func getDiscoveredDevices() -> [BleDeviceInfo] { |
|||
return Array(discoveredDevices.values).sorted { $0.lastSeen > $1.lastSeen } |
|||
} |
|||
|
|||
/// 清除设备列表 |
|||
public func clearDeviceList() { |
|||
discoveredDevices.removeAll() |
|||
os_log("🗑️ 已清除设备列表", log: logger, type: .info) |
|||
} |
|||
|
|||
/// 检查是否正在扫描 |
|||
public func isScanningActive() -> Bool { |
|||
return isScanning |
|||
} |
|||
|
|||
/// 获取蓝牙状态 |
|||
public func getBluetoothState() -> CBManagerState { |
|||
return centralManager.state // 直接返回,不需要 ?? .unknown |
|||
} |
|||
|
|||
// MARK: - 私有方法 |
|||
|
|||
|
|||
|
|||
private func addOrUpdateDevice(_ deviceInfo: BleDeviceInfo) { |
|||
let identifier = deviceInfo.identifier |
|||
discoveredDevices[identifier] = deviceInfo |
|||
|
|||
os_log("📱 发现设备: %{public}@", log: logger, type: .debug, deviceInfo.name ?? "Unknown") |
|||
} |
|||
|
|||
/// 强制触发蓝牙权限申请 |
|||
public func requestBluetoothPermission() -> Bool { |
|||
os_log("🔐 强制触发蓝牙权限申请", log: logger, type: .info) |
|||
|
|||
// 检查当前权限状态 |
|||
let authorization: CBManagerAuthorization |
|||
if #available(iOS 13.1, *) { |
|||
authorization = CBManager.authorization |
|||
} else if #available(iOS 13.0, *) { |
|||
// 确保centralManager已初始化 |
|||
if centralManager == nil { |
|||
setupCentralManager() |
|||
} |
|||
authorization = centralManager.authorization |
|||
} else { |
|||
// iOS 13以下没有权限概念,直接返回true |
|||
return true |
|||
} |
|||
|
|||
os_log("📊 当前权限状态: %d", log: logger, type: .info, authorization.rawValue) |
|||
|
|||
switch authorization { |
|||
case .notDetermined: |
|||
// 权限未确定,需要主动触发蓝牙操作来显示权限弹窗 |
|||
if centralManager == nil { |
|||
os_log("📊 初始化蓝牙管理对象", log: logger, type: .info, "") |
|||
setupCentralManager() |
|||
} |
|||
|
|||
// 等待CBCentralManager状态更新后,主动开始扫描来触发权限弹窗 |
|||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in |
|||
guard let self = self else { return } |
|||
|
|||
if self.centralManager.state == .poweredOn { |
|||
// 开始一个短暂的扫描来触发权限申请 |
|||
self.centralManager.scanForPeripherals(withServices: nil, options: nil) |
|||
// 立即停止扫描 |
|||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { |
|||
self.centralManager.stopScan() |
|||
} |
|||
} else if self.centralManager.state == .unauthorized { |
|||
// 权限被拒绝,显示设置提示 |
|||
self.showPermissionAlert() |
|||
} |
|||
} |
|||
return true |
|||
case .allowedAlways: |
|||
os_log("✅ 权限已授予", log: logger, type: .info) |
|||
return true |
|||
case .denied, .restricted: |
|||
os_log("❌ 权限被拒绝,显示设置提示", log: logger, type: .error) |
|||
showPermissionAlert() |
|||
return false |
|||
@unknown default: |
|||
os_log("⚠️ 未知权限状态", log: logger, type: .info) |
|||
return false |
|||
} |
|||
} |
|||
} |
|||
|
|||
// MARK: - CBCentralManagerDelegate |
|||
@available(iOS 13.0, *) |
|||
extension BleScanManager: CBCentralManagerDelegate { |
|||
|
|||
public func centralManagerDidUpdateState(_ central: CBCentralManager) { |
|||
let stateString = getBluetoothStateString(central.state) |
|||
os_log("🔧 CBCentralManager 状态更新: %{public}@ (原始值: %d)", log: logger, type: .info, stateString, central.state.rawValue) |
|||
|
|||
switch central.state { |
|||
case .poweredOn: |
|||
os_log("✅ 蓝牙已开启,可以开始扫描", log: logger, type: .info) |
|||
// 如果有待处理的扫描请求,现在可以执行 |
|||
case .poweredOff: |
|||
os_log("❌ 蓝牙已关闭", log: logger, type: .error) |
|||
if isScanning { |
|||
stopScan() |
|||
} |
|||
case .unauthorized: |
|||
os_log("❌ 蓝牙权限被拒绝", log: logger, type: .error) |
|||
if isScanning { |
|||
stopScan() |
|||
} |
|||
case .unsupported: |
|||
os_log("❌ 设备不支持蓝牙", log: logger, type: .error) |
|||
if isScanning { |
|||
stopScan() |
|||
} |
|||
case .unknown: |
|||
os_log("⚠️ 蓝牙状态未知,等待进一步更新", log: logger, type: .error) |
|||
case .resetting: |
|||
os_log("⚠️ 蓝牙正在重置", log: logger, type: .error) |
|||
if isScanning { |
|||
stopScan() |
|||
} |
|||
@unknown default: |
|||
os_log("⚠️ 未知蓝牙状态: %d", log: logger, type: .error, central.state.rawValue) |
|||
} |
|||
} |
|||
|
|||
public func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, |
|||
advertisementData: [String : Any], rssi RSSI: NSNumber) { |
|||
|
|||
let deviceInfo = BleDeviceInfo( |
|||
identifier: peripheral.identifier, |
|||
name: peripheral.name ?? advertisementData[CBAdvertisementDataLocalNameKey] as? String, |
|||
rssi: RSSI, |
|||
advertisementData: advertisementData, |
|||
discoveryMethod: .traditional, |
|||
lastSeen: Date() |
|||
) |
|||
|
|||
addOrUpdateDevice(deviceInfo) |
|||
} |
|||
|
|||
public func centralManager(_ central: CBCentralManager, willRestoreState dict: [String : Any]) { |
|||
os_log("🔄 恢复 CBCentralManager 状态", log: logger, type: .info) |
|||
|
|||
// 恢复扫描状态 |
|||
if dict[CBCentralManagerRestoredStateScanServicesKey] != nil { |
|||
isScanning = true |
|||
} |
|||
} |
|||
|
|||
private func getBluetoothStateString(_ state: CBManagerState) -> String { |
|||
switch state { |
|||
case .unknown: return "未知状态" |
|||
case .resetting: return "重置中" |
|||
case .unsupported: return "设备不支持蓝牙" |
|||
case .unauthorized: return "应用未授权使用蓝牙" |
|||
case .poweredOff: return "蓝牙已关闭" |
|||
case .poweredOn: return "蓝牙已开启" |
|||
@unknown default: return "未知状态" |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue