From 790c951cb458432f7f08bf6a3d8240db091e033f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iwo=20Strzebo=C5=84ski?= Date: Thu, 15 Oct 2026 22:32:31 +0000 Subject: [PATCH] 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 [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 --- examples/bambu_print_probe.rs | 54 +++++++++++++++++++++++++++++++++++ src/printer/bambu.rs | 16 +++++++++++ 2 files changed, 70 insertions(+) diff --git a/examples/bambu_print_probe.rs b/examples/bambu_print_probe.rs index dc68838..cef406e 100644 --- a/examples/bambu_print_probe.rs +++ b/examples/bambu_print_probe.rs @@ -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 [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 [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 [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 [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, diff --git a/src/printer/bambu.rs b/src/printer/bambu.rs index cf08bb8..e6fb4ad 100644 --- a/src/printer/bambu.rs +++ b/src/printer/bambu.rs @@ -1348,6 +1348,22 @@ impl GenericPrinter for BambuPrinter { "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 }