Files
continuum-proxy/src/printer/bambu_commands/print.rs
T
octoturge 6ce60542c6 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.
2026-09-07 14:39:55 +00:00

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,
}