Bambu's `stg_cur`: what a printer is doing when it is not laying down
plastic - bed levelling, nozzle cleaning, extrusion calibration. Passed
through as the printer's own number rather than decoded, because the
code-to-name table is not something we have. 0 means printing.
It is here because progress without it is ambiguous in a way that reads as a
fault. Confirmed on a P1P (2026-10-11): `percent` climbed to 14% while
`layer` stayed 0 for several minutes, the printer working through stages 54,
14 and 13 before starting layer 1. A console showing 14% at layer 0 with no
stage looks stuck, and an operator would reasonably go and power-cycle a
machine that was working perfectly.
Optional and additive, so gateways that predate it are unaffected.
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.