Category subdirectories for request/report; ams/xcam split; print vs pushing fix

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.
This commit is contained in:
2026-08-28 23:17:32 +00:00
parent 8d4a08e36d
commit cd18337e12
14 changed files with 201 additions and 47 deletions
+53 -15
View File
@@ -15,19 +15,40 @@ bambulab/
every command in every generation, so it lives every command in every generation, so it lives
here unversioned, not duplicated under v1/v2 here unversioned, not duplicated under v1/v2
v1/ v1/
request.schema.json BambuRequest dispatcher — {"info": ...} / {"pushing": ...} request.schema.json BambuRequest dispatcher — one key per category
report.schema.json BambuReport dispatcher ("info", "pushing", ...), each a oneOf (a
category can have more than one request shape)
report.schema.json BambuReport dispatcher, same idea
request/ request/
get_version.schema.json info/
pushall.schema.json get_version.schema.json
pushing/
pushall.schema.json
report/ report/
get_version.schema.json info/
pushall.schema.json get_version.schema.json
module_info.schema.json module_info.schema.json
v2/ add the same request/report split once V2's print/
wire format is known to differ from V1's print.schema.json the ongoing print-state push
ams.schema.json
ams_unit.schema.json
ams_tray.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 ## The "inheritance" pattern
Every command/response schema *extends* `envelope.schema.json` with Every command/response schema *extends* `envelope.schema.json` with
@@ -36,7 +57,7 @@ Every command/response schema *extends* `envelope.schema.json` with
```json ```json
{ {
"allOf": [ "allOf": [
{ "$ref": "../../envelope.schema.json" }, { "$ref": "../../../envelope.schema.json" },
{ "type": "object", "properties": { "command": { "const": "get_version" } } } { "type": "object", "properties": { "command": { "const": "get_version" } } }
] ]
} }
@@ -47,10 +68,16 @@ project's schema stack (proto, Rust): the instance must satisfy the base
schema *and* the extension schema simultaneously, and — verified with schema *and* the extension schema simultaneously, and — verified with
negative test cases, not just asserted — `envelope.schema.json`'s negative test cases, not just asserted — `envelope.schema.json`'s
`required: ["sequence_id", "command"]` is actually enforced on every `required: ["sequence_id", "command"]` is actually enforced on every
schema that extends it, not just copy-pasted as documentation. Proto and schema that extends it, not just copy-pasted as documentation.
Rust only ever gave us composition (embed a field holding the base),
never something that reuses a *named, referenced* schema's constraints `v1/report/print/ams_tray.schema.json` is the other JSON-Schema-specific
automatically like this. 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 **One gotcha if you add a new base-style schema**: don't set
`"additionalProperties": false` on something meant to be `allOf`-extended. `"additionalProperties": false` on something meant to be `allOf`-extended.
@@ -62,12 +89,23 @@ base; enforce closedness only on the top-level dispatcher schemas
(`v1/request.schema.json`/`v1/report.schema.json` do this correctly — (`v1/request.schema.json`/`v1/report.schema.json` do this correctly —
check their `additionalProperties: false`). 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 ## Validating something
`$ref` resolution here relies on each schema's `$id` as the base URI for `$ref` resolution here relies on each schema's `$id` as the base URI for
its own relative refs (standard JSON Schema behavior) — register schemas its own relative refs (standard JSON Schema behavior) — register schemas
by `$id`, not by filename, or you'll hit collisions (`get_version.schema.json` by `$id`, not by filename, or you'll hit collisions (`get_version.schema.json`
exists in both `request/` and `report/`): exists under both `request/info/` and `report/info/`):
```python ```python
from referencing import Registry, Resource from referencing import Registry, Resource
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"$schema": "https://json-schema.org/draft/2020-12/schema", "$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://continuum.internal/schemas/bambulab/envelope.schema.json", "$id": "https://git.octoturge.com/Continuum/continuum-schemas/raw/branch/main/schemas/bambulab/envelope.schema.json",
"title": "CommandEnvelope", "title": "CommandEnvelope",
"description": "Fields every Bambu MQTT command/response shares, across both protocol generations — unversioned, at the bambulab/ root, not duplicated under v1/ or v2/. This is the 'base class' — other schemas extend it via allOf + $ref. JSON Schema doesn't have inheritance either, strictly speaking (no 'extends' keyword) — allOf is composition: it says the instance must satisfy THIS schema AND whatever else is listed alongside it. It's the closest thing to real inheritance ergonomics of anything used across this project's schema stack (proto, Rust), because $ref + allOf lets you name and reuse a shape without re-declaring its fields, which proto/Rust composition can't quite do (there you still write `common: PrinterReportCommon` by hand on every struct).", "description": "Fields every Bambu MQTT command/response shares, across both protocol generations — unversioned, at the bambulab/ root, not duplicated under v1/ or v2/. This is the 'base class' — other schemas extend it via allOf + $ref. JSON Schema doesn't have inheritance either, strictly speaking (no 'extends' keyword) — allOf is composition: it says the instance must satisfy THIS schema AND whatever else is listed alongside it. It's the closest thing to real inheritance ergonomics of anything used across this project's schema stack (proto, Rust), because $ref + allOf lets you name and reuse a shape without re-declaring its fields, which proto/Rust composition can't quite do (there you still write `common: PrinterReportCommon` by hand on every struct).",
"type": "object", "type": "object",
+12 -4
View File
@@ -1,14 +1,22 @@
{ {
"$schema": "https://json-schema.org/draft/2020-12/schema", "$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://continuum.internal/schemas/bambulab/v1/report.schema.json", "$id": "https://git.octoturge.com/Continuum/continuum-schemas/raw/branch/main/schemas/bambulab/v1/report.schema.json",
"title": "BambuReport", "title": "BambuReport",
"description": "Matches continuum-proxy's src/printer/bambu_commands::BambuReport.", "description": "Matches continuum-proxy's src/printer/bambu_commands::BambuReport. Note the category here is \"print\", not \"pushing\" — sending a pushall request (which does go to \"pushing\", see request.schema.json) makes the printer start/refresh the ongoing state-push stream, which itself arrives under \"print\". They're different root keys because a report category means 'what key does this arrive under', not 'what request caused it'. Each category's value is a oneOf for the same reason as request.schema.json: room for more than one shape per category without a rewrite later.",
"type": "object", "type": "object",
"minProperties": 1, "minProperties": 1,
"maxProperties": 1, "maxProperties": 1,
"properties": { "properties": {
"info": { "$ref": "report/get_version.schema.json" }, "info": {
"pushing": { "$ref": "report/pushall.schema.json" } "oneOf": [
{ "$ref": "report/info/get_version.schema.json" }
]
},
"print": {
"oneOf": [
{ "$ref": "report/print/print.schema.json" }
]
}
}, },
"additionalProperties": false "additionalProperties": false
} }
@@ -1,9 +1,9 @@
{ {
"$schema": "https://json-schema.org/draft/2020-12/schema", "$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://continuum.internal/schemas/bambulab/v1/report/get_version.schema.json", "$id": "https://git.octoturge.com/Continuum/continuum-schemas/raw/branch/main/schemas/bambulab/v1/report/info/get_version.schema.json",
"title": "GetVersionResponse", "title": "GetVersionResponse",
"allOf": [ "allOf": [
{ "$ref": "../../envelope.schema.json" }, { "$ref": "../../../envelope.schema.json" },
{ {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -1,6 +1,6 @@
{ {
"$schema": "https://json-schema.org/draft/2020-12/schema", "$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://continuum.internal/schemas/bambulab/v1/report/module_info.schema.json", "$id": "https://git.octoturge.com/Continuum/continuum-schemas/raw/branch/main/schemas/bambulab/v1/report/info/module_info.schema.json",
"title": "ModuleInfo", "title": "ModuleInfo",
"type": "object", "type": "object",
"properties": { "properties": {
@@ -0,0 +1,14 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.octoturge.com/Continuum/continuum-schemas/raw/branch/main/schemas/bambulab/v1/report/print/ams.schema.json",
"title": "AmsState",
"description": "Bambu's own naming has the outer container and the inner array both called `ams` — print.ams.ams, not a typo here, just what the printer actually sends.",
"type": "object",
"properties": {
"ams": {
"type": "array",
"items": { "$ref": "ams_unit.schema.json" }
}
},
"required": ["ams"]
}
@@ -0,0 +1,48 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.octoturge.com/Continuum/continuum-schemas/raw/branch/main/schemas/bambulab/v1/report/print/ams_tray.schema.json",
"title": "AmsTray",
"description": "A tray slot is either empty (just `id`) or loaded (the full field set) — two genuinely different shapes for the same slot, not one shape with optional fields. oneOf says the instance must match EXACTLY one of these. Note LoadedTray requires every field it lists, not just `id` — with only `id` required, a bare {\"id\":\"0\"} would validate against BOTH branches at once (it trivially satisfies Loaded's single requirement too) and oneOf would fail, since oneOf demands exactly one match. Verified this the hard way — see git history. The Rust equivalent is an untagged enum with #[serde(deny_unknown_fields)] on the Empty variant, for the same reason in reverse: without it, a loaded tray's extra fields wouldn't be rejected by Empty, and untagged enums try variants in order — see bambu_commands/print.rs.",
"oneOf": [
{
"title": "EmptyTray",
"type": "object",
"properties": {
"id": { "type": "string" }
},
"required": ["id"],
"additionalProperties": false
},
{
"title": "LoadedTray",
"type": "object",
"properties": {
"id": { "type": "string" },
"bed_temp": { "type": "string" },
"bed_temp_type": { "type": "string" },
"cols": { "type": "array", "items": { "type": "string" } },
"drying_temp": { "type": "string" },
"drying_time": { "type": "string" },
"nozzle_temp_max": { "type": "string" },
"nozzle_temp_min": { "type": "string" },
"remain": { "type": "integer" },
"tag_uid": { "type": "string" },
"tray_color": { "type": "string" },
"tray_diameter": { "type": "string" },
"tray_id_name": { "type": "string" },
"tray_info_idx": { "type": "string" },
"tray_sub_brands": { "type": "string" },
"tray_type": { "type": "string" },
"tray_uuid": { "type": "string" },
"tray_weight": { "type": "string" },
"xcam_info": { "type": "string" }
},
"required": [
"id", "bed_temp", "bed_temp_type", "cols", "drying_temp", "drying_time",
"nozzle_temp_max", "nozzle_temp_min", "remain", "tag_uid", "tray_color",
"tray_diameter", "tray_id_name", "tray_info_idx", "tray_sub_brands",
"tray_type", "tray_uuid", "tray_weight", "xcam_info"
]
}
]
}
@@ -0,0 +1,17 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.octoturge.com/Continuum/continuum-schemas/raw/branch/main/schemas/bambulab/v1/report/print/ams_unit.schema.json",
"title": "AmsUnit",
"description": "One physical AMS unit (Bambu's numbering starts at 0 via `id`).",
"type": "object",
"properties": {
"humidity": { "type": "string" },
"id": { "type": "string" },
"temp": { "type": "string" },
"tray": {
"type": "array",
"items": { "$ref": "ams_tray.schema.json" }
}
},
"required": ["humidity", "id", "temp", "tray"]
}
@@ -0,0 +1,17 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.octoturge.com/Continuum/continuum-schemas/raw/branch/main/schemas/bambulab/v1/report/print/print.schema.json",
"title": "PrintReport",
"description": "The ongoing print-state push (root key \"print\") — NOT a direct reply to the pushall request (that goes to \"pushing\", see request/pushing/pushall.schema.json). Sending pushall makes the printer start/refresh this stream; it isn't itself request/response, which is also why this is its own category folder rather than sitting under pushing/ — a category here is 'what root key does this arrive under', not 'what triggered it'. Doesn't extend envelope.schema.json: the example seen so far doesn't show sequence_id/command on this one, and it may genuinely not carry them (an async push, not a direct reply) — revisit if a real capture proves otherwise. The real payload has many more fields than shown here (this example was truncated) — add them under `properties` the same way you'd add them to bambu_commands/print.rs.",
"type": "object",
"properties": {
"ams": { "$ref": "ams.schema.json" },
"ams_exist_bits": { "type": "string" },
"insert_flag": { "type": "boolean" },
"power_on_flag": { "type": "boolean" },
"tray_exist_bits": { "type": "string" },
"tray_is_bbl_bits": { "type": "string" },
"xcam": { "$ref": "xcam.schema.json" },
"xcam_status": { "type": "string" }
}
}
@@ -0,0 +1,20 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.octoturge.com/Continuum/continuum-schemas/raw/branch/main/schemas/bambulab/v1/report/print/xcam.schema.json",
"title": "XcamSettings",
"description": "Camera-based AI print monitoring settings (spaghetti detection, first-layer inspection, etc.).",
"type": "object",
"properties": {
"allow_skip_parts": { "type": "boolean" },
"buildplate_marker_detector": { "type": "boolean" },
"first_layer_inspector": { "type": "boolean" },
"halt_print_sensitivity": { "type": "string" },
"print_halt": { "type": "boolean" },
"printing_monitor": { "type": "boolean" },
"spaghetti_detector": { "type": "boolean" }
},
"required": [
"allow_skip_parts", "buildplate_marker_detector", "first_layer_inspector",
"halt_print_sensitivity", "print_halt", "printing_monitor", "spaghetti_detector"
]
}
@@ -1,16 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://continuum.internal/schemas/bambulab/v1/report/pushall.schema.json",
"title": "PushallResponse",
"description": "The real pushall/print report (the 'print' category push) has a lot more fields (ams, xcam, tray_exist_bits, ...) — add them under `properties` here the same way you'd add them to continuum-proxy's src/printer/bambu_commands/pushall.rs. Not required in `required` unless Bambu always sends them.",
"allOf": [
{ "$ref": "../../envelope.schema.json" },
{
"type": "object",
"properties": {
"result": { "type": "string" },
"reason": { "type": "string" }
}
}
]
}
+12 -4
View File
@@ -1,14 +1,22 @@
{ {
"$schema": "https://json-schema.org/draft/2020-12/schema", "$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://continuum.internal/schemas/bambulab/v1/request.schema.json", "$id": "https://git.octoturge.com/Continuum/continuum-schemas/raw/branch/main/schemas/bambulab/v1/request.schema.json",
"title": "BambuRequest", "title": "BambuRequest",
"description": "Matches continuum-proxy's src/printer/bambu_commands::BambuRequest — one JSON key naming the category ('info', 'pushing', ...), the value is that category's request schema.", "description": "Matches continuum-proxy's src/printer/bambu_commands::BambuRequest — one JSON key naming the category ('info', 'pushing', ...). Each category's value is a oneOf, not a single $ref: a category can have more than one possible request shape (e.g. 'info' might grow a second command besides get_version) — oneOf leaves room for that from the start instead of needing a rewrite the day it happens.",
"type": "object", "type": "object",
"minProperties": 1, "minProperties": 1,
"maxProperties": 1, "maxProperties": 1,
"properties": { "properties": {
"info": { "$ref": "request/get_version.schema.json" }, "info": {
"pushing": { "$ref": "request/pushall.schema.json" } "oneOf": [
{ "$ref": "request/info/get_version.schema.json" }
]
},
"pushing": {
"oneOf": [
{ "$ref": "request/pushing/pushall.schema.json" }
]
}
}, },
"additionalProperties": false "additionalProperties": false
} }
@@ -1,9 +1,9 @@
{ {
"$schema": "https://json-schema.org/draft/2020-12/schema", "$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://continuum.internal/schemas/bambulab/v1/request/get_version.schema.json", "$id": "https://git.octoturge.com/Continuum/continuum-schemas/raw/branch/main/schemas/bambulab/v1/request/info/get_version.schema.json",
"title": "GetVersionRequest", "title": "GetVersionRequest",
"allOf": [ "allOf": [
{ "$ref": "../../envelope.schema.json" }, { "$ref": "../../../envelope.schema.json" },
{ {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -1,9 +1,9 @@
{ {
"$schema": "https://json-schema.org/draft/2020-12/schema", "$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://continuum.internal/schemas/bambulab/v1/request/pushall.schema.json", "$id": "https://git.octoturge.com/Continuum/continuum-schemas/raw/branch/main/schemas/bambulab/v1/request/pushing/pushall.schema.json",
"title": "PushallRequest", "title": "PushallRequest",
"allOf": [ "allOf": [
{ "$ref": "../../envelope.schema.json" }, { "$ref": "../../../envelope.schema.json" },
{ {
"type": "object", "type": "object",
"properties": { "properties": {