From 1583f531faa384bdb962f3ddb80019576bcc8f20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iwo=20Strzebo=C5=84ski?= Date: Fri, 28 Aug 2026 23:19:20 +0000 Subject: [PATCH] Add print.rs (AmsState/AmsUnit/AmsTray/XcamSettings), fix print vs pushing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 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. --- examples/bambu_commands_demo.rs | 24 ++++++- src/printer/bambu_commands/mod.rs | 34 ++++++---- src/printer/bambu_commands/print.rs | 94 +++++++++++++++++++++++++++ src/printer/bambu_commands/pushall.rs | 14 ++-- src/printer/mod.rs | 3 +- 5 files changed, 144 insertions(+), 25 deletions(-) create mode 100644 src/printer/bambu_commands/print.rs diff --git a/examples/bambu_commands_demo.rs b/examples/bambu_commands_demo.rs index 791e8d1..b9fcd28 100644 --- a/examples/bambu_commands_demo.rs +++ b/examples/bambu_commands_demo.rs @@ -5,7 +5,7 @@ //! matches byte for byte, same as the proto-based version did, with none //! of the build.rs ceremony. -use continuum_proxy::printer::{CommandEnvelope, GetVersionRequest, GetVersionResponse, PushallRequest}; +use continuum_proxy::printer::{CommandEnvelope, GetVersionRequest, GetVersionResponse, PrintReport, PushallRequest}; fn main() { // --- get_version request --- @@ -31,5 +31,25 @@ fn main() { let actual = serde_json::to_string(&pushall).unwrap(); let expected = r#"{"sequence_id":"0","command":"pushall","version":1,"push_target":1}"#; println!("pushall request: {actual}"); - println!("matches Bambu's shape: {}", actual == expected); + println!("matches Bambu's shape: {}\n", actual == expected); + + // --- print report: arrives under "print", NOT "pushing" (pushall + // triggers this stream, it isn't a direct reply to it) — includes the + // full ams/xcam example, with one empty tray and one loaded tray, to + // exercise the untagged Empty/Loaded split. + let real_print_report = r#"{ + "ams": {"ams": [{"humidity":"4","id":"0","temp":"22.7","tray":[ + {"id":"0"}, + {"bed_temp":"0","bed_temp_type":"0","cols":["000000FF"],"drying_temp":"0","drying_time":"0","id":"1","nozzle_temp_max":"240","nozzle_temp_min":"190","remain":0,"tag_uid":"0000000000000000","tray_color":"000000FF","tray_diameter":"0.00","tray_id_name":"","tray_info_idx":"GFA00","tray_sub_brands":"","tray_type":"PLA","tray_uuid":"00000000000000000000000000000000","tray_weight":"0","xcam_info":"000000000000000000000000"} + ]}]}, + "ams_exist_bits": "1", + "insert_flag": true, + "power_on_flag": false, + "tray_exist_bits": "e", + "tray_is_bbl_bits": "e", + "xcam": {"allow_skip_parts":false,"buildplate_marker_detector":false,"first_layer_inspector":true,"halt_print_sensitivity":"medium","print_halt":true,"printing_monitor":true,"spaghetti_detector":true}, + "xcam_status": "0" + }"#; + let parsed: PrintReport = serde_json::from_str(real_print_report).unwrap(); + println!("print report parsed fine, empty + loaded tray both matched correctly:\n{parsed:#?}"); } diff --git a/src/printer/bambu_commands/mod.rs b/src/printer/bambu_commands/mod.rs index 3041892..b81e0ee 100644 --- a/src/printer/bambu_commands/mod.rs +++ b/src/printer/bambu_commands/mod.rs @@ -7,15 +7,26 @@ //! continuum-schemas for continuum.v1, which is a schema we designed //! ourselves and that both TS and Rust genuinely need. //! -//! One file per command (`get_version.rs`, `pushall.rs`, ...) — each holds -//! its request AND response together, since they're conceptually one unit. -//! Copy `get_version.rs`'s shape for your next command. +//! One file per Bambu *category* (`get_version.rs` for "info", `pushall.rs` +//! + `print.rs` for "pushing"/"print" — see below for why those are two +//! files, not one). Copy `get_version.rs`'s shape for your next command. +//! +//! Mirrors continuum-schemas' `schemas/bambulab/` JSON Schemas — same +//! category split, same "print vs pushing" correction (see that +//! directory's README for the full explanation): a `pushall` *request* +//! goes to the "pushing" key, but the ongoing state-push stream it +//! triggers arrives under a different key, "print" — so `PushallRequest` +//! lives in `pushall.rs` and the thing it triggers, `PrintReport`, gets +//! its own file, `print.rs`, rather than living together as if one were +//! simply "the response to" the other. mod get_version; +mod print; mod pushall; pub use get_version::{GetVersionRequest, GetVersionResponse, ModuleInfo}; -pub use pushall::{PushallRequest, PushallResponse}; +pub use print::{AmsState, AmsTray, AmsUnit, EmptyTray, LoadedTray, PrintReport, XcamSettings}; +pub use pushall::PushallRequest; /// Fields every Bambu command/response shares. This is the "inheritance" /// replacement for these structs — composition, not extension: every @@ -34,12 +45,10 @@ pub struct CommandEnvelope { /// One request you can send to a Bambu printer, keyed by Bambu's top-level /// JSON category ("info", "pushing", ...). NOTE: that top-level key is a /// *category*, not the specific command — `CommandEnvelope::command` is -/// what actually says which one ("get_version", "pushall", ...). Right -/// now each category maps to exactly one known command, one-to-one. If a -/// category ever needs a second command (e.g. "info" gaining something -/// besides get_version), that variant's payload becomes its own small -/// `#[serde(tag = "command")]` enum instead of a single struct — ask if -/// you get there and want a worked example. +/// what actually says which one ("get_version", "pushall", ...). A +/// category can have more than one possible request shape — if that ever +/// happens here, that variant's payload becomes its own small +/// `#[serde(tag = "command")]` enum instead of a single struct. #[derive(serde::Serialize, Debug)] #[serde(rename_all = "snake_case")] pub enum BambuRequest { @@ -47,10 +56,11 @@ pub enum BambuRequest { Pushing(PushallRequest), } -/// Same idea, the response direction. +/// Same idea, the response direction — note "print", not "pushing" (see +/// this module's doc comment). #[derive(serde::Deserialize, Debug)] #[serde(rename_all = "snake_case")] pub enum BambuReport { Info(GetVersionResponse), - Pushing(PushallResponse), + Print(PrintReport), } diff --git a/src/printer/bambu_commands/print.rs b/src/printer/bambu_commands/print.rs new file mode 100644 index 0000000..f12d483 --- /dev/null +++ b/src/printer/bambu_commands/print.rs @@ -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, + pub ams_exist_bits: Option, + pub insert_flag: Option, + pub power_on_flag: Option, + pub tray_exist_bits: Option, + pub tray_is_bbl_bits: Option, + pub xcam: Option, + pub xcam_status: Option, + // 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` 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, +} + +/// 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, +} + +/// 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`. +/// `#[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, + 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, +} diff --git a/src/printer/bambu_commands/pushall.rs b/src/printer/bambu_commands/pushall.rs index 7c25a1b..24ec505 100644 --- a/src/printer/bambu_commands/pushall.rs +++ b/src/printer/bambu_commands/pushall.rs @@ -8,13 +8,7 @@ pub struct PushallRequest { pub push_target: i32, } -#[derive(serde::Deserialize, Debug)] -pub struct PushallResponse { - #[serde(flatten)] - pub envelope: CommandEnvelope, - pub result: Option, - pub reason: Option, - // The real pushall/print report has a lot more fields (ams, xcam, - // tray_exist_bits, ...) — add them here the same way, one field per - // line, `Option` for anything that isn't always present. -} +// No PushallResponse here — sending this request doesn't get a direct +// reply. It makes the printer start/refresh the ongoing print-state push, +// which arrives under a different root key ("print", not "pushing") — see +// print.rs for that. diff --git a/src/printer/mod.rs b/src/printer/mod.rs index 142a21f..6a70ee1 100644 --- a/src/printer/mod.rs +++ b/src/printer/mod.rs @@ -35,7 +35,8 @@ mod prusa; pub use bambu::{BambuGenericPrinter, BambuTls, BambuV1Printer, BambuV2Printer}; pub use bambu_commands::{ - BambuReport, BambuRequest, CommandEnvelope, GetVersionRequest, GetVersionResponse, PushallRequest, PushallResponse, + AmsState, AmsTray, AmsUnit, BambuReport, BambuRequest, CommandEnvelope, EmptyTray, GetVersionRequest, + GetVersionResponse, LoadedTray, PrintReport, PushallRequest, XcamSettings, }; pub use klipper::KlipperPrinter; pub use prusa::{PrusaLinkPrinter, PrusaSerialPrinter};