主要改动: - 新增 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>
112 lines
3.9 KiB
Swift
112 lines
3.9 KiB
Swift
import Foundation
|
||
|
||
/// OTA 升级记录持久化存储。与 RN `bleStore.upgradeRecords` 1:1 对齐:
|
||
/// - 复合主键 `(sn, firmwareId)` 的 upsert
|
||
/// - 30 天 TTL:过期但保留为审计轨迹(标记 `reported=true, outcome=nil`)
|
||
/// - sn 级 in-flight 互斥锁,防止并发重复上报
|
||
final class UpgradeRecordStore {
|
||
private static let storageKey = "duooomi.firmware_upgrade_records"
|
||
/// 30 天毫秒
|
||
static let ttlMillis: Int64 = 30 * 24 * 60 * 60 * 1000
|
||
|
||
private let defaults: UserDefaults
|
||
private let queue = DispatchQueue(label: "duooomi.upgradeRecord", attributes: .concurrent)
|
||
private var records: [UpgradeRecord]
|
||
private var inFlight: Set<String> = []
|
||
|
||
/// mutation 后主线程回调当前快照(SDK 用它通过 delegate 通知集成方)。
|
||
var onChange: (([UpgradeRecord]) -> Void)?
|
||
|
||
init(defaults: UserDefaults = .standard) {
|
||
self.defaults = defaults
|
||
if let data = defaults.data(forKey: Self.storageKey),
|
||
let decoded = try? JSONDecoder().decode([UpgradeRecord].self, from: data) {
|
||
self.records = decoded
|
||
} else {
|
||
self.records = []
|
||
}
|
||
}
|
||
|
||
// MARK: - Read
|
||
|
||
var all: [UpgradeRecord] {
|
||
queue.sync { records }
|
||
}
|
||
|
||
/// 取该 sn 下所有未上报的记录,按 triggeredAt 升序(旧→新)。
|
||
func pending(sn: String) -> [UpgradeRecord] {
|
||
queue.sync {
|
||
records.filter { $0.sn == sn && !$0.reported }
|
||
.sorted { $0.triggeredAt < $1.triggeredAt }
|
||
}
|
||
}
|
||
|
||
// MARK: - Mutation
|
||
|
||
/// 复合主键 `(sn, firmwareId)`:已存在 → 覆盖并复位 reported/outcome;不存在 → 追加。
|
||
func upsert(_ record: UpgradeRecord) {
|
||
guard !record.sn.isEmpty, !record.firmwareId.isEmpty else { return }
|
||
queue.async(flags: .barrier) {
|
||
if let idx = self.records.firstIndex(where: { $0.sn == record.sn && $0.firmwareId == record.firmwareId }) {
|
||
self.records[idx] = record
|
||
} else {
|
||
self.records.append(record)
|
||
}
|
||
self.persistLocked()
|
||
}
|
||
}
|
||
|
||
/// 标记某条记录已上报(线程安全)。
|
||
func markReported(sn: String, firmwareId: String, outcome: UpgradeOutcome?) {
|
||
queue.async(flags: .barrier) {
|
||
guard let idx = self.records.firstIndex(where: { $0.sn == sn && $0.firmwareId == firmwareId }) else { return }
|
||
let original = self.records[idx]
|
||
self.records[idx] = UpgradeRecord(
|
||
sn: original.sn,
|
||
firmwareId: original.firmwareId,
|
||
fromVersion: original.fromVersion,
|
||
targetVersion: original.targetVersion,
|
||
triggeredAt: original.triggeredAt,
|
||
reported: true,
|
||
reportedAt: Self.nowMillis(),
|
||
outcome: outcome
|
||
)
|
||
self.persistLocked()
|
||
}
|
||
}
|
||
|
||
// MARK: - In-flight Mutex (sn 级)
|
||
|
||
/// 尝试占用 sn 互斥锁。返回 false 表示已有进行中的上报,调用方应直接 return。
|
||
func tryAcquire(sn: String) -> Bool {
|
||
queue.sync(flags: .barrier) { () -> Bool in
|
||
if inFlight.contains(sn) { return false }
|
||
inFlight.insert(sn)
|
||
return true
|
||
}
|
||
}
|
||
|
||
func release(sn: String) {
|
||
queue.async(flags: .barrier) {
|
||
self.inFlight.remove(sn)
|
||
}
|
||
}
|
||
|
||
// MARK: - Helpers
|
||
|
||
static func nowMillis() -> Int64 {
|
||
Int64(Date().timeIntervalSince1970 * 1000)
|
||
}
|
||
|
||
/// 持久化 + 主线程回调 onChange。**必须在 barrier 内调用**。
|
||
private func persistLocked() {
|
||
if let data = try? JSONEncoder().encode(records) {
|
||
defaults.set(data, forKey: Self.storageKey)
|
||
}
|
||
if let onChange = onChange {
|
||
let snapshot = records
|
||
DispatchQueue.main.async { onChange(snapshot) }
|
||
}
|
||
}
|
||
}
|