//! Run with: cargo run --example bambu_idle_checks -- //! //! **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 -- \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(()) }