import Foundation // MARK: - Firmware OTA (检查 / 烧录 / 对账上报) // // 所有公开方法显式入参,不依赖 SDK 内部状态自动取参。 // 集成方负责从 `sdk.sn` / `sdk.version` / `sdk.firmwareUpgrade` 等只读属性组装参数。 extension DuooomiBleSDK { /// OTA 烧录前置电量门槛(百分比)。低于此值拒绝下发,避免烧录中途断电变砖。 static let minBatteryForUpgrade = 40 /// 拉取服务端最新固件信息。**内部串两步业务编排**(顺序固定): /// /// 1. **对账上报**:把本地 pending `UpgradeRecord` 逐条报给服务端(容错失败,不阻断) /// 2. **拉服务端最新固件**:写入 `self.firmwareUpgrade`,通过 completion 回传 /// /// 软绑 / metadata 上报 / 硬绑失败回滚 → 已全部内聚到 `bind(userId:)`,本方法不再处理。 /// /// - Parameters: /// - sn: 设备序列号(bind 成功后从 `sdk.sn` 取) /// - currentVersion: 设备当前固件版本(从 `sdk.version` 取) /// - userId: 当前用户 ID(写入对账上报的 failureReason) public func fetchLatestFirmware( sn: String, currentVersion: String, userId: String, completion: @escaping (Result) -> Void ) { guard !sn.isEmpty else { DispatchQueue.main.async { completion(.failure(DuooomiBleError.firmwareUpgradeUnavailable("sn is empty (bind first)"))) } return } // Step 1: 对账上报历史 OTA 记录(容错失败,sn 级 in-flight 互斥) self.tryReportPendingUpgrade(sn: sn, currentVersion: currentVersion, userId: userId) // Step 2: 服务端 checkUpgrade self.firmwareUpgradeService.checkUpgrade(sn: sn, currentVersion: currentVersion) { [weak self] result in guard let self = self else { return } switch result { case .success(let r): DispatchQueue.main.async { self.firmwareUpgrade = r completion(.success(r)) } case .failure(let err): BleLog.w("checkUpgrade failed: \(err.localizedDescription)", "Firmware") DispatchQueue.main.async { completion(.failure(err)) } } } } /// OTA 固件烧录。完成后自动落 `UpgradeRecord` 到本地(复合主键 `(sn, firmwareId)`), /// 等待下次 `fetchLatestFirmware` 触发 `tryReportPendingUpgrade` 对账上报。 /// /// - Parameters: /// - sn: 设备序列号(用于落 UpgradeRecord 的主键) /// - fileUrl: 固件包下载 URL(一般从 `sdk.firmwareUpgrade?.targetFirmware?.fileUrl` 取) /// - firmwareId: 目标固件 ID(一般从 `sdk.firmwareUpgrade?.targetFirmware?.id` 取) /// - fromVersion: 升级前版本(一般从 `sdk.version` 取) /// - targetVersion: 升级目标版本(一般从 `sdk.firmwareUpgrade?.targetFirmware?.version` 取) /// - completion: 回调 `Result` /// - 成功:OTA 包已发完,设备会重启完成升级;UpgradeRecord 已落本地 /// - 失败:`.notConnected` / `.batteryTooLow(current:required:)`(电量 < `minBatteryForUpgrade`, /// 未下发任何字节)/ `.transferFailed(...)` / `getDeviceInfo` 的 `.timeout` / 解码错误 /// - Important: 下发前会**主动拉一次新鲜 `getDeviceInfo`** 校验实时电量,低于 /// `minBatteryForUpgrade`(40%)直接以 `.batteryTooLow` 失败,不发任何 OTA 字节。 public func upgradeFirmware( sn: String, fileUrl: String, firmwareId: String, fromVersion: String, targetVersion: String, completion: @escaping (Result) -> Void ) { guard connectedDevice != nil else { completion(.failure(DuooomiBleError.notConnected)) return } // OTA 前置电量校验:取实时电量(不用缓存,可能过期),低于门槛拒绝下发。 getDeviceInfo { [weak self] infoResult in guard let self = self else { return } switch infoResult { case .failure(let error): BleLog.w("OTA blocked: getDeviceInfo failed: \(error.localizedDescription)", "Firmware") completion(.failure(error)) case .success(let info): guard info.powerlevel >= Self.minBatteryForUpgrade else { BleLog.w("OTA blocked: battery \(info.powerlevel)% < \(Self.minBatteryForUpgrade)%", "Firmware") completion(.failure(DuooomiBleError.batteryTooLow( current: info.powerlevel, required: Self.minBatteryForUpgrade ))) return } self.performFirmwareTransfer( sn: sn, fileUrl: fileUrl, firmwareId: firmwareId, fromVersion: fromVersion, targetVersion: targetVersion, completion: completion ) } } } /// `upgradeFirmware` 通过电量门槛后的实际烧录 + 落 `UpgradeRecord`。 private func performFirmwareTransfer( sn: String, fileUrl: String, firmwareId: String, fromVersion: String, targetVersion: String, completion: @escaping (Result) -> Void ) { BleLog.i("OTA upgrade start: sn=\(sn) \(fromVersion)→\(targetVersion) (fw=\(firmwareId))", "Firmware") transferFile(fileUri: fileUrl, commandType: .otaPackage) { [weak self] result in if case .success = result { BleLog.i("OTA upgrade completed", "Firmware") if let self = self, !sn.isEmpty, !firmwareId.isEmpty, !fromVersion.isEmpty, !targetVersion.isEmpty { let record = UpgradeRecord( sn: sn, firmwareId: firmwareId, fromVersion: fromVersion, targetVersion: targetVersion, triggeredAt: UpgradeRecordStore.nowMillis() ) self.upgradeRecordStore.upsert(record) BleLog.i("UpgradeRecord upserted: sn=\(sn), fw=\(firmwareId), \(fromVersion)→\(targetVersion)", "Firmware") } } completion(result) } } /// 升级对账上报:把本地 pending `UpgradeRecord` 逐条报给服务端。 /// /// `fetchLatestFirmware` 内部第一步会调,集成方一般无需直接调用。**fire-and-forget**—— /// 没有 completion 回调,结果通过 `sdk.upgradeRecords` 数组的 `reported` / `outcome` 字段反映。 /// /// 内部逻辑(对齐 expo `bleStore.tryReportPendingUpgrade`): /// - 30 天 TTL:超期记录 → `markReported(outcome: nil)`,保留为审计轨迹 /// - 设备实际版本 == 目标版本 → `report-success` → `outcome: .success` /// - 不命中 → `report-failure(VERSION_MISMATCH ...)` → `outcome: .versionMismatch` /// - HTTP 失败 → 保持 `reported=false`,下次再重试 /// - sn 级 in-flight 互斥锁防止并发重复上报 /// /// - Parameters: /// - sn: 设备序列号 /// - currentVersion: 设备当前固件版本(与 record.targetVersion 比对) /// - userId: 当前用户 ID(写入 report 请求体) public func tryReportPendingUpgrade(sn: String, currentVersion: String, userId: String) { guard !sn.isEmpty, !currentVersion.isEmpty else { return } guard upgradeRecordStore.tryAcquire(sn: sn) else { return } let pending = upgradeRecordStore.pending(sn: sn) let now = UpgradeRecordStore.nowMillis() let group = DispatchGroup() for r in pending { if now - r.triggeredAt > UpgradeRecordStore.ttlMillis { upgradeRecordStore.markReported(sn: r.sn, firmwareId: r.firmwareId, outcome: nil) continue } group.enter() if currentVersion == r.targetVersion { firmwareUpgradeService.reportUpgradeSuccess( sn: r.sn, firmwareId: r.firmwareId, fromVersion: r.fromVersion, bindUserId: userId ) { [weak self] result in if case .success = result { self?.upgradeRecordStore.markReported(sn: r.sn, firmwareId: r.firmwareId, outcome: .success) } else if case .failure(let err) = result { BleLog.w("Report success failed for fw \(r.firmwareId): \(err.localizedDescription); will retry", "Firmware") } group.leave() } } else { let reason = "VERSION_MISMATCH expected=\(r.targetVersion) actual=\(currentVersion) sn=\(r.sn) userId=\(userId)" firmwareUpgradeService.reportUpgradeFailure( sn: r.sn, firmwareId: r.firmwareId, fromVersion: r.fromVersion, failureReason: reason ) { [weak self] result in if case .success = result { self?.upgradeRecordStore.markReported(sn: r.sn, firmwareId: r.firmwareId, outcome: .versionMismatch) } else if case .failure(let err) = result { BleLog.w("Report failure failed for fw \(r.firmwareId): \(err.localizedDescription); will retry", "Firmware") } group.leave() } } } group.notify(queue: .global()) { [weak self] in self?.upgradeRecordStore.release(sn: sn) } } }