Files
continuum-proxy/examples/bambu_idle_checks.rs
T
octoturgeandClaude Opus 5 6766ebbe80 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.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-15 21:43:06 +00:00

141 lines
5.8 KiB
Rust

//! Run with: cargo run --example bambu_idle_checks -- <printer-id>
//!
//! **This one writes.** Unlike `live_reports`, it sends commands that change
//! the printer: an FTPS file upload and a fan-speed change. So it does two
//! things before touching anything:
//!
//! 1. Requires an explicit printer id from the command line. No "do this to
//! every printer in the fleet" mode - that's not a thing you want to
//! discover you ran.
//! 2. Refuses to continue unless that printer reports `gcode_state: "IDLE"`.
//! It connects, waits for a real `print.push_status`, and bails out if the
//! printer turns out to be mid-job. Sending a fan command to a running
//! print would corrupt it.
//!
//! What it deliberately does **not** do, even on an idle printer:
//!
//! - **Start a print.** The upload here goes through `upload_file`, which is
//! FTPS only. `upload_project_file` (upload *then* print) and
//! `start_uploaded_file` are left alone - committing a machine to hours of
//! physical work needs a human deciding to.
//! - **Calibrate.** `get_calibration` runs a real multi-minute bed-levelling
//! / vibration-compensation sequence. Available, intentionally not wired
//! in here.
//! - **Heat anything.** No temperature commands.
//!
//! The fan is set and then put back to 0 before exiting.
use std::time::Duration;
use continuum_proxy::fleet;
use continuum_proxy::printer::{GenericPrinter, PrinterHandle};
const CONNECT_TIMEOUT: Duration = Duration::from_secs(20);
/// How long to wait for the first `push_status` before giving up on knowing
/// whether the printer is idle. v2 pushes within about a second; v1 needs the
/// `pushall` this example sends first.
const STATUS_TIMEOUT: Duration = Duration::from_secs(15);
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let Some(target) = std::env::args().nth(1) else {
anyhow::bail!(
"usage: cargo run --example bambu_idle_checks -- <printer-id>\n\
(the id from printers.toml - required on purpose, this example writes to the printer)"
);
};
let mut printers = fleet::load(std::path::Path::new("printers.toml"))?;
let printer = printers
.iter_mut()
.find(|p| p.base().id == target)
.ok_or_else(|| anyhow::anyhow!("no printer with id '{target}' in printers.toml"))?;
let PrinterHandle::Bambu(bambu) = printer else {
anyhow::bail!("'{target}' is not a Bambu printer - this example is Bambu-specific");
};
println!("=== {} ===", bambu.display_name());
match tokio::time::timeout(CONNECT_TIMEOUT, bambu.connect()).await {
Ok(Ok(())) => {}
Ok(Err(err)) => anyhow::bail!("connect failed: {err}"),
Err(_) => anyhow::bail!("connect timed out - printer off or unreachable?"),
}
// --- Safety gate: is it actually idle? ---
// v1 only sends a full object after a pushall; v2 sends one unprompted.
bambu.pushall().await.ok();
let deadline = tokio::time::Instant::now() + STATUS_TIMEOUT;
let state = loop {
if let Some(report) = bambu.print_status().await
&& let Some(state) = report.gcode_state.clone()
{
break state;
}
if tokio::time::Instant::now() >= deadline {
anyhow::bail!(
"no print.push_status with a gcode_state within {STATUS_TIMEOUT:?} - refusing to \
send anything, because there's no way to tell whether this printer is busy"
);
}
tokio::time::sleep(Duration::from_millis(500)).await;
};
println!(" gcode_state: {state}");
if state != "IDLE" {
anyhow::bail!(
"printer reports '{state}', not IDLE - refusing to send write commands to a printer \
that may be mid-job. Use the `live_reports` example instead, it's read-only."
);
}
println!(" idle - safe to continue\n");
// --- 1. FTPS upload (no print start) ---
// The real point of this example: the FTPS path in bambu_ftps.rs had
// never touched hardware. A tiny text file is enough to prove login,
// binary mode, cwd into cache/, and the transfer itself.
let file_name = "continuum-ftps-check.txt";
let body = format!(
"written by continuum-proxy bambu_idle_checks at {:?}\n",
std::time::SystemTime::now()
);
let expected = body.len() as u64;
println!(" uploading {file_name} ({expected} bytes) over FTPS to cache/ ...");
match bambu.upload_file(file_name, body.into_bytes()).await {
Ok(written) if written == expected => println!(" OK - {written} bytes, matches what we sent"),
Ok(written) => println!(" PARTIAL - printer accepted {written} of {expected} bytes"),
Err(err) => println!(" FAILED: {err}"),
}
// --- 2. Fan speed (an MQTT gcode_line publish under the hood) ---
// Audible and visible on the machine, and harmless on an idle printer.
println!("\n setting part-cooling fan to 50% ...");
match bambu.set_fan_speed(50).await {
Ok(()) => println!(" sent (M106 P1 S127) - listen for the fan"),
Err(err) => println!(" FAILED: {err}"),
}
tokio::time::sleep(Duration::from_secs(5)).await;
println!(" putting the fan back to 0% ...");
match bambu.set_fan_speed(0).await {
Ok(()) => println!(" sent"),
Err(err) => println!(" FAILED: {err}"),
}
// Give the printer a moment to report the change back, so the fan
// reading below reflects the command rather than racing it.
tokio::time::sleep(Duration::from_secs(3)).await;
if let Some(report) = bambu.print_status().await {
println!(
"\n cooling_fan_speed now reads: {:?}",
report.cooling_fan_speed.as_deref().unwrap_or("?")
);
}
println!("\ndone - nothing was printed, calibrated, or heated");
Ok(())
}