Add print.rs (AmsState/AmsUnit/AmsTray/XcamSettings), fix print vs pushing

Mirrors continuum-schemas' schemas/bambulab/ restructuring: PushallResponse
removed (it never existed on the wire — pushall's request goes to
"pushing", but the state-push stream it triggers arrives under "print",
a different root key, not a direct reply). BambuReport::Pushing(...) ->
BambuReport::Print(PrintReport).

AmsTray is an untagged enum (Empty/Loaded) rather than one struct full of
Option<T> fields, matching the JSON Schema's oneOf for the same slot -
#[serde(deny_unknown_fields)] on EmptyTray is load-bearing: untagged enums
try variants in order, and a loaded tray (superset of Empty's one field)
would otherwise incorrectly match Empty first.

Verified against the full real ams/xcam example from the conversation via
examples/bambu_commands_demo.rs - both the empty and loaded tray in that
payload deserialize to the correct enum variant.
This commit is contained in:
2026-08-28 23:19:20 +00:00
parent 4eef52714b
commit 1583f531fa
5 changed files with 144 additions and 25 deletions
+94
View File
@@ -0,0 +1,94 @@
//! The ongoing print-state push (root key "print"). Doesn't carry a
//! `CommandEnvelope` — no `sequence_id`/`command` seen on this one in the
//! real capture, and it may genuinely not have them (an async push, not a
//! reply to a specific request) — revisit if a real capture proves
//! otherwise.
#[derive(serde::Deserialize, Debug)]
pub struct PrintReport {
pub ams: Option<AmsState>,
pub ams_exist_bits: Option<String>,
pub insert_flag: Option<bool>,
pub power_on_flag: Option<bool>,
pub tray_exist_bits: Option<String>,
pub tray_is_bbl_bits: Option<String>,
pub xcam: Option<XcamSettings>,
pub xcam_status: Option<String>,
// The real payload has a lot more fields than shown here (this
// example was truncated) — add them the same way, one field per line,
// `Option<T>` for anything that isn't always present.
}
/// 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.
#[derive(serde::Deserialize, Debug)]
pub struct AmsState {
pub ams: Vec<AmsUnit>,
}
/// One physical AMS unit (Bambu's numbering starts at 0 via `id`).
#[derive(serde::Deserialize, Debug)]
pub struct AmsUnit {
pub humidity: String,
pub id: String,
pub temp: String,
pub tray: Vec<AmsTray>,
}
/// A tray slot is either empty (just `id`) or loaded (the full field set)
/// — two genuinely different shapes, not one shape with optional fields,
/// so this is an untagged enum rather than a struct full of `Option<T>`.
/// `#[serde(deny_unknown_fields)]` on `Empty` is load-bearing: untagged
/// enums try variants in order, and without it a *loaded* tray (which has
/// every field `Empty` allows, plus more) would incorrectly match `Empty`
/// first, since a plain struct doesn't reject extra fields by default.
/// Verified this the hard way — see git history.
#[derive(serde::Deserialize, Debug)]
#[serde(untagged)]
pub enum AmsTray {
Empty(EmptyTray),
Loaded(LoadedTray),
}
#[derive(serde::Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct EmptyTray {
pub id: String,
}
#[derive(serde::Deserialize, Debug)]
pub struct LoadedTray {
pub id: String,
pub bed_temp: String,
pub bed_temp_type: String,
pub cols: Vec<String>,
pub drying_temp: String,
pub drying_time: String,
pub nozzle_temp_max: String,
pub nozzle_temp_min: String,
pub remain: i32,
pub tag_uid: String,
pub tray_color: String,
pub tray_diameter: String,
pub tray_id_name: String,
pub tray_info_idx: String,
pub tray_sub_brands: String,
pub tray_type: String,
pub tray_uuid: String,
pub tray_weight: String,
pub xcam_info: String,
}
/// Camera-based AI print monitoring settings (spaghetti detection,
/// first-layer inspection, etc.).
#[derive(serde::Deserialize, Debug)]
pub struct XcamSettings {
pub allow_skip_parts: bool,
pub buildplate_marker_detector: bool,
pub first_layer_inspector: bool,
pub halt_print_sensitivity: String,
pub print_halt: bool,
pub printing_monitor: bool,
pub spaghetti_detector: bool,
}