Files
octoturge 13bfec97fe Clear the clippy warnings, except the two that are not cosmetic
Down from 29 warnings to 2, in the spirit of 4ab95ff.

The doc warnings were all one mistake made repeatedly: a paragraph wrapped so
that a line *began* with `- ` or `+ `, which rustdoc reads as a markdown list
marker and the following lines as unindented continuations. The prose always
meant an em-dash. Fixed by moving the dash to the end of the previous line and
re-wrapping; not one word changed, which is the point - these comments record
things confirmed against real printers and are worth more than the warning.
The genuine lists just had their continuations de-indented and stayed lists.

The code ones are `match x { V(v) => .., _ => {} }` to `if let`, and nested
`if let Some(x) = .. { if cond { .. } }` to a let-chain. None had an `else`,
so each is the same control flow written once instead of twice.

Two warnings are left deliberately, because fixing them is not cosmetic:
`AmsTray` (544 bytes against 32) and `BambuReport` (984 against 120) both have
one variant far larger than the rest. The fix clippy wants is a `Box`, which
changes how every reader of those enums is written and puts an allocation on
the push_status path that currently has none. Worth measuring before doing,
not worth doing to silence a warning.

51 tests still pass.
2026-10-15 21:43:06 +00:00

179 lines
7.9 KiB
Rust

//! 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<String> = 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<String> = 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(())
}