import Foundation // MARK: - Device Commands (无业务编排,纯协议层 RPC) extension DuooomiBleSDK { /// 读取设备信息(品牌、屏幕尺寸、电量、存储、播放模式等)。 /// /// 成功后同步写入 `sdk.deviceInfo` 并通过 delegate `didUpdateDeviceInfo` 回调。 /// /// - Parameter completion: 回调 `Result` /// - 成功:`DeviceInfo`(含 `name` / `brand` / `size` / `powerlevel` / `allspace` / `freespace` / `loop?`) /// - 失败:`.notConnected` / `.timeout(.getDeviceInfo)` / JSON 解码错误 public func getDeviceInfo(completion: @escaping (Result) -> 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`(含 `version` / `type` / `log?`) /// - 失败:`.notConnected` / `.timeout(.getDeviceVersion)` / JSON 解码错误 /// - Note: `log` 字段是设备返回的任意 JSON 对象,SDK 不解析其内部,存到 `sdk.versionLog`, /// `fetchLatestFirmware` 内部会透传给 shipment-sn `update-metadata` 接口。 public func getVersion(completion: @escaping (Result) -> 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) ?? "" 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`(`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) -> 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(success: 0)`(设备约定 0 表示成功) /// - 失败:`.notConnected` / `.deleteFileFailed(status:)`(status 非 0) /// / `.timeout(.deleteFile)` / JSON 解码错误 public func deleteFile(key: String, completion: @escaping (Result) -> 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(status: "ready", key)` /// - 失败:`.notConnected` / `.prepareTransferFailed(status:)` / `.timeout(.prepareTransfer)` public func prepareTransfer(key: String, size: Int, completion: @escaping (Result) -> 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) }) } }