Files
duooomi-ios-sdk/Sources/DuooomiBleSDK/DuooomiBleSDK+Binding.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

203 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: - Binding (BLE / + OTA // +Firmware)
extension DuooomiBleSDK {
///
///
/// 1. **BLE ** 0x0F sn / isActivated
/// 2. **getVersion** version / versionLogtype Int/Stringlog JSON nil
/// 3. ****POST `/api/auth/loomart/shipment-sn/device/bind`
/// - BLE 0x12 + `isActivated`/`sn`/`versionLog` bind
/// 4. **updateMetadata**fire-and-forget versionLog `shipment-sn/device/update-metadata`
/// - warn versionLog /
///
/// SDK
/// - `sdk.sn`
/// - `sdk.version` getVersion bind
/// - `sdk.versionLog` log JSON nil
/// - `sdk.isActivated = true`delegate `didChangeActivation(true)`
///
/// - Parameters:
/// - userId: ID BLE BIND payload + `bindUserId`
/// - completion: `Result<BindingResponse, Error>`
/// - `BindingResponse(success: 1, sn, contents)`metadata
/// - `.notConnected` / `.alreadyBoundByOtherUser`( success1) / `.timeout(.bindDevice)`
/// / `.brandMismatch` / `.softBindFailed`
/// - Note: `bind` / `unbind` / `setPlayMode` `0x0F` / `0x12` opId ****
public func bind(userId: String, completion: @escaping (Result<BindingResponse, Error>) -> Void) {
guard ensureConnected(completion: completion) else { return }
verifyBrand { [weak self] result in
guard let self = self else { return }
switch result {
case .failure(let err):
completion(.failure(err))
case .success:
self.performHardBind(userId: userId, completion: completion)
}
}
}
/// brand SDK brand deviceInfo
/// `.brandMismatch`
private func verifyBrand(completion: @escaping (Result<Void, Error>) -> Void) {
if let cached = deviceInfo {
checkBrand(actual: cached.brand, completion: completion)
return
}
getDeviceInfo { [weak self] result in
guard let self = self else { return }
switch result {
case .failure(let err):
completion(.failure(err))
case .success(let info):
self.checkBrand(actual: info.brand, completion: completion)
}
}
}
private func checkBrand(actual: String, completion: @escaping (Result<Void, Error>) -> Void) {
let expected = config.brand
if actual == expected {
completion(.success(()))
return
}
BleLog.w("Brand mismatch: expected=\(expected), actual=\(actual); disconnecting", "Command")
disconnect { _ in
completion(.failure(DuooomiBleError.brandMismatch(expected: expected, actual: actual)))
}
}
private func performHardBind(userId: String, completion: @escaping (Result<BindingResponse, Error>) -> Void) {
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")
// self.version / self.versionLog updateMetadata
self.getVersion { _ in
self.performSoftBindAndMetadata(resp: resp, userId: userId, completion: completion)
}
} catch {
completion(.failure(error))
}
}
}, send: { done in
self.deviceInfoService.bindDevice(userId: userId, completion: done)
})
}
/// + metadata + + bind
/// metadata fire-and-forget warn
private func performSoftBindAndMetadata(
resp: BindingResponse,
userId: String,
completion: @escaping (Result<BindingResponse, Error>) -> Void
) {
let sn = resp.sn
BleLog.d("Soft bind start: sn=\(sn)", "Command")
shipmentService.bind(sn: sn, bindUserId: userId) { [weak self] result in
guard let self = self else { return }
if case .failure(let err) = result {
BleLog.e("Soft bind failed: \(err.localizedDescription); rolling back hard unbind", "Command")
// fire-and-forgetBLE ack bind
self.deviceInfoService.unbindDevice(userId: userId) { rollbackResult in
if case .failure(let rbErr) = rollbackResult {
BleLog.w("Hard unbind rollback failed (ignored): \(rbErr.localizedDescription)", "Command")
} else {
BleLog.i("Hard unbind rollback sent", "Command")
}
}
self.isActivated = false
self.sn = ""
self.versionLog = nil
DispatchQueue.main.async { completion(.failure(err)) }
return
}
BleLog.i("Soft bind success: sn=\(sn)", "Command")
// metadata fire-and-forgetversionLog /
if let log = self.versionLog, !log.isEmpty {
self.shipmentService.updateMetadata(sn: sn, data: log) { mdResult in
if case .failure(let err) = mdResult {
BleLog.w("updateMetadata failed (ignored): \(err.localizedDescription)", "Command")
} else {
BleLog.i("updateMetadata success: sn=\(sn)", "Command")
}
}
}
DispatchQueue.main.async { completion(.success(resp)) }
}
}
/// 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 = ""
self.versionLog = nil
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)
})
}
}