Refuse a truncated project, and drive the whole path from the probe

`upload_project_file` already existed - as a `GenericPrinter` trait method,
which is why a grep for `pub async fn` missed it and this commit nearly
reimplemented it. It logged `written` and `byte_count` next to each other and
never compared them, so a short write was something you could only notice by
reading the logs afterwards, and the next line printed the truncated file
anyway. A `.3mf` is a zip: cut the end off and the central directory goes with
it, so the printer gets an archive it cannot open. That is also the failure a
slow transfer invites, and this transfer is slow - 4.96 MB took 31.3s, so a
62 MB project is minutes of exposure.

`print-project <local> [name]` runs that method against a real printer, so
the harness tests the production entry point rather than the three pieces in
sequence. It deliberately takes no `--ams`: the whole point is that the
mapping comes out of the archive. Gated like `start`, on a terminal state and
`--yes-start-a-real-print`.

Confirmed end to end on the P1P (2026-10-15, with the owner's go-ahead), with
the same project the 2026-10-11 run used: the derivation read one PETG/GFG99/
`#2850E0` filament out of `Metadata/slice_info.config` and produced
`mapping=[3]`, padded to five trays; 297,227 bytes went up with an exact byte
count; `IDLE -> PREPARE -> RUNNING`. The AMS on that machine has slot 2 in
black and slot 3 in blue, same product and same `tray_info_idx`, so picking
slot 3 is the colour tie-break doing real work rather than a one-candidate
match.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-10-15 22:32:31 +00:00
co-authored by Claude Opus 5
parent 3d907d0443
commit 790c951cb4
2 changed files with 70 additions and 0 deletions
+54
View File
@@ -16,6 +16,14 @@
//! at all? A clean rejection naming the file means the
//! shape is right and only the target was wrong. A
//! parse error, or silence, means the shape is wrong.
//! - `print-project <local> [name]`
//! **starts a real print.** The whole production path in
//! one call - derive the AMS mapping from the archive,
//! upload, start - via
//! `GenericPrinter::upload_project_file`. Gated on a
//! terminal state and `--yes-start-a-real-print`. Takes
//! no `--ams`: the point is that it reads the mapping
//! itself.
//! - `upload <local> [name]`
//! **inert.** FTPS-transfers a local file onto the card
//! and stops - nothing prints until told to. Reports
@@ -294,6 +302,52 @@ async fn main() -> anyhow::Result<()> {
}
}
// `print-project <local> [name]` is the whole path in one call:
// derive the filament source from the archive, upload, start. Calls
// `GenericPrinter::upload_project_file`, so it tests the production
// entry point rather than the three pieces in sequence.
"print-project" => {
let Some(local) = args.get(3) else {
anyhow::bail!("`print-project` needs a local .3mf path");
};
const READY_TO_START: [&str; 3] = ["IDLE", "FINISH", "FAILED"];
if !READY_TO_START.contains(&state.as_str()) {
anyhow::bail!(
"printer reports '{state}', which is not one of {READY_TO_START:?} - \
refusing to start a print on a machine that may already be working"
);
}
if !confirmed {
anyhow::bail!(
"refusing to start a real print without --yes-start-a-real-print.\n\
This uploads a project and commits the machine to it. Be at the printer."
);
}
let data = std::fs::read(local)?;
let name = args
.get(4)
.filter(|a| !a.starts_with("--"))
.cloned()
.unwrap_or_else(|| format!("continuum-probe-{}", file_stem(local)));
println!("\n upload_project_file({name}, {} bytes):", data.len());
println!(" no --ams flag: the mapping comes out of the archive, not this harness");
let started = std::time::Instant::now();
match bambu.upload_project_file(name.clone(), data).await {
Ok(()) => println!(" accepted after {:?} - watching", started.elapsed()),
Err(err) => {
// The interesting failures are here: an unmappable
// project should fail *before* the upload, leaving
// nothing on the card.
println!(" FAILED after {:?}: {err}", started.elapsed());
println!(" if that was the mapping, the card should be untouched - check with `status`");
return Ok(());
}
}
watch(bambu, state).await;
println!("\n the file is still on the card as {name} - `delete` it when done");
return Ok(());
}
// `upload <local> [name]` puts a file on the card and stops there.
// Inert: a file on the SD card does nothing until something is told
// to print it. The only proven transfer before this was 102 bytes,