import Foundation // MARK: - Binding (BLE 协议层硬绑/硬解 + 解绑端的软解;OTA 检查/对账/软绑搬到 +Firmware) extension DuooomiBleSDK { /// 完整绑定:内部串四步业务编排(顺序固定),对集成方表现为一次原子调用。 /// /// 1. **BLE 硬绑**(命令 0x0F):写 sn / isActivated /// 2. **getVersion**:写 version / versionLog(type 兼容 Int/String;log 为任意 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(success: 1, sn, contents)`(软绑也已成功,metadata 上报与否不影响结果) /// - 失败:`.notConnected` / `.alreadyBoundByOtherUser`(硬绑 success≠1) / `.timeout(.bindDevice)` /// / `.brandMismatch` / `.softBindFailed`(含硬解绑回滚后) /// - Note: `bind` / `unbind` / `setPlayMode` 共享 `0x0F` / `0x12` opId 通道,**不能并发**。 public func bind(userId: String, completion: @escaping (Result) -> 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) { 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) { 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) -> 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) -> 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-forget;BLE 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-forget;versionLog 缺失/空则跳过) 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 HTTP)。SDK 内部完成全套。 /// /// 软解失败仅打 warn 不阻断硬解结果回传——对端云记录如果残留,由后续 bind 自然修正。 /// /// 完成后 SDK 状态变化: /// - `sdk.sn` 清空 /// - `sdk.isActivated = false`,delegate `didChangeActivation(false)` 回调 /// /// - Parameters: /// - userId: 当前用户 ID(写入 BLE UNBIND payload + 软解请求体的 `bindUserId`) /// - completion: 回调 `Result` /// - 成功:`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) -> 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) }) } }