主要改动: - 新增 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>
190 lines
9.3 KiB
Swift
190 lines
9.3 KiB
Swift
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-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<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)
|
||
}
|
||
}
|
||
}
|