import Foundation import MusubiCore public enum RemoteProtocol { public static let serviceType = "_musubi._tcp" public static let version = 1 } /// State of one screen on a node, as reported to the control. public struct ScreenStatus: Codable, Hashable, Sendable, Identifiable { public var id: ScreenRef { screen } public var screen: ScreenRef public var width: Int public var height: Int public var refreshRate: Double public var output: OutputStatus? public init(screen: ScreenRef, width: Int, height: Int, refreshRate: Double, output: OutputStatus?) { self.screen = screen self.width = width self.height = height self.refreshRate = refreshRate self.output = output } } public struct OutputStatus: Codable, Hashable, Sendable { public enum State: String, Codable, Sendable { case connecting, live, stalled } public var source: Source public var state: State public var width: Int public var height: Int public var frameRate: Double public var lostFrames: Int64 public var lossRate: Double public init(source: Source, state: State, width: Int, height: Int, frameRate: Double, lostFrames: Int64, lossRate: Double) { self.source = source self.state = state self.width = width self.height = height self.frameRate = frameRate self.lostFrames = lostFrames self.lossRate = lossRate } } public struct NodeStatus: Codable, Hashable, Sendable { public var screens: [ScreenStatus] /// What the node currently outputs (so a control can adopt it, e.g. when saving a scene). public var config: MachineConfig public var cpuPercent: Double public var sleepBlocked: Bool public init(screens: [ScreenStatus], config: MachineConfig, cpuPercent: Double, sleepBlocked: Bool) { self.screens = screens self.config = config self.cpuPercent = cpuPercent self.sleepBlocked = sleepBlocked } } public enum RemoteMessage: Codable, Hashable, Sendable { /// First message in both directions. case hello(name: String, role: Role, version: Int) /// Node → control, once per second. case status(NodeStatus) /// Control → node: replace the whole configuration. case apply(MachineConfig) /// Control → node: show one source on one screen. case show(VideoAssignment) /// Control → node: stop the output on one screen. case stop(ScreenRef) } /// Length-prefixed JSON framing: 4-byte big-endian length, then the JSON body. public struct MessageFramer: Sendable { public static let maxMessageSize = 4 * 1024 * 1024 private var buffer = Data() public init() {} public static func encode(_ message: RemoteMessage) throws -> Data { let body = try JSONEncoder().encode(message) var length = UInt32(body.count).bigEndian return Data(bytes: &length, count: 4) + body } public enum FramingError: Error { case tooLarge(Int) } /// Appends received bytes and returns all complete messages. Malformed JSON is skipped. public mutating func append(_ data: Data) throws -> [RemoteMessage] { buffer.append(data) var messages: [RemoteMessage] = [] while buffer.count >= 4 { let length = buffer.prefix(4).reduce(0) { Int($0) << 8 | Int($1) } guard length <= Self.maxMessageSize else { throw FramingError.tooLarge(length) } guard buffer.count >= 4 + length else { break } let body = buffer.subdata(in: buffer.startIndex + 4 ..< buffer.startIndex + 4 + length) buffer.removeFirst(4 + length) if let message = try? JSONDecoder().decode(RemoteMessage.self, from: body) { messages.append(message) } } return messages } }