Confirm the print path on a real P1P, and stop discarding its answer

Print control (pause/resume/stop) and print start are now both confirmed
against the P1P on the LAN, and doing that turned up three real defects.

`start_uploaded_file` published fire-and-forget, so the printer's reply was
thrown away. Asking it to print a file it does not have returned `Ok(())`,
and watching `print.push_status` could not tell that apart from a successful
start, since both leave the machine IDLE for a while. The printer does in
fact answer - `{"command":"gcode_file","param":"...","reason":"error
string","result":"fail"}` - and it echoes the `sequence_id` we send, so
`request` correlates it. Both this and `control_print` now go out as
correlated requests and run the reply through `print_command_outcome`.

The upload and the print command disagreed about where files live. `upload`
cwd'd into `cache/` while the printable projects are in the card's root: the
root holds 43 `<name>.gcode.3mf` archives, `cache/` holds the printer's own
extracted `<project>_plate_N.gcode` working copies with `.gcode.bbl`
sidecars. Those extracted copies are not independently printable -
`gcode_file` with `param: "/cache/Cube_plate_1.gcode"` was rejected for a
file the FTPS listing shows is right there - while `project_file` with
`url: "ftp:///Cube.gcode.3mf"` started a print. So the destination is now a
parameter and both halves point at `SD_ROOT`.

What the hardware settled, recorded in the doc comments: the `ftp:///` url
spelling is right, `param: "Metadata/plate_1.gcode"` works for a
single-plate project, `use_ams: false` + `ams_mapping: ""` is accepted, and
a stop during PREPARE/RUNNING moves the printer to FAILED, which is how
Bambu reports an operator cancellation. What it did not settle: all three
control verbs answer `result: "success"` on an *idle* printer, so the reply
means "accepted", not "the state changed" - callers still have to read
`gcode_state` back.

`PrinterCommand::ControlPrint` makes this reachable from the cloud at all;
until now pause/resume/stop existed in the proxy with no way to send them.
Still no start-print variant, now for a different reason: the path around it
is missing, not the hardware proof.

Also: `list_dir` for any directory (how the layout above was found, without
sending a print command to guess), `request_raw` for probing an unconfirmed
command shape, and two examples - `bambu_cache_list` (read-only) and
`bambu_print_probe` (gated steps, `start` needing both IDLE and
--yes-start-a-real-print).
This commit is contained in:
2026-10-11 17:38:27 +00:00
parent 588ca85842
commit 939c35fb91
10 changed files with 776 additions and 49 deletions
+50
View File
@@ -111,6 +111,17 @@ pub trait GenericPrinter {
/// difference lives, same division of labor as every other method here.
async fn send_gcode_line(&mut self, gcode_line: String) -> Result<(), PrinterError>;
/// REQUIRED, no default body: pause, resume or cancel the print that is
/// already running. Each vendor has one mechanism for all three - an
/// MQTT `print` command, a PrusaLink job endpoint, a Klipper macro, a
/// Marlin gcode word - so they arrive here as one call taking which.
///
/// There is deliberately no "start a print" counterpart on this trait.
/// Starting one needs a file that is already on the printer, which is
/// `upload_project_file`'s job, and the two have very different
/// consequences; see `BambuPrinter::start_uploaded_file`.
async fn control_print(&mut self, action: PrintControl) -> Result<(), PrinterError>;
/// OPTIONAL: a default body every printer gets for free unless it
/// writes its own. None of ours do below, so all three vendors share
/// this exact implementation - the trait-method equivalent of
@@ -131,6 +142,36 @@ fn percent_to_pwm(percent: u8) -> u8 {
}
/// The three things you can do to a print that is already running.
///
/// One enum and one trait method rather than three separate methods: every
/// vendor implements all three the same way it implements any of them (one
/// MQTT command for Bambu, one REST call for PrusaLink, one macro for
/// Klipper, one gcode word for Marlin), so splitting them would triple the
/// delegation boilerplate without separating anything that differs.
///
/// `Stop` is Bambu's own word for it and the one the schemas use. It
/// cancels the print - it is not a pause you can come back from.
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PrintControl {
Pause,
Resume,
Stop,
}
impl PrintControl {
/// The vendor command name. Bambu's `print` category happens to use
/// exactly these three words, so this doubles as the MQTT command.
pub fn as_str(self) -> &'static str {
match self {
Self::Pause => "pause",
Self::Resume => "resume",
Self::Stop => "stop",
}
}
}
/// The closed set of printer kinds this fleet can talk to.
pub enum PrinterHandle {
Bambu(BambuPrinter),
@@ -190,6 +231,15 @@ impl PrinterHandle {
// only "boilerplate tax" for not having inheritance - everything else reads
// like normal code.
impl GenericPrinter for PrinterHandle {
async fn control_print(&mut self, action: PrintControl) -> Result<(), PrinterError> {
match self {
PrinterHandle::Bambu(p) => p.control_print(action).await,
PrinterHandle::PrusaLink(p) => p.control_print(action).await,
PrinterHandle::PrusaSerial(p) => p.control_print(action).await,
PrinterHandle::Klipper(p) => p.control_print(action).await,
}
}
fn base(&self) -> &PrinterBase {
match self {
PrinterHandle::Bambu(p) => p.base(),