Files
duooomi-ios-sdk/Sources/DuooomiBleSDK/DuooomiBleSDK+Commands.swift
km2023 4bc8fdb64d feat(sdk): 绑定时上报设备 log 到 update-metadata 并内聚双层绑定
- bind(userId:) 内聚为硬绑 + getVersion + 软绑 + updateMetadata 一次原子调用,
  软绑失败自动硬解绑回滚 + 清状态;集成方无需感知双层绑定
- fetchLatestFirmware 简化为对账上报 + checkUpgrade 两步,不再重复软绑
- VersionInfo 增加 log: [String: Any]?,走 JSONSerialization 解析(放弃 Codable);
  SDK 暴露只读 versionLog,断连/解绑/软绑回滚时清空
- ShipmentSnDeviceService 新增 updateMetadata(sn:data:),抽出 performPOST
  共享 HTTP 编排,新增 postAnyJSON 支持 [String: Any] body
- 老固件不返回 log → versionLog == nil → 自动跳过 updateMetadata,前向兼容

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 16:25:55 +08:00

206 lines
9.8 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Foundation
// MARK: - Device Commands ( RPC)
extension DuooomiBleSDK {
///
///
/// `sdk.deviceInfo` delegate `didUpdateDeviceInfo`
///
/// - Parameter completion: `Result<DeviceInfo, Error>`
/// - `DeviceInfo` `name` / `brand` / `size` / `powerlevel` / `allspace` / `freespace` / `loop?`
/// - `.notConnected` / `.timeout(.getDeviceInfo)` / JSON
public func getDeviceInfo(completion: @escaping (Result<DeviceInfo, Error>) -> Void) {
guard ensureConnected(completion: completion) else { return }
BleLog.d("Sending getDeviceInfo", "Command")
sendAndWait(commandType: .getDeviceInfo, 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 info = try self.decodeResponse(DeviceInfo.self, from: data)
BleLog.i("DeviceInfo received: name=\(info.name), brand=\(info.brand)", "Command")
self.deviceInfo = info
completion(.success(info))
} catch {
completion(.failure(error))
}
}
}, send: {
try self.deviceInfoService.getDeviceInfo()
})
}
///
///
/// `sdk.version` delegate `didUpdateVersion`
/// `bind()`
///
/// - Parameter completion: `Result<VersionInfo, Error>`
/// - `VersionInfo` `version` / `type` / `log?`
/// - `.notConnected` / `.timeout(.getDeviceVersion)` / JSON
/// - Note: `log` JSON SDK `sdk.versionLog`
/// `fetchLatestFirmware` shipment-sn `update-metadata`
public func getVersion(completion: @escaping (Result<VersionInfo, Error>) -> Void) {
guard ensureConnected(completion: completion) else { return }
BleLog.d("Sending getDeviceVersion", "Command")
sendAndWait(commandType: .getDeviceVersion, 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 info = try VersionInfo.parse(from: data)
self.version = info.version
self.versionLog = info.log
BleLog.i("Version received: \(info.version) (type=\(info.type), log=\(info.log != nil ? "present" : "nil"))", "Command")
completion(.success(info))
} catch {
let raw = String(data: data.filter { $0 != 0 }, encoding: .utf8) ?? "<non-utf8, \(data.count) bytes>"
BleLog.e("VersionInfo parse failed: \(error.localizedDescription)\nRaw: \(raw)", "Decode")
completion(.failure(error))
}
}
}, send: {
try self.deviceInfoService.getDeviceVersion()
})
}
/// /
///
/// `BIND_DEVICE` (0x0F) `BindingResponse`
/// `deviceInfo` `loop` expo
///
/// - Parameters:
/// - mode: `.single`(0)= / `.loop`(1)=
/// - userId: ID userId
/// - completion: `Result<BindingResponse, Error>`
/// - `BindingResponse``success=1` / `sn` / `contents[]`
/// - `.notConnected` / `.bindingFailed("Device not bound; call bind(userId:) first")`
/// / `.bindingFailed("Set play mode failed")` / `.timeout(.bindDevice)`
/// - Important: `bind(userId:)` `isActivated == true` `success=0`
/// SDK
/// - Note: `bind` / `unbind` / `setPlayMode` `0x0F` / `0x12` opId ****
public func setPlayMode(
_ mode: PlayMode,
userId: String,
completion: @escaping (Result<BindingResponse, Error>) -> Void
) {
guard ensureConnected(completion: completion) else { return }
guard isActivated else {
BleLog.w("setPlayMode called before bind (isActivated=false)", "Command")
DispatchQueue.main.async {
completion(.failure(DuooomiBleError.bindingFailed("Device not bound; call bind(userId:) first")))
}
return
}
BleLog.d("Sending setPlayMode (mode=\(mode), 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)
if resp.success != 1 {
BleLog.w("setPlayMode failed: device rejected", "Command")
completion(.failure(DuooomiBleError.bindingFailed("Set play mode failed")))
return
}
BleLog.i("setPlayMode success: mode=\(mode)", "Command")
self.getDeviceInfo { _ in
completion(.success(resp))
}
} catch {
completion(.failure(error))
}
}
}, send: { done in
self.deviceInfoService.setPlayMode(userId: userId, loop: mode, completion: done)
})
}
///
///
/// - Parameters:
/// - key: key `BindingResponse.contents`
/// - completion: `Result<DeleteFileResponse, Error>`
/// - `DeleteFileResponse(success: 0)` 0
/// - `.notConnected` / `.deleteFileFailed(status:)`status 0
/// / `.timeout(.deleteFile)` / JSON
public func deleteFile(key: String, completion: @escaping (Result<DeleteFileResponse, Error>) -> Void) {
guard ensureConnected(completion: completion) else { return }
BleLog.d("Sending deleteFile (key=\(key))", "Command")
sendAndWait(commandType: .deleteFile, 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(DeleteFileResponse.self, from: data)
if resp.success != 0 {
BleLog.w("Delete failed with status=\(resp.success)", "Command")
completion(.failure(DuooomiBleError.deleteFileFailed(status: resp.success)))
} else {
BleLog.i("Delete success (key=\(key))", "Command")
completion(.success(resp))
}
} catch {
completion(.failure(error))
}
}
}, send: {
try self.deviceInfoService.deleteFile(key: key)
})
}
/// key
///
/// `transferMedia(fileUrl:)`
///
/// - Parameters:
/// - key: key URL
/// - size:
/// - completion: `Result<PrepareTransferResponse, Error>`
/// - `PrepareTransferResponse(status: "ready", key)`
/// - `.notConnected` / `.prepareTransferFailed(status:)` / `.timeout(.prepareTransfer)`
public func prepareTransfer(key: String, size: Int, completion: @escaping (Result<PrepareTransferResponse, Error>) -> Void) {
guard ensureConnected(completion: completion) else { return }
let opId = "\(CommandType.prepareTransfer.rawValue)_\(key)"
BleLog.d("Sending prepareTransfer (key=\(key), size=\(size))", "Transfer")
sendAndWait(opId: opId, commandType: .prepareTransfer, 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(PrepareTransferResponse.self, from: data)
if resp.status != "ready" {
BleLog.w("PrepareTransfer not ready: status=\(resp.status)", "Transfer")
completion(.failure(DuooomiBleError.prepareTransferFailed(status: resp.status)))
} else {
BleLog.i("PrepareTransfer ready (key=\(key))", "Transfer")
completion(.success(resp))
}
} catch {
completion(.failure(error))
}
}
}, send: {
try self.deviceInfoService.prepareTransfer(key: key, size: size)
})
}
}