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.
This commit is contained in:
2026-10-15 22:32:31 +00:00
parent 7d4cf41999
commit df6dfca3ca
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 //! at all? A clean rejection naming the file means the
//! shape is right and only the target was wrong. A //! shape is right and only the target was wrong. A
//! parse error, or silence, means the shape is wrong. //! 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]` //! - `upload <local> [name]`
//! **inert.** FTPS-transfers a local file onto the card //! **inert.** FTPS-transfers a local file onto the card
//! and stops - nothing prints until told to. Reports //! 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. // `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 // 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, // to print it. The only proven transfer before this was 102 bytes,
+16
View File
@@ -1348,6 +1348,22 @@ impl GenericPrinter for BambuPrinter {
"uploaded a project file to the SD card root" "uploaded a project file to the SD card root"
); );
// These two were logged next to each other and never compared, which
// made a short write something you could only notice by reading the
// logs afterwards - and the next line would have 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 rather than a partial model. It is also the failure a
// slow transfer invites, and this one is slow: 4.96 MB took 31.3s on
// a P1P (2026-10-15), so a 62 MB project is minutes of exposure.
if written != byte_count as u64 {
return Err(PrinterError(format!(
"{}: uploaded {written} bytes of {byte_count} - refusing to print a \
truncated project",
self.base.name
)));
}
self.start_uploaded_file(&file_name, &filament).await self.start_uploaded_file(&file_name, &filament).await
} }