Files
continuum-proxy/examples/bambu_cache_list.rs
T
octoturge 0077e15490 Record what a real sliced project did, and widen the start guard
A second live run, this time against a project the owner sliced in Bambu
Studio rather than a file already on the card, and through
`start_uploaded_file` itself rather than a hand-built body - so the
root-vs-cache fix is confirmed end to end. `gcode_file` in the printer's own
status came back as `cube-test-2026-10-11.gcode.3mf`, 150 layers, print_type
`local`.

It also found a defect the first run could not. The print heated to 250C,
reached 1%, and then the printer paused itself: `print_error: 50364420`
(`0x03008004`), `hms: []`, `tray_now: 255`. 255 is the external spool, which
is exactly what the hardcoded `use_ams: false` selects - so that pair
overrode the slicer's own filament choice and pointed an AMS-sliced project
at an external spool. The first run looked fine only because it was stopped
during PREPARE, before filament mattered. The doc comment said that pair was
"accepted, and the print ran"; it now says what it actually does, because
silently changing which filament a print uses is worse than not supporting
AMS at all.

The `start` guard demanded IDLE, which meant the harness refused every print
following a cancelled one - a Bambu reports an operator cancellation as
FAILED. IDLE, FINISH and FAILED are all terminal; PREPARE, RUNNING, PAUSE and
SLICING mean the machine is committed, and anything unrecognised is still
refused rather than assumed idle.

`bambu_cache_list` now prints nozzle/bed temperatures and remaining time.
State plus layer number cannot tell a print that is working from one that is
stalled: a P1P sits at layer 0 for minutes during bed levelling and purge,
and the only thing moving in that window is the heat.
2026-10-11 17:55:01 +00:00

106 lines
4.4 KiB
Rust

//! Run with: cargo run --example bambu_cache_list -- <printer-id> [dir]
//!
//! `dir` defaults to `cache`; pass `/` to see the SD card root, which is
//! how to find out what a file's real absolute path is.
//!
//! **Read-only.** Connects, reads one `print.push_status`, and lists the
//! printer's `cache/` directory over FTPS. It sends no commands that change
//! anything - the `pushall` it issues is a request for a status report, which
//! is the one write-shaped thing a read-only tool has to do on a v1 printer
//! to learn anything at all.
//!
//! The point of it: `start_uploaded_file` can only print a file that is
//! already in `cache/`, so this answers "is there anything there, and what is
//! it called?" before any print command is sent. It is also how to find a
//! real slicer-produced `.3mf` to test that command against, which is worth
//! a great deal more than a hand-built one - if a hand-built file fails there
//! is no way to tell whether the command was wrong or the file was.
use std::time::Duration;
use continuum_proxy::fleet;
use continuum_proxy::printer::{GenericPrinter, PrinterHandle};
const CONNECT_TIMEOUT: Duration = Duration::from_secs(20);
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_cache_list -- <printer-id> [dir]");
};
let dir = std::env::args().nth(2).unwrap_or_else(|| "cache".to_string());
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?"),
}
bambu.pushall().await.ok();
let deadline = tokio::time::Instant::now() + STATUS_TIMEOUT;
loop {
if let Some(report) = bambu.print_status().await {
if let Some(state) = report.gcode_state.clone() {
println!(" gcode_state: {state}");
println!(" subtask_name: {}", report.subtask_name.clone().unwrap_or_default());
println!(
" layer: {}/{}",
report.layer_num.unwrap_or_default(),
report.total_layer_num.unwrap_or_default()
);
println!(" percent: {}", report.mc_percent.unwrap_or_default());
// Temperatures and remaining time, because state plus layer
// number cannot tell a print that is working from one that is
// stalled: a P1P sits at layer 0 for minutes during bed
// levelling and purge, and the only thing moving in that
// window is the heat.
println!(
" nozzle: {:.1} -> {:.1} C",
report.nozzle_temper.unwrap_or_default(),
report.nozzle_target_temper.unwrap_or_default()
);
println!(
" bed: {:.1} -> {:.1} C",
report.bed_temper.unwrap_or_default(),
report.bed_target_temper.unwrap_or_default()
);
println!(" remaining: {} min", report.mc_remaining_time.unwrap_or_default());
break;
}
}
if tokio::time::Instant::now() >= deadline {
println!(" (no gcode_state within {STATUS_TIMEOUT:?} - listing cache/ anyway)");
break;
}
tokio::time::sleep(Duration::from_millis(500)).await;
}
println!("\n {dir} over FTPS:");
match bambu.list_dir(&dir).await {
Ok(entries) if entries.is_empty() => println!(" (empty)"),
Ok(entries) => {
for entry in &entries {
println!(" {entry}");
}
println!("\n {} entr(ies)", entries.len());
}
Err(err) => println!(" FAILED: {err}"),
}
Ok(())
}