import CoreBluetooth import Foundation /// Duooomi BLE SDK - 纯原生 iOS 蓝牙 SDK /// /// 提供设备扫描、连接、命令交互、文件传输等原子蓝牙操作。 /// 业务编排(双层绑定 / OTA 检查 / 升级对账上报)由 SDK 主类内部完成,集成方仅与公开 API 交互。 /// /// 文件分层(按职责拆分;类即门面,对外 API 形态不变): /// - `DuooomiBleSDK.swift` 状态属性 / init / 扫描 / RPC helpers / delegate 实现 /// - `DuooomiBleSDK+Connection.swift` 连接 / 断开 /// - `DuooomiBleSDK+Commands.swift` 纯协议层 RPC(getDeviceInfo / getVersion / setPlayMode / deleteFile / prepareTransfer) /// - `DuooomiBleSDK+Binding.swift` 双层绑定编排(硬绑 → getVersion → OTA 检查/对账 → 软绑,失败回滚) /// - `DuooomiBleSDK+Firmware.swift` OTA 检查 / 安装 / 升级对账上报 /// - `DuooomiBleSDK+Transfer.swift` 文件传输(mime 校验 / ANI 转换 / 远程 size 探测) /// /// 注:`scan()` / `stopScan()` 留在主类不放 extension:name 与 `Combine.Publisher.scan(_:_:)` 冲突, /// 当 SDK 实例被 `@Published` 包装时 extension 上的 `scan` 会被 Publisher.scan 抢占名字解析。 public final class DuooomiBleSDK: NSObject { // MARK: - Observable State public weak var delegate: DuooomiBleSDKDelegate? public internal(set) var btState: ConnectionState = .idle { didSet { guard btState != oldValue else { return } notifyMain { self.delegate?.sdk(self, didChangeState: self.btState) } } } public internal(set) var connectedDevice: DiscoveredDevice? = nil { didSet { notifyMain { self.delegate?.sdk(self, didConnectDevice: self.connectedDevice) } } } public internal(set) var deviceInfo: DeviceInfo? = nil { didSet { notifyMain { self.delegate?.sdk(self, didUpdateDeviceInfo: self.deviceInfo) } } } public internal(set) var version: String = "" { didSet { notifyMain { self.delegate?.sdk(self, didUpdateVersion: self.version) } } } /// 设备返回的 `log` 字段(任意 JSON 对象)。`getVersion` 成功时写入,断连/解绑时清空。 /// `fetchLatestFirmware` 软绑后透传给 shipment-sn `update-metadata`,集成方一般无需直接读。 public internal(set) var versionLog: [String: Any]? public internal(set) var isActivated: Bool = false { didSet { guard isActivated != oldValue else { return } notifyMain { self.delegate?.sdk(self, didChangeActivation: self.isActivated) } } } /// 当前已绑定设备的 SN(绑定成功后写入,解绑后清空) public internal(set) var sn: String = "" public internal(set) var transferProgress: Int = 0 { didSet { guard transferProgress != oldValue else { return } notifyMain { self.delegate?.sdk(self, didUpdateProgress: self.transferProgress) } } } public internal(set) var error: String? = nil { didSet { notifyMain { self.delegate?.sdk(self, didEncounterError: self.error) } } } public internal(set) var discoveredDevices: [DiscoveredDevice] = [] { didSet { notifyMain { self.delegate?.sdk(self, didUpdateDevices: self.discoveredDevices) } } } // MARK: - Configuration public let config: DuooomiBleConfig // MARK: - Internal Services (extension 文件按职责调用) let bleClient: BleClient let protocolService: BleProtocolService let deviceInfoService: DeviceInfoService let fileTransferService: FileTransferService let aniConverter: AniConverter let timeProvider: BeijingTimeProvider let shipmentService: ShipmentSnDeviceService let firmwareUpgradeService: FirmwareUpgradeService let upgradeRecordStore: UpgradeRecordStore /// 服务端最新一次 `fetchLatestFirmware` 结果 public internal(set) var firmwareUpgrade: FirmwareUpgradeCheckResult? = nil /// 当前 OTA 升级记录快照(按 triggeredAt 倒序展示用;持久化于 UserDefaults) public var upgradeRecords: [UpgradeRecord] { upgradeRecordStore.all } // MARK: - Request-Response (extension 通过 sendAndWait / sendAsyncAndWait 共享通道) var pendingCallbacks: [String: (Result) -> Void] = [:] var pendingTimeouts: [String: DispatchWorkItem] = [:] let callbackQueue = DispatchQueue(label: "com.duooomi.ble.callback") // MARK: - Scan Batching var scanDelegate: ScanBleClientDelegate? var allDiscoveredDevices: [DiscoveredDevice] = [] var pendingDevices: [DiscoveredDevice] = [] var flushWorkItem: DispatchWorkItem? // MARK: - Init public init(config: DuooomiBleConfig) { self.config = config bleClient = BleClient() protocolService = BleProtocolService(client: bleClient, frameIntervalMs: config.frameIntervalMs) timeProvider = BeijingTimeProvider() deviceInfoService = DeviceInfoService(protocolService: protocolService, timeProvider: timeProvider) fileTransferService = FileTransferService(protocolService: protocolService) aniConverter = AniConverter(config: config) shipmentService = ShipmentSnDeviceService(config: config) firmwareUpgradeService = FirmwareUpgradeService(config: config) upgradeRecordStore = UpgradeRecordStore() super.init() protocolService.delegate = self BleLog.i("SDK initialized (apiHost=\(config.apiHost), brand=\(config.brand))", "SDK") setupDisconnectHandler() // UpgradeRecord 变化(含对账上报结果)→ delegate 通知 upgradeRecordStore.onChange = { [weak self] records in guard let self = self else { return } self.delegate?.sdk(self, didUpdateUpgradeRecords: records) } } // MARK: - Scanning /// 开始扫描 BLE 设备。结果 throttle 500ms 后通过 delegate `didUpdateDevices` 回调, /// 数据同步写入 `sdk.discoveredDevices`。`btState` 切到 `.scanning`。 /// /// 已经在扫描则会先 stopScan 再重启。已连接设备时 stopScan 后 btState 回 `.connected`,否则回 `.idle`。 /// 扫描结果按 `config.scanNamePrefix`(如 "Duooomi-")做名字前缀过滤(必填)。 public func scan() { stopScan() btState = .scanning discoveredDevices = [] allDiscoveredDevices = [] pendingDevices = [] BleLog.i("Start scanning...", "Scan") let proxy = ScanBleClientDelegate(sdk: self, original: protocolService) scanDelegate = proxy bleClient.delegate = proxy bleClient.scan() } /// 停止 BLE 扫描,flush 待回调的设备列表,恢复 `bleClient.delegate` 给协议服务。 /// 已连接设备时 `btState` 回 `.connected`,否则回 `.idle`。 public func stopScan() { bleClient.stopScan() flushDevices() scanDelegate = nil bleClient.delegate = protocolService if connectedDevice != nil { btState = .connected } else if btState == .scanning { btState = .idle } BleLog.i("Stop scanning (state=\(btState.rawValue))", "Scan") } // MARK: - Disconnect func setupDisconnectHandler() { bleClient.onDisconnected = { [weak self] in guard let self = self else { return } BleLog.w("Peripheral disconnected; resetting state", "SDK") self.connectedDevice = nil self.deviceInfo = nil self.version = "" self.versionLog = nil self.isActivated = false self.btState = .disconnected self.protocolService.stopListening() self.cancelAllPending(error: DuooomiBleError.notConnected) } } // MARK: - Request-Response Pattern func sendAndWait( commandType: CommandType, timeout: TimeInterval = 10, completion: @escaping (Result) -> Void, send: @escaping () throws -> Void ) { let opId = "\(commandType.rawValue)" sendAndWait(opId: opId, commandType: commandType, timeout: timeout, completion: completion, send: send) } /// 异步 send 版本:用于需要先取 BeijingTime 再发命令的 bind/unbind/setPlayMode。 /// 整体 timeout 仍为 10s,覆盖时间获取 + BLE 命令发送 + 设备响应。 func sendAsyncAndWait( commandType: CommandType, timeout: TimeInterval = 10, completion: @escaping (Result) -> Void, send: @escaping (@escaping (Result) -> Void) -> Void ) { let opId = "\(commandType.rawValue)" BleLog.d("Register opId=\(opId), cmd=\(commandType) (async send)", "RPC") callbackQueue.sync { self.pendingCallbacks[opId] = { result in DispatchQueue.main.async { completion(result) } } } let timeoutWork = DispatchWorkItem { [weak self] in guard let self = self else { return } self.callbackQueue.sync { if let cb = self.pendingCallbacks.removeValue(forKey: opId) { self.pendingTimeouts.removeValue(forKey: opId) cb(.failure(DuooomiBleError.timeout(command: commandType))) } } } callbackQueue.sync { pendingTimeouts[opId] = timeoutWork } DispatchQueue.global().asyncAfter(deadline: .now() + timeout, execute: timeoutWork) send { [weak self] result in guard let self = self else { return } if case .failure(let error) = result { self.callbackQueue.sync { self.pendingCallbacks.removeValue(forKey: opId) self.pendingTimeouts.removeValue(forKey: opId)?.cancel() } BleLog.e("Async send failed for opId=\(opId): \(error.localizedDescription)", "RPC") DispatchQueue.main.async { completion(.failure(error)) } } else { BleLog.d("Async command sent for opId=\(opId)", "RPC") } } } func sendAndWait( opId: String, commandType: CommandType, timeout: TimeInterval = 10, completion: @escaping (Result) -> Void, send: @escaping () throws -> Void ) { BleLog.d("Register opId=\(opId), cmd=\(commandType)", "RPC") callbackQueue.sync { self.pendingCallbacks[opId] = { result in DispatchQueue.main.async { completion(result) } } } let timeoutWork = DispatchWorkItem { [weak self] in guard let self = self else { return } self.callbackQueue.sync { if let cb = self.pendingCallbacks.removeValue(forKey: opId) { self.pendingTimeouts.removeValue(forKey: opId) cb(.failure(DuooomiBleError.timeout(command: commandType))) } } } callbackQueue.sync { pendingTimeouts[opId] = timeoutWork } DispatchQueue.global().asyncAfter(deadline: .now() + timeout, execute: timeoutWork) do { try send() BleLog.d("Command sent for opId=\(opId)", "RPC") } catch { callbackQueue.sync { self.pendingCallbacks.removeValue(forKey: opId) self.pendingTimeouts.removeValue(forKey: opId)?.cancel() } BleLog.e("Send failed for opId=\(opId): \(error.localizedDescription)", "RPC") DispatchQueue.main.async { completion(.failure(error)) } } } // MARK: - Helpers func ensureConnected(completion: @escaping (Result) -> Void) -> Bool { guard connectedDevice != nil else { BleLog.w("Operation requires connection", "SDK") completion(.failure(DuooomiBleError.notConnected)) return false } return true } func decodeResponse(_ type: T.Type, from data: Data) throws -> T { let cleaned = data.filter { $0 != 0 } do { return try JSONDecoder().decode(type, from: cleaned) } catch { let raw = String(data: cleaned, encoding: .utf8) ?? "" BleLog.e("JSON decode \(T.self) failed: \(error.localizedDescription)\nRaw: \(raw)", "Decode") throw error } } func cancelAllPending(error: Error) { callbackQueue.sync { let callbacks = self.pendingCallbacks self.pendingCallbacks.removeAll() for (_, timeout) in self.pendingTimeouts { timeout.cancel() } self.pendingTimeouts.removeAll() for (_, cb) in callbacks { cb(.failure(error)) } } } func notifyMain(_ block: @escaping () -> Void) { if Thread.isMainThread { block() } else { DispatchQueue.main.async(execute: block) } } // MARK: - Scan Batching (500ms throttle) func queueDevice(_ device: DiscoveredDevice) { guard !allDiscoveredDevices.contains(where: { $0.id == device.id }) else { return } let name = device.name?.lowercased() ?? "" if !name.hasPrefix(config.scanNamePrefix.lowercased()) { return } allDiscoveredDevices.append(device) pendingDevices.append(device) flushWorkItem?.cancel() let workItem = DispatchWorkItem { [weak self] in self?.flushDevices() } flushWorkItem = workItem DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: workItem) } func flushDevices() { guard !pendingDevices.isEmpty else { return } pendingDevices = [] discoveredDevices = allDiscoveredDevices BleLog.d("Discovered devices flushed: total=\(discoveredDevices.count)", "Scan") } } // MARK: - BleProtocolServiceDelegate (incoming messages) extension DuooomiBleSDK: BleProtocolServiceDelegate { func protocolService(_ service: BleProtocolService, didReceiveMessage commandType: UInt8, data: Data) { BleLog.d("Incoming message: type=\(commandType), size=\(data.count)", "RPC") let opId = "\(commandType)" if commandType == CommandType.prepareTransfer.rawValue { if let resp = try? decodeResponse(PrepareTransferResponse.self, from: data) { let keyedOpId = "\(commandType)_\(resp.key)" var cb: ((Result) -> Void)? callbackQueue.sync { cb = self.pendingCallbacks.removeValue(forKey: keyedOpId) self.pendingTimeouts.removeValue(forKey: keyedOpId)?.cancel() } if let cb = cb { BleLog.d("Resuming keyed opId=\(keyedOpId)", "RPC") cb(.success(data)) return } } } var cb: ((Result) -> Void)? callbackQueue.sync { cb = self.pendingCallbacks.removeValue(forKey: opId) self.pendingTimeouts.removeValue(forKey: opId)?.cancel() } if let cb = cb { BleLog.d("Resuming opId=\(opId)", "RPC") cb(.success(data)) } else { BleLog.w("No pending callback for opId=\(opId)", "RPC") } } } // MARK: - Scan Proxy /// Forwards scan results to SDK while keeping protocol service as data receiver. final class ScanBleClientDelegate: BleClientDelegate { private weak var sdk: DuooomiBleSDK? private let original: BleClientDelegate? init(sdk: DuooomiBleSDK, original: BleClientDelegate?) { self.sdk = sdk self.original = original } func bleClient(_ client: BleClient, didDiscoverDevice device: DiscoveredDevice) { BleLog.d("Discovered: id=\(device.id), name=\(device.name ?? "-"), rssi=\(device.rssi)", "Scan") sdk?.queueDevice(device) } func bleClient(_ client: BleClient, didReceiveData data: Data) { original?.bleClient(client, didReceiveData: data) } }