51dba8e896
- 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
159 lines
6.8 KiB
Markdown
159 lines
6.8 KiB
Markdown
# 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`:
|
|
|
|
```json
|
|
{
|
|
"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. `$ref`s 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/`):
|
|
|
|
```python
|
|
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"}}
|
|
)
|
|
```
|