PrintReport now flattens CommandEnvelope (sequence_id/command) - push_status.schema.json composes the shared envelope like every other Bambu message; the struct's old doc comment concluded otherwise from a capture that turned out to be an incomplete example (also missing dozens of other real fields), not proof the message lacks them. Added BambuMqttSession's PrintStatusCache: the in-memory print.push_status object, replaced wholesale on v2 (always full) or on a v1 message with msg != 1 (forced/full), merged field-by-field otherwise (v1's organic partial updates) via PrintReport::merge_from. Tracks last_sequence_id to warn on a gap in the organic stream. BambuPrinter gets pushall() (v1-only; pushing.pushall doesn't exist in v2's schema at all) and print_status(). Three real bugs found and fixed via live testing against an actual P1P, none of them guessable from the schema alone: - pushall's reply doesn't echo the sequence_id we send - the printer stamps its own free-running counter regardless of what triggered it. request()'s ID correlation silently can't work here; first fix attempt added request_next_of_category (wait for the next message under a category, no ID needed). - That undershot too: matching by category alone let an unrelated "print" message (that category carries many commands - gcode_line, pause, push_status, ...) satisfy the wait before the real push_status reply arrived, so pushall() returned Ok having consumed the wrong message - now keyed by (category, command). - A real ordering race: handle_incoming_report woke a waiting oneshot before updating the print_status cache, so a caller could resume and check print_status() before the write landed - pushall() returned Ok immediately followed by print_status() still showing None. Fixed by applying the cache update before sending on any matched oneshot. Superseded by real protocol knowledge before any of the above needed to ship, though: push_status carries its own msg field (1 = automatic/ partial, 0 = forced/full) - the actual, reliable, self-describing signal for which kind a message is, regardless of timing or which client (if any) triggered it. PrintStatusCache now reads that directly instead of inferring it from request correlation, and pushall() is plain fire-and- forget (publish, no wait) since there's nothing left to correlate for - confirmed live: msg reads correctly off a real P1P, and the cache populates/updates correctly with no gap warnings across several organic pushes. request_next_of_category is kept (a future case may still need "no correlation ID at all" for real), just no longer used here. Also: the poll loop now logs when it exits instead of returning silently - that silence is exactly what made a real dropped MQTT connection to a live H2C indistinguishable from "nothing happening" during testing.
151 lines
5.6 KiB
Rust
151 lines
5.6 KiB
Rust
//! The ongoing print-state push (root key "print"). The one real capture
|
|
//! this struct was originally built from didn't show `sequence_id`/
|
|
//! `command` - turned out to be because that capture was itself a
|
|
//! truncated example (it's also missing dozens of other fields the real
|
|
//! schema defines), not because the message genuinely lacks them:
|
|
//! `push_status.schema.json` composes the shared envelope schema like
|
|
//! every other Bambu message, so it does carry `sequence_id` - which
|
|
//! matters a lot, see `BambuMqttSession`'s doc comment on how it's used
|
|
//! both to correlate a `pushall` reply and to detect a gap in the
|
|
//! ongoing organic stream.
|
|
use super::CommandEnvelope;
|
|
|
|
#[derive(serde::Deserialize, Debug, Clone)]
|
|
pub struct PrintReport {
|
|
#[serde(flatten)]
|
|
pub envelope: CommandEnvelope,
|
|
/// BambuMQTT v1 only: `1` on an automatic/organic push (a partial
|
|
/// update - only the fields that changed), `0` on a forced one (a
|
|
/// `pushing.pushall`-triggered reply - the full object). This is the
|
|
/// actual, reliable signal for which kind a given message is -
|
|
/// confirmed against a real printer, unlike trying to infer it from
|
|
/// request/reply timing (see `BambuMqttSession`'s `PrintStatusCache`,
|
|
/// which used to try that and got it wrong twice). Not in
|
|
/// `push_status.schema.json` yet - add it there too when you're back
|
|
/// in continuum-schemas.
|
|
pub msg: Option<i32>,
|
|
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. Remember to
|
|
// also add each new field to `merge_from` below.
|
|
}
|
|
|
|
impl PrintReport {
|
|
/// Merges `newer`'s fields into `self`, in place - BambuMQTT v1's
|
|
/// organic `push_status` pushes only carry the fields that changed,
|
|
/// so any field `newer` doesn't have (`None`) leaves `self`'s
|
|
/// existing value alone instead of clobbering it. Not used for v2
|
|
/// (always a full object) or for a `pushall`-triggered reply on
|
|
/// either version (also guaranteed full) - see `BambuMqttSession`.
|
|
pub fn merge_from(&mut self, newer: PrintReport) {
|
|
self.envelope = newer.envelope;
|
|
self.msg = newer.msg;
|
|
if newer.ams.is_some() {
|
|
self.ams = newer.ams;
|
|
}
|
|
if newer.ams_exist_bits.is_some() {
|
|
self.ams_exist_bits = newer.ams_exist_bits;
|
|
}
|
|
if newer.insert_flag.is_some() {
|
|
self.insert_flag = newer.insert_flag;
|
|
}
|
|
if newer.power_on_flag.is_some() {
|
|
self.power_on_flag = newer.power_on_flag;
|
|
}
|
|
if newer.tray_exist_bits.is_some() {
|
|
self.tray_exist_bits = newer.tray_exist_bits;
|
|
}
|
|
if newer.tray_is_bbl_bits.is_some() {
|
|
self.tray_is_bbl_bits = newer.tray_is_bbl_bits;
|
|
}
|
|
if newer.xcam.is_some() {
|
|
self.xcam = newer.xcam;
|
|
}
|
|
if newer.xcam_status.is_some() {
|
|
self.xcam_status = newer.xcam_status;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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, Clone)]
|
|
pub struct AmsState {
|
|
pub ams: Vec<AmsUnit>,
|
|
}
|
|
|
|
/// One physical AMS unit (Bambu's numbering starts at 0 via `id`).
|
|
#[derive(serde::Deserialize, Debug, Clone)]
|
|
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, Clone)]
|
|
#[serde(untagged)]
|
|
pub enum AmsTray {
|
|
Empty(EmptyTray),
|
|
Loaded(LoadedTray),
|
|
}
|
|
|
|
#[derive(serde::Deserialize, Debug, Clone)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct EmptyTray {
|
|
pub id: String,
|
|
}
|
|
|
|
#[derive(serde::Deserialize, Debug, Clone)]
|
|
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, Clone)]
|
|
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,
|
|
}
|