import Foundation // MARK: - Firmware OTA (检查 / 烧录 / 对账上报) // // 所有公开方法显式入参,不依赖 SDK 内部状态自动取参。 // 集成方负责从 `sdk.sn` / `sdk.version` / `sdk.firmwareUpgrade` 等只读属性组装参数。 extension DuooomiBleSDK { /// 拉取服务端最新固件信息。**内部串三步业务编排**(顺序固定): /// /// 1. **对账上报**:把本地 pending `UpgradeRecord` 逐条报给服务端(容错失败,不阻断) /// 2. **软绑**:POST `/api/auth/loomart/shipment-sn/device/bind`,登记云端绑定关系 /// - 软绑失败 → 自动硬解绑(BLE 0x12)回滚 + 重置 `isActivated`/`sn`,整个 fetch 失败 /// 3. **拉服务端最新固件**:写入 `self.firmwareUpgrade`,通过 completion 回传 /// /// - Parameters: /// - sn: 设备序列号(bind 成功后从 `sdk.sn` 取) /// - currentVersion: 设备当前固件版本(从 `sdk.version` 取) /// - userId: 当前用户 ID 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: 软绑(shipment-sn HTTP) // 失败 → 自动硬解绑(BLE 0x12)回滚 + 重置 isActivated/sn,保持云端 ↔ 本地状态一致; // 整个 fetchLatestFirmware 失败回传,调用方收到错误后可重新走 bind 流程。 BleLog.d("Soft bind start: sn=\(sn)", "Firmware") shipmentService.bind(sn: sn, bindUserId: userId) { [weak self] result in guard let self = self else { return } if case .failure(let err) = result { BleLog.e("Soft bind failed during fetchLatestFirmware: \(err.localizedDescription); rolling back hard unbind", "Firmware") // 硬解绑回滚(fire-and-forget;BLE ack 不影响这次 fetch 错误回传) self.deviceInfoService.unbindDevice(userId: userId) { rollbackResult in if case .failure(let rbErr) = rollbackResult { BleLog.w("Hard unbind rollback failed (ignored): \(rbErr.localizedDescription)", "Firmware") } else { BleLog.i("Hard unbind rollback sent", "Firmware") } } self.isActivated = false self.sn = "" DispatchQueue.main.async { completion(.failure(err)) } return } BleLog.i("Soft bind success: sn=\(sn)", "Firmware") // Step 3: 服务端 checkUpgrade self.firmwareUpgradeService.checkUpgrade(sn: sn, currentVersion: currentVersion) { result in 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` / `.transferFailed(...)` 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 } 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) } } }