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:
@@ -3,9 +3,11 @@ import Foundation
|
||||
/// 设备命令服务:发送 JSON 命令(fire-and-forget,响应由协议层回调处理)
|
||||
final class DeviceInfoService {
|
||||
private let protocolService: BleProtocolService
|
||||
private let timeProvider: BeijingTimeProvider
|
||||
|
||||
init(protocolService: BleProtocolService) {
|
||||
init(protocolService: BleProtocolService, timeProvider: BeijingTimeProvider) {
|
||||
self.protocolService = protocolService
|
||||
self.timeProvider = timeProvider
|
||||
}
|
||||
|
||||
// MARK: - Commands
|
||||
@@ -24,30 +26,66 @@ final class DeviceInfoService {
|
||||
)
|
||||
}
|
||||
|
||||
func bindDevice(userId: String) throws {
|
||||
try protocolService.sendJSON(
|
||||
type: .bindDevice,
|
||||
payload: BindPayload(type: CommandType.bindDevice.rawValue, userId: userId, loop: nil)
|
||||
)
|
||||
/// 绑定:先取北京时间再发命令,与 RN 行为对齐。
|
||||
func bindDevice(userId: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
timeProvider.fetch { [weak self] time in
|
||||
guard let self = self else { return }
|
||||
do {
|
||||
try self.protocolService.sendJSON(
|
||||
type: .bindDevice,
|
||||
payload: BindPayload(
|
||||
type: CommandType.bindDevice.rawValue,
|
||||
userId: userId,
|
||||
loop: nil,
|
||||
time: time
|
||||
)
|
||||
)
|
||||
completion(.success(()))
|
||||
} catch {
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func unbindDevice(userId: String) throws {
|
||||
try protocolService.sendJSON(
|
||||
type: .unbindDevice,
|
||||
payload: BindPayload(type: CommandType.unbindDevice.rawValue, userId: userId, loop: nil)
|
||||
)
|
||||
func unbindDevice(userId: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
timeProvider.fetch { [weak self] time in
|
||||
guard let self = self else { return }
|
||||
do {
|
||||
try self.protocolService.sendJSON(
|
||||
type: .unbindDevice,
|
||||
payload: BindPayload(
|
||||
type: CommandType.unbindDevice.rawValue,
|
||||
userId: userId,
|
||||
loop: nil,
|
||||
time: time
|
||||
)
|
||||
)
|
||||
completion(.success(()))
|
||||
} catch {
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 切换播放模式(复用 BIND_DEVICE 命令字 0x0F,payload 携带 loop 字段)
|
||||
func setPlayMode(userId: String, loop: PlayMode) throws {
|
||||
try protocolService.sendJSON(
|
||||
type: .bindDevice,
|
||||
payload: BindPayload(
|
||||
type: CommandType.bindDevice.rawValue,
|
||||
userId: userId,
|
||||
loop: UInt8(loop.rawValue)
|
||||
)
|
||||
)
|
||||
func setPlayMode(userId: String, loop: PlayMode, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
timeProvider.fetch { [weak self] time in
|
||||
guard let self = self else { return }
|
||||
do {
|
||||
try self.protocolService.sendJSON(
|
||||
type: .bindDevice,
|
||||
payload: BindPayload(
|
||||
type: CommandType.bindDevice.rawValue,
|
||||
userId: userId,
|
||||
loop: UInt8(loop.rawValue),
|
||||
time: time
|
||||
)
|
||||
)
|
||||
completion(.success(()))
|
||||
} catch {
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func deleteFile(key: String) throws {
|
||||
@@ -77,19 +115,22 @@ private struct CommandPayload: Encodable {
|
||||
|
||||
/// 绑定 / 解绑 / 切模式 共用 payload。
|
||||
/// `loop == nil` 时严格省略该字段(不写 null),与 Expo 端 zod.optional 行为对齐。
|
||||
/// `time` 为北京时间 ISO 8601 字符串。
|
||||
struct BindPayload: Encodable {
|
||||
let type: UInt8
|
||||
let userId: String
|
||||
let loop: UInt8?
|
||||
let time: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case type, userId, loop
|
||||
case type, userId, loop, time
|
||||
}
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(type, forKey: .type)
|
||||
try container.encode(userId, forKey: .userId)
|
||||
try container.encode(time, forKey: .time)
|
||||
if let loop = loop {
|
||||
try container.encode(loop, forKey: .loop)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user