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,109 @@
import Foundation
// MARK: - Binding (BLE / + OTA // +Firmware)
extension DuooomiBleSDK {
/// BLE 0x0F
///
/// **** BLE + `getDeviceVersion`
/// / / `fetchLatestFirmware(sn:currentVersion:userId:)`
///
/// SDK
/// - `sdk.sn`
/// - `sdk.version` getVersion bind
/// - `sdk.isActivated = true`delegate `didChangeActivation(true)`
///
/// - Parameters:
/// - userId: ID BLE BIND payload `bindUserId` token
/// - completion: `Result<BindingResponse, Error>`
/// - `BindingResponse(success: 1, sn, contents)`
/// - `.notConnected` / `.alreadyBoundByOtherUser`(success1) / `.timeout(.bindDevice)`
/// - Note: `bind` / `unbind` / `setPlayMode` `0x0F` / `0x12` opId ****
public func bind(userId: String, completion: @escaping (Result<BindingResponse, Error>) -> Void) {
guard ensureConnected(completion: completion) else { return }
BleLog.d("Sending bind (userId=\(userId))", "Command")
sendAsyncAndWait(commandType: .bindDevice, completion: { [weak self] result in
guard let self = self else { return }
switch result {
case .failure(let error):
completion(.failure(error))
case .success(let data):
do {
let resp = try self.decodeResponse(BindingResponse.self, from: data)
self.isActivated = resp.success == 1
if resp.success != 1 {
BleLog.w("Hard bind failed: success=0", "Command")
completion(.failure(DuooomiBleError.alreadyBoundByOtherUser))
return
}
self.sn = resp.sn
BleLog.i("Hard bind success: sn=\(resp.sn)", "Command")
// fetchLatestFirmware currentVersion
self.getVersion { _ in
completion(.success(resp))
}
} catch {
completion(.failure(error))
}
}
}, send: { done in
self.deviceInfoService.bindDevice(userId: userId, completion: done)
})
}
/// BLE 0x12 shipment-sn HTTPSDK
///
/// warn bind
///
/// SDK
/// - `sdk.sn`
/// - `sdk.isActivated = false`delegate `didChangeActivation(false)`
///
/// - Parameters:
/// - userId: ID BLE UNBIND payload + `bindUserId`
/// - completion: `Result<UnbindResponse, Error>`
/// - `UnbindResponse(success: 1)`
/// - `.notConnected` / `.unbindFailed`(BLE success1) / `.timeout(.unbindDevice)`
/// - Note: `bind` / `unbind` / `setPlayMode` `0x0F` / `0x12` opId ****
public func unbind(userId: String, completion: @escaping (Result<UnbindResponse, Error>) -> Void) {
guard ensureConnected(completion: completion) else { return }
let cachedSn = self.sn
BleLog.d("Sending unbind (userId=\(userId))", "Command")
sendAsyncAndWait(commandType: .unbindDevice, completion: { [weak self] result in
guard let self = self else { return }
switch result {
case .failure(let error):
completion(.failure(error))
case .success(let data):
do {
let resp = try self.decodeResponse(UnbindResponse.self, from: data)
if resp.success == 1 {
self.isActivated = false
self.sn = ""
BleLog.i("Hard unbind success", "Command")
if !cachedSn.isEmpty {
self.shipmentService.unbind(sn: cachedSn, bindUserId: userId) { softResult in
if case .failure(let err) = softResult {
BleLog.w("Soft unbind failed (ignored): \(err.localizedDescription)", "Command")
}
completion(.success(resp))
}
} else {
completion(.success(resp))
}
} else {
BleLog.w("Unbind failed", "Command")
completion(.failure(DuooomiBleError.unbindFailed))
}
} catch {
completion(.failure(error))
}
}
}, send: { done in
self.deviceInfoService.unbindDevice(userId: userId, completion: done)
})
}
}