From c3a726fc02390848bc554e2bd4cbc8ba347dabd1 Mon Sep 17 00:00:00 2001 From: km2023 Date: Wed, 6 May 2026 10:42:09 +0800 Subject: [PATCH] feat: add setPlayMode SDK API with isActivated precondition --- Sources/DuooomiBleSDK/DuooomiBleSDK.swift | 50 ++++++++++++++++++++ Tests/DuooomiBleSDKTests/PlayModeTests.swift | 22 +++++++++ 2 files changed, 72 insertions(+) diff --git a/Sources/DuooomiBleSDK/DuooomiBleSDK.swift b/Sources/DuooomiBleSDK/DuooomiBleSDK.swift index 89ff42d..ca27c39 100644 --- a/Sources/DuooomiBleSDK/DuooomiBleSDK.swift +++ b/Sources/DuooomiBleSDK/DuooomiBleSDK.swift @@ -291,6 +291,56 @@ public final class DuooomiBleSDK: NSObject { }) } + /// 切换播放模式(0=单播 / 1=循环播放)。 + /// + /// 复用 `BIND_DEVICE` (0x0F) 命令通道;响应类型为 `BindingResponse`。 + /// 成功后自动刷新一次 `deviceInfo`(含最新 `loop` 字段),与 expo 端行为对齐。 + /// + /// - Important: 此方法要求 **设备已和该 `userId` 完成 `bind`**(即 `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") + + sendAndWait(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") + // 自动刷新 deviceInfo(含最新 loop 字段) + self.getDeviceInfo { _ in + completion(.success(resp)) + } + } catch { + completion(.failure(error)) + } + } + }, send: { + try self.deviceInfoService.setPlayMode(userId: userId, loop: mode) + }) + } + /// 解除设备绑定 public func unbind(userId: String, completion: @escaping (Result) -> Void) { guard ensureConnected(completion: completion) else { return } diff --git a/Tests/DuooomiBleSDKTests/PlayModeTests.swift b/Tests/DuooomiBleSDKTests/PlayModeTests.swift index 7248bc8..ca7463c 100644 --- a/Tests/DuooomiBleSDKTests/PlayModeTests.swift +++ b/Tests/DuooomiBleSDKTests/PlayModeTests.swift @@ -81,3 +81,25 @@ final class BindPayloadEncodingTests: XCTestCase { XCTAssertEqual(dict["loop"] as? Int, 0) } } + +final class SetPlayModeGuardTests: XCTestCase { + private func makeSDK() -> DuooomiBleSDK { + DuooomiBleSDK(config: DuooomiBleConfig(apiKey: "test")) + } + + func testSetPlayModeFailsWhenNotConnected() { + let sdk = makeSDK() + let exp = expectation(description: "completion") + sdk.setPlayMode(.loop, userId: "u1") { result in + switch result { + case .success: + XCTFail("expected failure when not connected") + case .failure(let error): + XCTAssertTrue("\(error)".lowercased().contains("connect"), + "expected notConnected-style error, got \(error)") + } + exp.fulfill() + } + wait(for: [exp], timeout: 1.0) + } +}