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.
Pause, resume and stop, as one variant carrying a verb rather than three
near-identical ones - matching the gateway's `PrintControl`, which exists so
there is one method across four vendor implementations instead of three.
Confirmed live on a P1P (2026-10-11), which is also where the two warnings
in the description come from. A stop is a cancel and is not undoable: the
printer reports the cancelled print as FAILED. And `result: "success"` from
the printer means the command was accepted, not that anything changed - all
three verbs answered success while it sat IDLE with nothing to pause - so a
caller that needs to know the state changed has to read telemetry back.
The enum is `pause|resume|stop` and deliberately not `cancel`, even though
`cancel` is Moonraker's word and the Klipper implementation behind this does
send a cancel. A verb only one vendor recognises must not validate on the
wire.
The "no start a print variant" note stays, with its reason corrected:
starting an uploaded file has now been confirmed by hand, so what is missing
is the path around it - nothing uploads a sliced file to a gateway and
nothing decides which printer runs which job.
`stream:camera_frames` and `channel:print_alerts` are the only interface
between `continuum-backend` and `continuum-ai-worker`, and there was
nothing specifying either. They had drifted completely: the backend
wrote `farmId`, `printerId`, `r2ObjectKey` and `capturedAt`; the worker
read a single field called `printer_id` and ignored the rest. Every task
was delivered, acknowledged, and attributed to a printer called
"unknown" - and nothing errored, because a stream entry is a flat string
map and asking it for a field that is not there is not a failure. Same
shape as the /ws/edge/v1 drift, found the same way.
`schemas/continuum/v1/ai/` is now canonical, with a TypeBox mirror in
ts-types and a Rust mirror in the worker.
Two things the schemas have to say that JSON Schema does not naturally
express, both of which caused real bugs:
- A Valkey stream entry is a flat field/value map of *strings*. No
nesting, no numbers, no null. So FrameTask has no numeric or nested
properties, and a field with no value is omitted from the XADD rather
than written as `""` - an empty string is a value, and the producer
was writing one for `print_job_id`, which a consumer would carry into
a lookup or a uuid column.
- PrintAlert, by contrast, *is* a JSON document, because PUBLISH carries
one opaque string. It is also fire-and-forget with no persistence, so
it can only ever be a live-console feed and never the record of a
detection.
The README's layout section was stale (it still listed four DTOs that
were removed in 2026-09) and did not mention `edge/` or `ai/` at all. It
now names both, says why the inter-service protocols live here, and
notes that their Rust mirrors sit in the single repo that consumes each
rather than in `packages/rust-types`.
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.
Confirmed live: both BambuMQTT versions reply to a calibration request
with a dedicated print.calibration report, not just push_status - the
v1 request schema's own description said the opposite ("printer responds
with push_status instead of calibration"), and v2's report schema framed
this as a v1-vs-v2 contrast that no longer holds. Both descriptions
corrected; the printer does still send ordinary push_status updates
alongside the dedicated report as calibration progresses, same as any
other print job, not instead of it.
Added v1/report/print/calibration.schema.json (only existed for v2
before) and wired it into v1/report.schema.json's print category
dispatcher, which never listed it since the file didn't exist.
Replaces the old flat material/print_job/print_params/template schemas
with a richer structure: schemas/continuum/common/ for shared envelope +
enums (material_type taxonomy, printer_type), schemas/continuum/v1/
material/ (material + spool) and printer/ (printer_info/firmware_info/
module_info), plus a new vendor.schema.json.
Fixes found while verifying (all files added in this pass, so nothing
downstream depended on the broken paths yet):
- 5 broken $refs to common/envelope.schema.json - files under the new
v1/material/ and v1/printer/ subdirectories need ../../common/, not
../common/ (one more level of nesting than vendor.schema.json, which
sits directly under v1/).
- $id in 5 files (material.schema.json, spool.schema.json,
firmware_info/module_info/printer_info.schema.json) didn't match
their real path - mostly copy-paste from before the material/printer/
subdirectories existed.
- $id in all 13 files under common/material_type/ and
common/printer_type.enum.json said v1/ instead of common/.
- material.schema.json's hardness_value used minItems (an array-only
keyword, silently ignored on an integer) where minimum: 0 was
evidently intended, matching the maximum: 100 next to it.
Not fixed, needs a decision rather than a mechanical correction:
- spool.schema.json's `required` list is material.schema.json's
fields (material_type, min_nozzle_temp, ...), none of which exist in
spool's own properties (material, empty_spool_weight, ...) - copy-paste
left over from material.schema.json. continuum-schema-instance's error
message points right at it: "'material_type' is required but has no
schema in properties".
- envelope.schema.json's created_by/updated_by are typed
format: date-time (copy-pasted from created_at/updated_at) - almost
certainly meant to identify an actor, not a timestamp.
- printer_info.schema.json requires `host` via a oneOf (ipv4-or-ipv6
format) nested inside an allOf branch - continuum-schema-instance
doesn't merge a oneOf nested inside allOf (only plain
properties/required), so it currently builds an empty {} for this
schema; also worth confirming ipv4/ipv6-only is intended, since it'd
reject a DNS hostname.
- print_job.schema.json and print_params.schema.json were removed with
no replacement under the new structure - confirm that's intentional.
Also: the hand-written TypeBox schemas (ts-types) and serde structs
(rust-types) for Material/Template/PrintJob/PrintParameters now describe
the OLD flat shape and are out of sync with this restructure - per this
repo's hand-sync convention (see README), they need updating to match,
or the old ones should be removed if these new schemas supersede them.
Nothing imported the proto-generated code (zero references to
continuum-rust-types or @continuum/ts-types's proto export in any
consumer repo), and protobuf's real value - one schema compiled to a
compact binary wire format for both languages - never applied: BE<->proxy
and FE<->BE are both JSON. Also fixes 8 broken $ref paths in the bambulab
schemas and a root tsconfig.json that was sweeping the gitignored
examples/ directory into typecheck.
- proto/continuum/v1/*.proto -> schemas/continuum/v1/*.schema.json
- packages/rust-types: hand-written serde structs instead of
prost-build codegen
- packages/ts-types: drop the generated/ proto barrel export, keep the
hand-written TypeBox schemas as the TS source of truth
- root package.json: drop build:ts, protoc, ts-proto
- v2/common/device/device.schema.json: stray backtick in bed $ref broke resolution
- v2/report/print/print.schema.json: ams $ref pointed at wrong path (missing ams/ subdir)
- v2/common/device/plate.schema.json: $id was copy-pasted from bed.schema.json
- Add v2/request.schema.json and v2/report.schema.json dispatchers - v2's leaf
schemas were unreachable without them, same oneOf convention as v1
- Rewrite schemas/bambulab/README.md layout section, which still described
v1's pre-rename paths and claimed v2 was removed
- PrintJob.status serialized as a raw i32 in Rust (prost stores proto3 enums
as i32) while ts-types' TypeBox schema validates full enum name strings -
add serde with= on the field so Rust JSON matches TS and protobuf's own
canonical JSON enum mapping, plus a regression test
- print.schema.json, _ams.schema.json, _ams_unit.schema.json still
pointed at the pre-rename filenames (ams.schema.json, ams_unit.schema.json,
ams_tray.schema.json) after the underscore-prefix split - fixed all
three, verified with a full jsonschema + referencing validation run
against real captures (get_version, pushall, a full print/ams/xcam
report with both empty and loaded trays) plus negative tests.
- _upgrade_state.schema.json: fixed its $id (pointed at report/info/,
file lives under report/print/) and its title (said "ModuleInfo",
clearly copy-pasted from _module_info.schema.json). Wired it into
print.schema.json as the print report's `upgrade_state` property -
the file existed but nothing referenced it yet.
- Deleted schemas/bambulab/v2/, which had been recreated as stale,
broken copies of an older v1 (missing the envelope allOf, missing
the underscore renames, $ids still pointing at v1/ paths) - the
repo's own README documents v2 as intentionally removed until V2's
wire format is known to differ from V1's.
- Updated schemas/bambulab/README.md's layout tree and inheritance-
example reference, both stale after the underscore rename.
- Removed stray tsc -b build output (packages/ts-types/src/*.d.ts/js)
that isn't meant to be committed (outDir is dist/, gitignored).
Verified: cargo check clean (continuum-proxy, continuum-ai-worker,
rust-types), tsc clean (continuum-backend, continuum-common ts-core),
10/10 JSON Schema validation cases passing.
Each root JSON key ('info', 'pushing', 'print') now gets its own category
folder under request/ and report/, so a category can hold multiple command
schemas without flattening them into one directory - v1/request.schema.json
and v1/report.schema.json route into these via oneOf per category (also
future-proofs for a category ever needing more than one shape).
Split pushall's ams/xcam fields the same way module_info was already
split: ams.schema.json/ams_unit.schema.json/ams_tray.schema.json/
xcam.schema.json/print.schema.json, each referencing the next by relative
$ref. ams_tray.schema.json uses oneOf for the empty-vs-loaded tray shapes
- verified (and initially got wrong, then fixed) that LoadedTray needs
every field required, not just id, or a bare {"id":"0"} validates
against both branches and oneOf fails, since oneOf demands exactly one
match.
Real correction: the report/ category for what a pushall request
triggers is "print", not "pushing" - re-reading the original examples,
pushall's REQUEST goes to "pushing", but the resulting state-push stream
arrives under "print", a different root key. report/pushing/ (my
original mistake) is removed; report/print/ holds the corrected content.
request/pushing/ is untouched - that side was correct.
Also: v2/ removed (found as broken copy-pasted stubs still pointing at
v1 $ids - no real V2-specific content yet, better to have nothing than
broken duplicates); all $ids switched to real, verified Gitea raw-file
URLs (raw/branch/main/<path> - confirmed this is the correct pattern,
not raw/<path>) instead of a placeholder domain; fixed a broken relative
$ref in the request-side files left over from an earlier move (still
pointing 2 levels up instead of 3 after nesting one level deeper).
Re-validated everything against real examples after restructuring,
including the full ams/xcam print report and two negative cases -
see schemas/bambulab/README.md for the validation script.
Schemas (any notation - proto, JSON Schema) belong in the schemas repo
regardless of how many languages currently consume them; that's what
makes this 'the schemas repo'. Rust-only code (the hand-written structs
in continuum-proxy's bambu_commands/) is a different category and stays
where it was.
Also restructured per readability feedback:
- envelope.schema.json moved out of v1/ to the bambulab/ root - it's
shared across protocol generations, not v1-specific, same principle
already applied to the (now-removed) proto/bambulab/common.proto.
- v1/ split into v1/request/ and v1/report/, one file per command in
each, instead of *.request.schema.json / *.response.schema.json
filename suffixes doing the same job less clearly. The two top-level
dispatcher schemas (matching BambuRequest/BambuReport) stay at the v1/
root since they aren't themselves a single command's schema.
Re-verified after restructuring, not just moved and assumed still
correct: resolution now has to handle a real basename collision
(get_version.schema.json exists in both request/ and report/) — switched
the validation approach to register schemas by (their actual base
URI) rather than by filename, which is what makes relative resolve
correctly here. All three real examples plus the negative tests from the
original version still pass.