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:
109
Sources/DuooomiBleSDK/DuooomiBleSDK+Binding.swift
Normal file
109
Sources/DuooomiBleSDK/DuooomiBleSDK+Binding.swift
Normal 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`(success≠1) / `.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 HTTP)。SDK 内部完成全套。
|
||||
///
|
||||
/// 软解失败仅打 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 层 success≠1) / `.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)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user