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).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-10-11 17:38:27 +00:00
co-authored by Claude Opus 5
parent 4ab95ff7ce
commit 4d644937af
10 changed files with 776 additions and 49 deletions
+63 -1
View File
@@ -46,6 +46,26 @@ const FTPS_USER: &str = "bblp";
/// print something that already lives here.
pub const CACHE_DIR: &str = "cache";
/// The SD card's root, which is where an FTPS login lands and - confirmed
/// live on a P1P, 2026-10-11 - the only directory a `print.project_file`
/// command has been seen to print from.
///
/// The card's two interesting directories hold different things, and a
/// listing of the real P1P is the clearest way to say which:
///
/// - the root holds 43 `<name>.gcode.3mf` files, the printable project
/// archives Bambu Studio sends when it uploads a plate;
/// - `cache/` holds 20 `<project>_plate_N.gcode` files with
/// `<N>_<project>.gcode.bbl` sidecars - the printer's own extracted
/// per-plate working copies.
///
/// Those extracted copies look like the obvious thing to print and are not:
/// `print.gcode_file` with `param: "/cache/Cube_plate_1.gcode"` was
/// rejected (`result: "fail"`) for a file that demonstrably exists, while
/// `print.project_file` with `url: "ftp:///Cube.gcode.3mf"` against the
/// root started a print. So uploads go here, not into `cache/`.
pub const SD_ROOT: &str = "/";
/// Blocking: opens an implicit-FTPS session to `host`, logs in with
/// `access_code`, and writes `data` to `cache/{file_name}`. Returns however
/// many bytes the server reported accepting.
@@ -58,6 +78,7 @@ pub fn upload(
host: &str,
access_code: &str,
connector: native_tls::TlsConnector,
dir: &str,
file_name: &str,
data: &[u8],
) -> anyhow::Result<u64> {
@@ -73,7 +94,12 @@ pub fn upload(
// either way, letting the server treat the stream as text is free
// corruption.
ftp.transfer_type(FileType::Binary)?;
ftp.cwd(CACHE_DIR)?;
// `SD_ROOT` means "don't change directory at all" - the FTPS login
// already lands in the card's root, and `cwd("/")` is a needless round
// trip that some servers answer less predictably than no request.
if dir != SD_ROOT {
ftp.cwd(dir)?;
}
// `put_file` wants something that implements `Read`. `data` is already
// bytes in memory (see `GenericPrinter::upload_project_file`'s doc
@@ -89,3 +115,39 @@ pub fn upload(
Ok(written)
}
/// Blocking: lists `cache/` on the printer.
///
/// Read-only, and the counterpart to `upload`. The previous version of this
/// comment said a `project_file` command "can only print something that is
/// already in this directory", which is wrong twice over: the printable
/// projects are in the root (see `SD_ROOT`), and this directory's contents
/// were rejected when asked for directly. It is still how you find a real
/// slicer-produced `.3mf`
/// to test against, instead of hand-building one and then not knowing
/// whether a failure was the command or the file.
///
/// Returns the server's raw LIST lines rather than parsed entries: Bambu's
/// FTPS server formats them in the usual unix style, but the parsing is the
/// caller's problem until something needs more than the file names.
pub fn list(
host: &str,
access_code: &str,
connector: native_tls::TlsConnector,
dir: &str,
) -> anyhow::Result<Vec<String>> {
let mut ftp = NativeTlsFtpStream::connect_secure_implicit(
(host, FTPS_PORT),
NativeTlsConnector::from(connector),
host,
)?;
ftp.login(FTPS_USER, access_code)?;
ftp.transfer_type(FileType::Binary)?;
let entries = ftp.list(Some(dir))?;
let _ = ftp.quit();
Ok(entries)
}