主要改动: - 新增 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>
150 lines
4.6 KiB
Swift
150 lines
4.6 KiB
Swift
import Foundation
|
||
|
||
/// 设备命令服务:发送 JSON 命令(fire-and-forget,响应由协议层回调处理)
|
||
final class DeviceInfoService {
|
||
private let protocolService: BleProtocolService
|
||
private let timeProvider: BeijingTimeProvider
|
||
|
||
init(protocolService: BleProtocolService, timeProvider: BeijingTimeProvider) {
|
||
self.protocolService = protocolService
|
||
self.timeProvider = timeProvider
|
||
}
|
||
|
||
// MARK: - Commands
|
||
|
||
func getDeviceInfo() throws {
|
||
try protocolService.sendJSON(
|
||
type: .getDeviceInfo,
|
||
payload: CommandPayload(type: CommandType.getDeviceInfo.rawValue)
|
||
)
|
||
}
|
||
|
||
func getDeviceVersion() throws {
|
||
try protocolService.sendJSON(
|
||
type: .getDeviceVersion,
|
||
payload: CommandPayload(type: CommandType.getDeviceVersion.rawValue)
|
||
)
|
||
}
|
||
|
||
/// 绑定:先取北京时间再发命令,与 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, 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, 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 {
|
||
try protocolService.sendJSON(
|
||
type: .deleteFile,
|
||
payload: FileKeyPayload(type: CommandType.deleteFile.rawValue, key: key)
|
||
)
|
||
}
|
||
|
||
func prepareTransfer(key: String, size: Int) throws {
|
||
try protocolService.sendJSON(
|
||
type: .prepareTransfer,
|
||
payload: PrepareTransferPayload(
|
||
type: CommandType.prepareTransfer.rawValue,
|
||
key: key,
|
||
size: size
|
||
)
|
||
)
|
||
}
|
||
}
|
||
|
||
// MARK: - Command Payloads
|
||
|
||
private struct CommandPayload: Encodable {
|
||
let type: UInt8
|
||
}
|
||
|
||
/// 绑定 / 解绑 / 切模式 共用 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, 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)
|
||
}
|
||
}
|
||
}
|
||
|
||
private struct FileKeyPayload: Encodable {
|
||
let type: UInt8
|
||
let key: String
|
||
}
|
||
|
||
private struct PrepareTransferPayload: Encodable {
|
||
let type: UInt8
|
||
let key: String
|
||
let size: Int
|
||
}
|