import Foundation /// 设备命令服务:发送 JSON 命令(fire-and-forget,响应由协议层回调处理) final class DeviceInfoService { private let protocolService: BleProtocolService private let timeProvider: BeijingTimeProvider init(protocolService: BleProtocolService, timeProvider: BeijingTimeProvider) { self.protocolService = protocolService self.timeProvider = timeProvider } // MARK: - Commands func getDeviceInfo() throws { try protocolService.sendJSON( type: .getDeviceInfo, payload: CommandPayload(type: CommandType.getDeviceInfo.rawValue) ) } func getDeviceVersion() throws { try protocolService.sendJSON( type: .getDeviceVersion, payload: CommandPayload(type: CommandType.getDeviceVersion.rawValue) ) } /// 绑定:先取北京时间再发命令,与 RN 行为对齐。 func bindDevice(userId: String, completion: @escaping (Result) -> Void) { timeProvider.fetch { [weak self] time in guard let self = self else { return } do { try self.protocolService.sendJSON( type: .bindDevice, payload: BindPayload( type: CommandType.bindDevice.rawValue, userId: userId, loop: nil, time: time ) ) completion(.success(())) } catch { completion(.failure(error)) } } } func unbindDevice(userId: String, completion: @escaping (Result) -> Void) { timeProvider.fetch { [weak self] time in guard let self = self else { return } do { try self.protocolService.sendJSON( type: .unbindDevice, payload: BindPayload( type: CommandType.unbindDevice.rawValue, userId: userId, loop: nil, time: time ) ) completion(.success(())) } catch { completion(.failure(error)) } } } /// 切换播放模式(复用 BIND_DEVICE 命令字 0x0F,payload 携带 loop 字段) func setPlayMode(userId: String, loop: PlayMode, completion: @escaping (Result) -> Void) { timeProvider.fetch { [weak self] time in guard let self = self else { return } do { try self.protocolService.sendJSON( type: .bindDevice, payload: BindPayload( type: CommandType.bindDevice.rawValue, userId: userId, loop: UInt8(loop.rawValue), time: time ) ) completion(.success(())) } catch { completion(.failure(error)) } } } func deleteFile(key: String) throws { try protocolService.sendJSON( type: .deleteFile, payload: FileKeyPayload(type: CommandType.deleteFile.rawValue, key: key) ) } func prepareTransfer(key: String, size: Int) throws { try protocolService.sendJSON( type: .prepareTransfer, payload: PrepareTransferPayload( type: CommandType.prepareTransfer.rawValue, key: key, size: size ) ) } } // MARK: - Command Payloads private struct CommandPayload: Encodable { let type: UInt8 } /// 绑定 / 解绑 / 切模式 共用 payload。 /// `loop == nil` 时严格省略该字段(不写 null),与 Expo 端 zod.optional 行为对齐。 /// `time` 为北京时间 ISO 8601 字符串。 struct BindPayload: Encodable { let type: UInt8 let userId: String let loop: UInt8? let time: String enum CodingKeys: String, CodingKey { case type, userId, loop, time } 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) try container.encode(time, forKey: .time) if let loop = loop { try container.encode(loop, forKey: .loop) } } } private struct FileKeyPayload: Encodable { let type: UInt8 let key: String } private struct PrepareTransferPayload: Encodable { let type: UInt8 let key: String let size: Int }