Add pushall + print_status cache; fix envelope, race, and matching bugs
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. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MWQVtBGxHYLkVw2kBupd5g
This commit is contained in:
@@ -1,11 +1,29 @@
|
||||
//! 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.
|
||||
//! 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)]
|
||||
#[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>,
|
||||
@@ -16,19 +34,57 @@ pub struct PrintReport {
|
||||
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.
|
||||
// `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)]
|
||||
#[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)]
|
||||
#[derive(serde::Deserialize, Debug, Clone)]
|
||||
pub struct AmsUnit {
|
||||
pub humidity: String,
|
||||
pub id: String,
|
||||
@@ -44,20 +100,20 @@ pub struct AmsUnit {
|
||||
/// 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)]
|
||||
#[derive(serde::Deserialize, Debug, Clone)]
|
||||
#[serde(untagged)]
|
||||
pub enum AmsTray {
|
||||
Empty(EmptyTray),
|
||||
Loaded(LoadedTray),
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Debug)]
|
||||
#[derive(serde::Deserialize, Debug, Clone)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct EmptyTray {
|
||||
pub id: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Debug)]
|
||||
#[derive(serde::Deserialize, Debug, Clone)]
|
||||
pub struct LoadedTray {
|
||||
pub id: String,
|
||||
pub bed_temp: String,
|
||||
@@ -82,7 +138,7 @@ pub struct LoadedTray {
|
||||
|
||||
/// Camera-based AI print monitoring settings (spaghetti detection,
|
||||
/// first-layer inspection, etc.).
|
||||
#[derive(serde::Deserialize, Debug)]
|
||||
#[derive(serde::Deserialize, Debug, Clone)]
|
||||
pub struct XcamSettings {
|
||||
pub allow_skip_parts: bool,
|
||||
pub buildplate_marker_detector: bool,
|
||||
|
||||
Reference in New Issue
Block a user