`/ws/telemetry` is the socket a live fleet view needs and the backend does
not have yet. This is the shape both ends will mirror, written first so
neither invents one.
Which is exactly what happened before: continuum-app's telemetry store
consumes `printerId`, `nozzleTempC`, `bedTempC`, `progressPct`,
`updatedAt` and a closed `idle|printing|paused|error|offline` enum, inside
an envelope `{type: "printer.telemetry", data}` that no backend ever sent.
Every field of it was invented against nothing.
Two deliberate departures from the rest of this repository:
- **camelCase**, where `continuum/v1/edge/` is snake_case. That one is
shared with a Rust gateway whose serde derives the names directly; this
one is the backend's own HTTP vocabulary, and the browser already
consumes camelCase from `GET /printers`. Making it snake_case would buy
a consistency nobody benefits from at the price of a second model of a
printer in the client.
- **The payload is the printer resource**, the same shape
`GET /printers/:id` returns, rather than a telemetry-shaped subset. A
client can then apply a push to the row it already holds instead of
reconciling two vocabularies for the same machine.
Every telemetry field is nullable, which is load-bearing rather than
lazy: a printer that has never reported has no temperature, and "no data
yet" is not 0 degrees. A client rendering `?? 0` will draw a cold
printer as a real reading. `vendorState` is a free string and not an
enum for the same class of reason - an unrecognised state must reach the
screen as itself rather than being dropped on the way.
There is a `ready` message, sent once after the socket authenticates,
carrying how many printers the user can see. It exists because an
operator with nothing claimed and an operator whose gateway is offline
both see nothing arrive, and those two need different things said to
them - which is impossible if readiness is inferred from silence.
No Rust mirror: this protocol has no Rust consumer. `npm run typecheck`
and `npm run build` clean.
continuum-schemas
Single source of truth for the Continuum data model. Everything here is JSON Schema, compiled into two consumer packages:
packages/ts-types- hand-written TypeBox schemas undersrc/schemas/used for runtime validation incontinuum-backend's ElysiaJS routes.packages/rust-types- a Rust crate (continuum-rust-types) with hand-written,serde-derived structs forcontinuum-proxyandcontinuum-ai-worker.
Layout
schemas/continuum/v1/ Continuum's own model
ai/ continuum-backend <-> continuum-ai-worker: the
stream:camera_frames work queue and the
channel:print_alerts notification channel
edge/ continuum-backend <-> continuum-proxy: the
/ws/edge/v1 gateway uplink protocol
material/ printer/ domain objects
vendor.schema.json
schemas/bambulab/ Bambu Lab MQTT wire schemas (v1/v2, report + request)
packages/ts-types/ TypeBox schemas (hand-written, mirrors schemas/)
packages/rust-types/ serde structs (hand-written, mirrors schemas/)
The two inter-service protocols - edge/ and ai/ - are here for a
specific reason: each has two independent implementations, one per repo, and
when there was nothing written down between them they drifted completely
while every test passed. /ws/edge/v1 had an auth handshake the gateway
never sent; stream:camera_frames had a producer writing printerId and a
consumer reading printer_id, so every frame was attributed to a printer
called "unknown". Neither failure was visible from either side alone, which
is what a canonical schema plus a contract test on each end is for.
schemas/continuum/v1/*.schema.json is the canonical shape for each
domain object; packages/ts-types/src/schemas/ and
packages/rust-types/src/lib.rs both mirror it by hand. A mirror is added
per schema only once something actually consumes it, so not every file here
has one, and the two inter-service protocols keep their Rust mirror in the
single repo that consumes it rather than in packages/rust-types:
edge/ in continuum-proxy/src/uplink/protocol.rs, ai/ in
continuum-ai-worker/src/task.rs. The set is small and changes slowly,
which is why keeping hand-synced copies in sync is less overhead than a
codegen pipeline (protoc/ts-proto/prost-build) nothing actually
consumed - see git history for the
protobuf version this replaced, and why it was dropped: every service in
this system talks JSON (JSON Schema for the wire, TypeBox/serde for
runtime validation), so protobuf's actual value - a single schema compiled
into a compact binary wire format for both languages - never applied.
Whenever a domain schema changes, update all three by hand: the JSON Schema file, the TypeBox schema, and the Rust struct.
Build / verify
Requires npm install at the repo root, and a Rust toolchain, first.
npm install
npm run build:rust # cargo build, confirms the Rust structs compile
npm run typecheck # tsc -b, confirms the TypeBox schemas compile
There's no build:ts step - the TS side is hand-written, not generated.