- 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.
bambulab/ JSON Schemas
Documentation/validation schemas for Bambu's raw MQTT wire format, mirroring
continuum-proxy's src/printer/bambu_commands/ - not used for codegen (the
Rust structs there are hand-written; see that module's doc comment for why),
but genuinely useful on their own: language-agnostic documentation of the
wire format, and usable to validate a captured MQTT payload without writing
any Rust to do it.
Layout
bambulab/
envelope.schema.json the base - sequence_id + command, shared by
every command in every generation, so it lives
here unversioned, not duplicated under v1/v2
v1/
request.schema.json BambuRequest dispatcher - one key per category
("info", "pushing", ...), each a oneOf (a
category can have more than one request shape)
report.schema.json BambuReport dispatcher, same idea
request/
info/
get_version.schema.json
pushing/
pushall.schema.json
report/
info/
get_version.schema.json
_module_info.schema.json
print/
print.schema.json the ongoing print-state push
_ams.schema.json
_ams_unit.schema.json
_ams_tray.schema.json
_vt_tray.schema.json shared base for AmsTray + the virtual/
external spool slot, via allOf + $ref
_upgrade_state.schema.json
_xcam.schema.json
v2/ add the same split once V2's wire format is
known to differ from V1's - removed for now
rather than leave broken copy-pasted stubs
One real correction baked into this layout: the report category for
whatever a pushall request triggers is report/print/, not
report/pushing/. Sending pushall (which does go to the pushing key
on the request side) makes the printer start/refresh an ongoing state-push
stream, and that stream arrives under the print key - a different root
key from the request that triggered it. A category folder here means
"what root key does this arrive under", not "what request caused it" - the
first version of this schema got that wrong (see git history) before a
closer read of the real capture caught it.
The "inheritance" pattern
Every command/response schema extends envelope.schema.json with
allOf + $ref:
{
"allOf": [
{ "$ref": "../../../envelope.schema.json" },
{ "type": "object", "properties": { "command": { "const": "get_version" } } }
]
}
This is genuinely closer to real inheritance than anything else in this
project's schema stack (proto, Rust): the instance must satisfy the base
schema and the extension schema simultaneously, and - verified with
negative test cases, not just asserted - envelope.schema.json's
required: ["sequence_id", "command"] is actually enforced on every
schema that extends it, not just copy-pasted as documentation.
v1/report/print/_ams_tray.schema.json is the other JSON-Schema-specific
tool worth knowing: oneOf, for "this is exactly one of several
genuinely different shapes" (an AMS tray slot is either empty - just
id - or loaded - the full field set - never something in between).
oneOf requires exactly one branch to match; watch out for a base-style
branch (like EmptyTray) accidentally also matching a more specific
branch's instance if the specific branch doesn't require enough fields -
verified and fixed once already here, see that file's description.
One gotcha if you add a new base-style schema: don't set
"additionalProperties": false on something meant to be allOf-extended.
allOf validates every sub-schema against the whole instance
independently - it doesn't merge object schemas - so a closed base schema
would reject every field the extending schema adds. Leave
additionalProperties unset (defaults to allowed) on anything used as a
base; enforce closedness only on the top-level dispatcher schemas
(v1/request.schema.json/v1/report.schema.json do this correctly -
check their additionalProperties: false).
$id convention
Every schema's $id is its real Gitea raw-file URL:
https://git.octoturge.com/Continuum/continuum-schemas/raw/branch/main/schemas/bambulab/...
- a genuinely dereferenceable URI (verified:
raw/branch/main/<path>is the correct Gitea pattern, notraw/<path>orraw/<branch>/<path>), rather than a made-up placeholder domain.$refs between files stay relative paths;$idis what a validator uses as the base URI to resolve those relative refs, and it's also what lets a schema be fetched standalone by anything that wants to dereference it directly.
Validating something
$ref resolution here relies on each schema's $id as the base URI for
its own relative refs (standard JSON Schema behavior) - register schemas
by $id, not by filename, or you'll hit collisions (get_version.schema.json
exists under both request/info/ and report/info/):
from referencing import Registry, Resource
from jsonschema import Draft202012Validator
import json, glob
resources = [
(json.load(open(p))["$id"], Resource.from_contents(json.load(open(p))))
for p in glob.glob("**/*.schema.json", recursive=True)
]
registry = Registry().with_resources(resources)
schema = json.load(open("v1/request.schema.json"))
Draft202012Validator(schema, registry=registry).validate(
{"info": {"sequence_id": "0", "command": "get_version"}}
)