feat(sdk): OTA 升级流程对齐 expo + 拆 extension 文件分层 + 秘钥本地化

主要改动:
- 新增 fetchLatestFirmware/upgradeFirmware/tryReportPendingUpgrade
  三段 OTA 接口,对齐 expo bindDeviceWithOrchestration 行为
- bind 仅做硬绑+getVersion,软绑/对账上报/拉最新固件搬到
  fetchLatestFirmware 内部串;软绑失败自动硬解回滚
- UpgradeRecord 复合主键 (sn, firmwareId) 持久化 + sn 级 in-flight 互斥
- DuooomiBleSDKDelegate 新增 didUpdateUpgradeRecords,集成方
  可观察对账上报结果(reported/outcome 状态)
- DuooomiBleSDK 主类按职责拆 5 个 extension 文件,公开 API 不变
- TargetFirmware 简化为只取必要字段 + 新增 forceUpgrade
- scanNamePrefix 改为必填 String
- demo 秘钥移至本地 DemoSecrets.swift (gitignored)
- README 重写:完整 OTA 流程示例 + 上报状态判定表

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
km2023
2026-05-08 14:46:41 +08:00
parent f4a32bb7e8
commit d5729a1a41
24 changed files with 2349 additions and 880 deletions

View File

@@ -0,0 +1,189 @@
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<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: 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-forgetBLE 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<Void, Error>`
/// - OTA UpgradeRecord
/// - `.notConnected` / `.transferFailed(...)`
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
}
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)
}
}
}