From 7aec09be2ac344da0a5b2441d8f9a0cf40d17164 Mon Sep 17 00:00:00 2001 From: km2023 Date: Wed, 6 May 2026 10:38:19 +0800 Subject: [PATCH] feat: extend BindPayload with optional loop field; add setPlayMode service Co-Authored-By: Claude Sonnet 4.6 --- .../Services/DeviceInfoService.swift | 34 +++++++++++++++++-- Tests/DuooomiBleSDKTests/PlayModeTests.swift | 31 +++++++++++++++++ 2 files changed, 62 insertions(+), 3 deletions(-) diff --git a/Sources/DuooomiBleSDK/Services/DeviceInfoService.swift b/Sources/DuooomiBleSDK/Services/DeviceInfoService.swift index 2794867..4666d7d 100644 --- a/Sources/DuooomiBleSDK/Services/DeviceInfoService.swift +++ b/Sources/DuooomiBleSDK/Services/DeviceInfoService.swift @@ -27,14 +27,26 @@ final class DeviceInfoService { func bindDevice(userId: String) throws { try protocolService.sendJSON( type: .bindDevice, - payload: BindPayload(type: CommandType.bindDevice.rawValue, userId: userId) + payload: BindPayload(type: CommandType.bindDevice.rawValue, userId: userId, loop: nil) ) } func unbindDevice(userId: String) throws { try protocolService.sendJSON( type: .unbindDevice, - payload: BindPayload(type: CommandType.unbindDevice.rawValue, userId: userId) + payload: BindPayload(type: CommandType.unbindDevice.rawValue, userId: userId, loop: nil) + ) + } + + /// 切换播放模式(复用 BIND_DEVICE 命令字 0x0F,payload 携带 loop 字段) + func setPlayMode(userId: String, loop: PlayMode) throws { + try protocolService.sendJSON( + type: .bindDevice, + payload: BindPayload( + type: CommandType.bindDevice.rawValue, + userId: userId, + loop: UInt8(loop.rawValue) + ) ) } @@ -63,9 +75,25 @@ private struct CommandPayload: Encodable { let type: UInt8 } -private struct BindPayload: Encodable { +/// 绑定 / 解绑 / 切模式 共用 payload。 +/// `loop == nil` 时严格省略该字段(不写 null),与 Expo 端 zod.optional 行为对齐。 +struct BindPayload: Encodable { let type: UInt8 let userId: String + let loop: UInt8? + + enum CodingKeys: String, CodingKey { + case type, userId, loop + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(type, forKey: .type) + try container.encode(userId, forKey: .userId) + if let loop = loop { + try container.encode(loop, forKey: .loop) + } + } } private struct FileKeyPayload: Encodable { diff --git a/Tests/DuooomiBleSDKTests/PlayModeTests.swift b/Tests/DuooomiBleSDKTests/PlayModeTests.swift index 8463d13..7248bc8 100644 --- a/Tests/DuooomiBleSDKTests/PlayModeTests.swift +++ b/Tests/DuooomiBleSDKTests/PlayModeTests.swift @@ -50,3 +50,34 @@ final class DeviceInfoLoopTests: XCTestCase { XCTAssertNil(info.loop) } } + +final class BindPayloadEncodingTests: XCTestCase { + private func encode(_ payload: BindPayload) throws -> [String: Any] { + let data = try JSONEncoder().encode(payload) + return try JSONSerialization.jsonObject(with: data) as? [String: Any] ?? [:] + } + + func testBindPayloadWithoutLoopOmitsKey() throws { + let payload = BindPayload(type: 0x0F, userId: "u1", loop: nil) + let dict = try encode(payload) + XCTAssertEqual(dict["type"] as? Int, 0x0F) + XCTAssertEqual(dict["userId"] as? String, "u1") + XCTAssertNil(dict["loop"], "loop key MUST be omitted when nil (not encoded as null)") + XCTAssertEqual(dict.count, 2) + } + + func testBindPayloadWithLoopIncludesValue() throws { + let payload = BindPayload(type: 0x0F, userId: "u1", loop: 1) + let dict = try encode(payload) + XCTAssertEqual(dict["type"] as? Int, 0x0F) + XCTAssertEqual(dict["userId"] as? String, "u1") + XCTAssertEqual(dict["loop"] as? Int, 1) + XCTAssertEqual(dict.count, 3) + } + + func testBindPayloadWithLoopZeroIncludesValue() throws { + let payload = BindPayload(type: 0x0F, userId: "u1", loop: 0) + let dict = try encode(payload) + XCTAssertEqual(dict["loop"] as? Int, 0) + } +}