72 lines
3 KiB
Swift
72 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
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|