rainbow-dragon/Sources/ForgePanel/TangentWire.swift

71 lines
3 KiB
Swift

import Foundation
/// Messages exchanged with the Tangent Hub bridge.
/// Wire framing: u32 little-endian length + JSON payload.
/// NOTE: real Tangent Hub TIPC is a binary protocol on TCP 64246; this JSON
/// framing is our internal bridge format the macOS TIPC shim translates.
/// Framing/parse logic (buffering, partial frames) is identical either way.
public enum TangentMessage: Equatable, Sendable {
case controlUpdate(PanelInput)
case displayUpdate(line: Int, text: String)
}
public enum TangentWireError: Error {
case badPayload
}
public enum TangentWire {
public static func encode(_ msg: TangentMessage) -> Data {
var obj: [String: Any] = [:]
switch msg {
case .controlUpdate(.encoder(let id, let delta)):
obj = ["type": "encoder", "id": id, "delta": delta]
case .controlUpdate(.button(let id, let pressed)):
obj = ["type": "button", "id": id, "pressed": pressed]
case .displayUpdate(let line, let text):
obj = ["type": "display", "line": line, "text": text]
}
let payload = (try? JSONSerialization.data(withJSONObject: obj, options: [.sortedKeys])) ?? Data()
var out = Data()
var len = UInt32(payload.count).littleEndian
withUnsafeBytes(of: &len) { out.append(contentsOf: $0) }
out.append(payload)
return out
}
/// Decode first complete frame from buffer, consuming it. nil if incomplete.
public static func decodeFirst(_ buffer: inout Data) throws -> TangentMessage? {
guard buffer.count >= 4 else { return nil }
// Byte-wise LE assembly (Data slices may be misaligned for u32 loads).
let b = [UInt8](buffer.prefix(4))
let length = UInt32(b[0]) | (UInt32(b[1]) << 8) | (UInt32(b[2]) << 16) | (UInt32(b[3]) << 24)
guard buffer.count >= 4 + Int(length) else { return nil }
let payload = buffer.subdata(in: buffer.startIndex + 4..<buffer.startIndex + 4 + Int(length))
buffer.removeFirst(4 + Int(length))
guard let obj = try? JSONSerialization.jsonObject(with: payload) as? [String: Any],
let type = obj["type"] as? String else {
throw TangentWireError.badPayload
}
switch type {
case "encoder":
guard let id = obj["id"] as? Int, let delta = obj["delta"] as? Int else {
throw TangentWireError.badPayload
}
return .controlUpdate(.encoder(id: id, delta: delta))
case "button":
guard let id = obj["id"] as? Int, let pressed = obj["pressed"] as? Bool else {
throw TangentWireError.badPayload
}
return .controlUpdate(.button(id: id, pressed: pressed))
case "display":
guard let line = obj["line"] as? Int, let text = obj["text"] as? String else {
throw TangentWireError.badPayload
}
return .displayUpdate(line: line, text: text)
default:
throw TangentWireError.badPayload
}
}
}