Files
duooomi-ios-sdk/Sources/DuooomiBleSDK/DuooomiBleSDK+Firmware.swift
km2023 a15403ae36 feat(sdk): upgradeFirmware 下发前校验电量 ≥40%,不足抛 .batteryTooLow
OTA 烧录前主动拉新鲜 getDeviceInfo 取实时电量,<40% 直接以
.batteryTooLow(current:required:) 失败、不发任何字节,防烧录中途断电变砖。
新增常量 minBatteryForUpgrade=40;烧录体抽出私有 performFirmwareTransfer。
README 补可用方法/属性汇总表。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 17:19:23 +08:00

210 lines
9.9 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<FirmwareUpgradeCheckResult, Error>) -> 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<Void, Error>`
/// - 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, Error>) -> 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, Error>) -> 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)
}
}
}