Files
continuum-schemas/schemas/continuum/v1/edge/client_message.schema.json
T
octoturge bb5a2680af 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.
2026-10-03 10:37:13 +00:00

145 lines
4.8 KiB
JSON

{
"$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"
]
}
]
}