//! Getting a file *onto* a Bambu printer - the other half of //! `upload_project_file`, which `bambu.rs` stubbed with a `println!` until //! now. MQTT can start a print, but it can only name a file the printer //! already has; moving the bytes there is a completely separate protocol. //! //! This is deliberately suppaftp's **blocking** client, not its async one, //! driven from `tokio::task::spawn_blocking` at the single call site in //! `bambu.rs`. Two reasons: //! //! 1. The blocking client takes a plain `native_tls::TlsConnector`, so this //! module reuses `BambuTls::build_connector()` exactly as-is and every //! TLS trust decision for a printer stays in that one method (which has //! a long doc comment explaining some hard-won specifics - duplicating //! that reasoning against a second TLS stack is how it drifts). //! 2. suppaftp's async client needs that second TLS stack //! (`async-native-tls`), whose futures-vs-tokio `AsyncRead` mismatch has //! already cost this project one debugging session. //! //! **Unverified against real hardware.** The port, the username, and //! implicit TLS are well-established for Bambu LAN mode, and the TLS layer //! itself is the same one `test_tls_handshake` already proves against a //! real printer on 8883. But no physical printer has accepted an upload //! from *this* code yet. Expect the first live run to be where the //! remaining details get settled - see `bambu.rs::upload_project_file` for //! the list of specific things to watch. use std::io::Cursor; use suppaftp::types::FileType; use suppaftp::{NativeTlsConnector, NativeTlsFtpStream}; /// Bambu LAN mode speaks *implicit* FTPS: TLS starts the instant the socket /// opens, with no plaintext `AUTH TLS` negotiation first. That's the older /// of the two FTPS styles and formally deprecated, which is exactly why /// suppaftp hides `connect_secure_implicit` behind a `deprecated` cargo /// feature. The printers only speak this, so the feature is switched on in /// `Cargo.toml`. const FTPS_PORT: u16 = 990; /// The same for every Bambu printer in LAN mode. The password is the same /// access code MQTT authenticates with - one shared secret, two protocols. const FTPS_USER: &str = "bblp"; /// Storage the printer's own firmware owns, and the directory Bambu Studio /// uploads to as well. A `project_file`/`gcode_file` MQTT command can only /// print something that already lives here. pub const CACHE_DIR: &str = "cache"; /// 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. /// /// Takes an already-built `connector` rather than a `&BambuTls` so the /// caller resolves trust on the async side and this function can be moved /// wholesale onto a blocking thread - `native_tls::TlsConnector` is `Send`, /// which is what makes that legal. pub fn upload( host: &str, access_code: &str, connector: native_tls::TlsConnector, file_name: &str, data: &[u8], ) -> anyhow::Result { let mut ftp = NativeTlsFtpStream::connect_secure_implicit( (host, FTPS_PORT), NativeTlsConnector::from(connector), host, )?; ftp.login(FTPS_USER, access_code)?; // A .3mf is a zip archive and a .gcode has meaningful line endings - // either way, letting the server treat the stream as text is free // corruption. ftp.transfer_type(FileType::Binary)?; ftp.cwd(CACHE_DIR)?; // `put_file` wants something that implements `Read`. `data` is already // bytes in memory (see `GenericPrinter::upload_project_file`'s doc // comment for why it's bytes and not a path), and `Cursor` is the // standard way to let something read from a slice - roughly wrapping a // Buffer in a stream in Node. let mut reader = Cursor::new(data); let written = ftp.put_file(file_name, &mut reader)?; // Best-effort: the bytes are already delivered and acknowledged, so // failing to say goodbye politely isn't worth failing the upload over. let _ = ftp.quit(); Ok(written) }