//! Run with: cargo run --example live_reports //! //! **Read-only. Safe to run against a printer that is mid-print.** //! //! Connects to every Bambu printer in printers.toml, asks each one what //! firmware it's running, then watches its `print.push_status` stream for a //! while. Everything it sends either reads state or asks the printer to //! re-send what it already knows: //! //! - `get_version` - read-only. //! - `pushall` - "re-send the full status object". A request for data, //! not a state change. v1 only; a no-op on v2. //! - `print_status` - reads this process's own in-memory cache, no traffic. //! //! It deliberately sends **no** `gcode_line`, **no** `set_fan_speed`, **no** //! `calibration` (that one starts a physical calibration sequence and would //! wreck a running print), no upload, and nothing that starts or stops a //! job. Keep it that way - the whole point of this example is that it can //! be pointed at a working printer without a second thought. Use //! `bambu_idle_checks` for anything that writes. //! //! The raw payloads are the real output. `PrintReport` models only a small //! fraction of what a real `push_status` carries (see its doc comment), so //! this turns on the `trace`-level raw-payload log in `bambu_mqtt.rs` and //! lets you capture the untouched JSON: //! //! ```sh //! cargo run --example live_reports 2> capture.jsonl //! ``` //! //! Then grow `PrintReport` and `continuum-schemas`' `push_status.schema.json` //! from what's actually in there, rather than from guesses. use std::time::Duration; use continuum_proxy::fleet; use continuum_proxy::printer::{GenericPrinter, PrinterHandle}; use tracing_subscriber::EnvFilter; /// How long to sit and watch each printer's stream before moving on. const WATCH_FOR: Duration = Duration::from_secs(25); /// How often to print a one-line summary of the cached report. const POLL_EVERY: Duration = Duration::from_secs(2); /// A printer that's switched off shouldn't hold the whole run up - the TLS /// handshake inside `connect` has its own 5s timeout, but the MQTT half /// doesn't, so this bounds the lot. const CONNECT_TIMEOUT: Duration = Duration::from_secs(20); #[tokio::main] async fn main() -> anyhow::Result<()> { // Default to trace for the raw-payload target specifically, so a plain // run captures payloads without needing RUST_LOG set by hand. An // explicit RUST_LOG still wins. tracing_subscriber::fmt() .with_env_filter(EnvFilter::try_from_default_env().unwrap_or_else(|_| { EnvFilter::new("info,continuum_proxy::printer::bambu_mqtt=trace") })) .with_writer(std::io::stderr) .init(); let mut printers = fleet::load(std::path::Path::new("printers.toml"))?; // Optional: a printer id to look at just that one. No argument means the // whole fleet. Read-only either way, so there's nothing to guard here. let only: Option = std::env::args().nth(1); for printer in &mut printers { if let Some(id) = &only && &printer.base().id != id { continue; } let PrinterHandle::Bambu(bambu) = printer else { println!("skipping {} - not a Bambu printer", printer.display_name()); continue; }; println!("\n=== {} ===", bambu.display_name()); match tokio::time::timeout(CONNECT_TIMEOUT, bambu.connect()).await { Ok(Ok(())) => {} Ok(Err(err)) => { println!(" connect failed: {err}"); continue; } Err(_) => { println!(" connect timed out after {CONNECT_TIMEOUT:?} - printer off or unreachable?"); continue; } } match bambu.get_version().await { Ok(version) => { println!(" get_version OK: {} modules", version.module.len()); for module in &version.module { println!(" {:<12} sw {:<14} hw {}", module.name, module.sw_ver, module.hw_ver); } } Err(err) => println!(" get_version failed: {err}"), } // v1 needs this to get a full object at all; it's a no-op on v2 (see // `pushall`'s doc comment). Harmless either way. if let Err(err) = bambu.pushall().await { println!(" pushall failed: {err}"); } println!(" watching print.push_status for {WATCH_FOR:?}..."); let deadline = tokio::time::Instant::now() + WATCH_FOR; // Keyed on the *contents*, not on `sequence_id`. v2 leaves // `sequence_id` pinned at one value while the report genuinely // changes underneath it (confirmed live - see `PrintStatusCache`), // so keying on it reports one update for a whole print. let mut last_seen: Option = None; let mut updates = 0u32; while tokio::time::Instant::now() < deadline { tokio::time::sleep(POLL_EVERY).await; match bambu.print_status().await { Some(report) => { let seq = report.envelope.sequence_id.clone(); let fingerprint = format!( "{:?}|{:?}|{:?}|{:?}|{:?}|{:?}", report.gcode_state, report.mc_percent, report.layer_num, report.mc_remaining_time, report.nozzle_temper, report.bed_temper ); let changed = last_seen.as_ref() != Some(&fingerprint); if changed { updates += 1; last_seen = Some(fingerprint); } println!( " seq={seq:<6} {:<8} {:>3}% layer {}/{} {} min left nozzle {:.1}/{:.1}C bed {:.1}/{:.1}C{}", report.gcode_state.as_deref().unwrap_or("?"), report.mc_percent.unwrap_or(0), report.layer_num.unwrap_or(0), report.total_layer_num.unwrap_or(0), report.mc_remaining_time.unwrap_or(0), report.nozzle_temper.unwrap_or(0.0), report.nozzle_target_temper.unwrap_or(0.0), report.bed_temper.unwrap_or(0.0), report.bed_target_temper.unwrap_or(0.0), if changed { "" } else { " (unchanged)" } ); if let Some(job) = report.subtask_name.as_deref().filter(|s| !s.is_empty()) { println!(" job: {job}"); } if let Some(hms) = report.hms.as_ref().filter(|h| !h.is_empty()) { println!(" hms: {} active entr(y/ies)", hms.len()); } println!( " fans: cooling={} heatbreak={} big1={} big2={} wifi={}", report.cooling_fan_speed.as_deref().unwrap_or("?"), report.heatbreak_fan_speed.as_deref().unwrap_or("?"), report.big_fan1_speed.as_deref().unwrap_or("?"), report.big_fan2_speed.as_deref().unwrap_or("?"), report.wifi_signal.as_deref().unwrap_or("?") ); } // Reaching here now means genuinely nothing arrived. It used // to also mean "something arrived and failed to parse", // which is why a mid-print H2C once looked completely // silent - see `handle_incoming_report`. None => println!(" no push_status received yet"), } } println!(" {updates} distinct push_status update(s) in {WATCH_FOR:?}"); } println!("\ndone - raw payloads went to stderr (redirect it to capture them)"); Ok(()) }