Files
continuum-schemas/schemas/bambulab

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/
  common/
    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
    common/                 shapes shared across v1's request/ and report/
      ams/
        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
      module_info.schema.json
      hms.schema.json, net.schema.json, online.schema.json,
      upload.schema.json, upgrade_state.schema.json, xcam.schema.json,
      ipcam.schema.json, lights_report.schema.json, nozzle_type.schema.json,
      nozzle_diameter.schema.json
    request/
      info/
        get_version.schema.json
      pushing/
        pushall.schema.json
    report/
      info/
        get_version.schema.json
      print/
        print.schema.json    the ongoing print-state push
  v2/                       same split as v1, once the wire format actually
                            diverges per field - built out below as that's
                            been confirmed, not copy-pasted speculatively
    request.schema.json      dispatcher, same shape as v1's
    report.schema.json       dispatcher, same shape as v1's
    common/                 v2 grew a much larger shared shape set than v1
                            (device/, job/, care, info, ...) because more
                            of the wire format has been captured/confirmed
      ams/, device/, job/, and the same flat common/*.schema.json files
      as v1 (hms, net, online, upload, upgrade_state, xcam, ipcam,
      lights_report, nozzle_type, nozzle_diameter, vt_tray)
    request/
      info/
        get_version.schema.json
    report/
      info/
        get_version.schema.json
        _module_info.schema.json
      print/
        print.schema.json    the ongoing print-state push (command
                            "push_status")
        _2d_report.schema.json, _3d_report.schema.json,
        _nozzle_type.schema.json

Add a category to a dispatcher (v1|v2/request.schema.json or report.schema.json) whenever a new request/<category>/ or report/<category>/ schema is added on disk - an unwired leaf schema is unreachable from validation even though it parses fine on its own.

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": "../../../common/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, not raw/<path> or raw/<branch>/<path>), rather than a made-up placeholder domain. $refs between files stay relative paths; $id is 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"}}
)