Define the /ws/edge/v1 gateway uplink protocol
continuum-proxy and continuum-backend each had their own idea of what the edge WebSocket carried, and the two did not overlap anywhere: the backend expected an `auth` handshake the gateway never sends, telemetry shaped one printer per message where the gateway sends a batch, and UUID printer ids a gateway has no way to know. Neither end had a command path that matched the other. Nothing failed, because nothing was written down for both sides to mirror. This is that document. schemas/continuum/v1/edge/ is canonical: client_message, server_message, printer_telemetry and printer_command. packages/ts-types gets its first real TypeBox mirror - the placeholder there said to add one "once something actually consumes it", and continuum-backend's edge route now does. The Rust mirror stays in continuum-proxy/src/uplink/protocol.rs rather than packages/rust-types, since the gateway is its only Rust consumer. Two decisions worth recording: Field names are snake_case, unlike the backend's HTTP routes. The other end is serde-derived Rust, and snake_case is what those structs produce without a rename attribute on every field. It is also what the rest of this repository already uses. Optional fields accept an explicit null as well as being absent. A gateway omits what it has no value for, which is what these schemas describe - but serde emits null by default unless every field is annotated, and a single such field would otherwise fail validation and take a whole farm's telemetry batch down with it. Strict in what we send, liberal in what we accept; the contract tests on both sides pin down each half. Also bumps this package's @sinclair/typebox to ^0.33.12 to match what continuum-backend already pins. It was on ^0.32.0, which resolved to a separate 0.32.37 install - two TypeBox versions means two incompatible TSchema identities, and Elysia rejected every schema from here on those grounds alone.
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://git.octoturge.com/Continuum/continuum-schemas/raw/branch/main/schemas/continuum/v1/edge/client_message.schema.json",
|
||||
"title": "EdgeClientMessage",
|
||||
"description": "What a continuum-proxy gateway sends up to continuum-backend's /ws/edge/v1 route. Discriminated on 'type'. Field names are snake_case, matching every other schema in this repository - note that this differs from the camelCase the backend's HTTP routes use, because this wire format is shared with a Rust consumer whose serde structs derive these names directly.",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"title": "Hello",
|
||||
"description": "First message on every connection, before anything else is accepted. Carries the edge JWT minted by POST /auth/edge-token.",
|
||||
"properties": {
|
||||
"type": {
|
||||
"const": "hello"
|
||||
},
|
||||
"gateway_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"farm_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"token": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type",
|
||||
"gateway_id",
|
||||
"farm_id",
|
||||
"token"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"title": "Heartbeat",
|
||||
"properties": {
|
||||
"type": {
|
||||
"const": "heartbeat"
|
||||
},
|
||||
"seq": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"description": "Monotonic per connection, restarting at 1 on reconnect. Echoed back in heartbeat_ack so the gateway can tell a stalled connection from a slow one."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type",
|
||||
"seq"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"title": "Telemetry",
|
||||
"description": "One snapshot per printer that has something to report, on a timer. Sent as a batch rather than a message per printer: a farm has one uplink and many printers, and the backend wants a coherent picture of the farm at an instant more than it wants the lowest possible latency per machine.",
|
||||
"properties": {
|
||||
"type": {
|
||||
"const": "telemetry"
|
||||
},
|
||||
"printers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "./printer_telemetry.schema.json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type",
|
||||
"printers"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"title": "CommandResult",
|
||||
"description": "The reply to a server 'command'. Correlated by command_id, which the backend generates - unlike Bambu's sequence_id, Continuum owns both ends of this protocol, so the side that needs the correlation gets to choose it.",
|
||||
"properties": {
|
||||
"type": {
|
||||
"const": "command_result"
|
||||
},
|
||||
"command_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"ok": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"error": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"description": "Present only when ok is false. A human-readable message, not a code: its only destination is an operator looking at why a command did not take. Readers must accept an explicit null here as equivalent to the key being absent: a gateway omits what it has no value for, but a serde-derived Rust producer emits null unless every field is annotated, and rejecting a whole batch over that is not worth it."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type",
|
||||
"command_id",
|
||||
"ok"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"title": "FrameBatch",
|
||||
"description": "Camera stills already uploaded to object storage, handed over for the AI worker to score. Not yet sent by any gateway - continuum-proxy restreams cameras through go2rtc and has no frame-capture path yet - but the backend consumes it, so it is part of the contract.",
|
||||
"properties": {
|
||||
"type": {
|
||||
"const": "frame_batch"
|
||||
},
|
||||
"printer_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"print_job_id": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"format": "uuid",
|
||||
"description": "Readers must accept an explicit null here as equivalent to the key being absent: a gateway omits what it has no value for, but a serde-derived Rust producer emits null unless every field is annotated, and rejecting a whole batch over that is not worth it."
|
||||
},
|
||||
"frames": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"object_key": {
|
||||
"type": "string"
|
||||
},
|
||||
"captured_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"object_key",
|
||||
"captured_at"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type",
|
||||
"printer_id",
|
||||
"frames"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user