README/certs/README/.env.example/printers.example.toml were all still describing this as a learning-stage stub with no real printer protocol clients - rewritten to reflect the real Bambu MQTT client, cert auto-pinning, and marlin serial connection that now exist. - Remove examples/fetch_bambu_cert.rs: its manual fetch-and-save workflow is now what BambuPrinter::connect()'s ensure_trusted does automatically on every connect, so it has no remaining use case. - Fix examples/printer_polymorphism.rs, which no longer compiled - dispatch_gcode was replaced by real methods earlier this session; swapped in print_status() as the vendor-only-extension example. - Remove Config::bambu_ca_cert_overrides/bambu_require_valid_cert and the .env vars documenting them - confirmed dead (nothing read them), fully superseded by printers.toml's per-printer ca_cert_path/ require_valid_cert plus automatic trust-on-first-connect pinning. - Fix stale doc comments in printer/mod.rs and printer/bambu.rs that described certificate trust and MQTT as not built yet, or referencing the now-removed example. No behavior change in the daemon itself - verified with cargo build --examples and a live run of printer_polymorphism and auto_connect_bambu against the real P1P/H2C printers.
216 lines
9.8 KiB
Rust
216 lines
9.8 KiB
Rust
//! Printer hierarchy - the Rust answer to
|
|
//! `GenericPrinter -> BambuV1/BambuV2/PrusaLink/PrusaSerial/Klipper`-style
|
|
//! inheritance, since Rust structs can't `extend` each other.
|
|
//!
|
|
//! Bambu is a single `BambuPrinter` type, not a per-version split: which
|
|
//! MQTT/BambuMQTT protocol generation (v1 vs v2) a given printer uses is
|
|
//! something `connect()` discovers (or is told to force), not something
|
|
//! config picks a *type* for - see `bambu.rs`'s doc comment.
|
|
//!
|
|
//! Three pieces, each doing one job an OOP base class would normally do:
|
|
//!
|
|
//! 1. `PrinterBase` - a plain struct holding the shared *fields*. Every
|
|
//! printer struct below *has* one of these (composition) instead of
|
|
//! *extending* one.
|
|
//! 2. `GenericPrinter` - a trait holding the shared *behavior*. `connect()`
|
|
//! has no default body, so every printer type is forced to write its
|
|
//! own - that's what "overriding" looks like when there's no inherited
|
|
//! body to override in the first place.
|
|
//! 3. `PrinterHandle` - an enum listing the closed set of printer kinds.
|
|
//! Stands in for "any subclass of GenericPrinter".
|
|
//!
|
|
//! Run `cargo run --example printer_polymorphism` to see it in action.
|
|
//!
|
|
//! Real MQTT is built now - `bambu.rs`/`bambu_mqtt.rs` for Bambu's
|
|
//! protocol, `marlin_serial.rs` for a real Marlin serial connection - see
|
|
//! each module's own doc comment. PrusaLink/Klipper's HTTP clients are
|
|
//! still stubs; that's the next piece to build past this module.
|
|
|
|
mod bambu;
|
|
mod bambu_commands;
|
|
mod bambu_mqtt;
|
|
mod klipper;
|
|
mod marlin_serial;
|
|
mod prusa;
|
|
|
|
pub use bambu::{BambuPrinter, BambuProtocolPreference, BambuProtocolVersion, BambuTls};
|
|
pub use bambu_commands::{
|
|
AmsState, AmsTray, AmsUnit, BambuReport, BambuRequest, CommandEnvelope, EmptyTray, GetVersionRequest,
|
|
GetVersionResponse, LoadedTray, PrintReport, PushallRequest, XcamSettings,
|
|
};
|
|
pub use klipper::KlipperPrinter;
|
|
pub use prusa::{PrusaLinkPrinter, PrusaSerialPrinter};
|
|
|
|
/// A tiny error type: just a message. `String` would work too - this exists
|
|
/// mainly so `?` has something concrete to convert into everywhere.
|
|
#[derive(Debug, thiserror::Error)]
|
|
#[error("{0}")]
|
|
pub struct PrinterError(pub String);
|
|
|
|
/// Fields every printer has, regardless of vendor.
|
|
#[derive(Debug, Clone)]
|
|
pub struct PrinterBase {
|
|
pub id: String,
|
|
pub name: String,
|
|
pub host: String,
|
|
}
|
|
|
|
/// The shared interface - think `interface GenericPrinter` (Java/TS) or an
|
|
/// ABC (Python). Every vendor's struct implements this.
|
|
pub trait GenericPrinter {
|
|
/// Read-only access to the shared fields.
|
|
fn base(&self) -> &PrinterBase;
|
|
|
|
/// REQUIRED, no default body: every vendor speaks a different protocol,
|
|
/// so there's nothing sensible to share here. Each `impl` in
|
|
/// `bambu.rs`/`prusa.rs`/`klipper.rs` provides a totally different
|
|
/// body - that's the override.
|
|
///
|
|
/// `cargo check` will warn about `async fn` in a public trait here.
|
|
/// That's about a more advanced concern (whether the resulting work can
|
|
/// safely move across OS threads) that only matters once this is wired
|
|
/// into the multi-threaded daemon - safe to ignore for now.
|
|
async fn connect(&mut self) -> Result<(), PrinterError>;
|
|
|
|
|
|
async fn publish(&mut self, payload: String) -> Result<(), PrinterError>;
|
|
|
|
/// REQUIRED, no default body: `percent` is 0-100, but how it actually
|
|
/// gets sent - an MQTT gcode-injection command, a REST call, raw bytes
|
|
/// on a serial port - is different for every vendor, same as
|
|
/// `connect()`.
|
|
async fn set_fan_speed(&mut self, percent: u8) -> Result<(), PrinterError>;
|
|
|
|
/// REQUIRED, no default body: takes the *whole file* already in memory
|
|
/// (`data`), not a path - whatever called this already had to download
|
|
/// it from the S3 URL the cloud handed over, so there'd be nothing on
|
|
/// disk to point a path at anyway. Each vendor does something quite
|
|
/// different with `data`: Bambu/PrusaLink/Klipper each do one bulk
|
|
/// transfer to storage the printer's own firmware owns; PrusaSerial
|
|
/// has no such storage at all - see that impl's doc comment.
|
|
async fn upload_project_file(&mut self, file_name: String, data: Vec<u8>) -> Result<(), PrinterError>;
|
|
|
|
/// REQUIRED, no default body: sends exactly *one* gcode line, using
|
|
/// whatever single-command mechanism this vendor has (an MQTT
|
|
/// `gcode_line` publish for Bambu, an HTTP command-injection endpoint
|
|
/// for PrusaLink/Klipper, a raw write over the port for PrusaSerial).
|
|
/// `upload_gcode` below is the generic driver that calls this once per
|
|
/// line for a whole file - this method is where each vendor's actual
|
|
/// difference lives, same division of labor as every other method here.
|
|
async fn send_gcode_line(&mut self, gcode_line: String) -> 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
|
|
/// inheriting a base-class method unchanged.
|
|
fn display_name(&self) -> String {
|
|
format!("{} ({})", self.base().name, self.base().id)
|
|
}
|
|
}
|
|
|
|
/// M106's `S` parameter is a raw 0-255 PWM value, not a percentage. Shared
|
|
/// by anything that ultimately sends gcode (Bambu, PrusaLink, PrusaSerial)
|
|
/// - *not* a trait default, since Klipper's hypothetical native fan API
|
|
/// below takes a percent directly and has no use for this at all. Private,
|
|
/// but still visible to bambu.rs/prusa.rs: Rust's module privacy reaches
|
|
/// into child modules, not just the exact file it's defined in.
|
|
fn percent_to_pwm(percent: u8) -> u8 {
|
|
((percent.min(100) as u16 * 255) / 100) as u8
|
|
}
|
|
|
|
|
|
/// The closed set of printer kinds this fleet can talk to.
|
|
pub enum PrinterHandle {
|
|
Bambu(BambuPrinter),
|
|
PrusaLink(PrusaLinkPrinter),
|
|
PrusaSerial(PrusaSerialPrinter),
|
|
Klipper(KlipperPrinter),
|
|
}
|
|
|
|
// Hand-written delegation: each trait method just matches on the variant
|
|
// and forwards to that variant's own implementation. This one block is the
|
|
// only "boilerplate tax" for not having inheritance - everything else reads
|
|
// like normal code.
|
|
impl GenericPrinter for PrinterHandle {
|
|
fn base(&self) -> &PrinterBase {
|
|
match self {
|
|
PrinterHandle::Bambu(p) => p.base(),
|
|
PrinterHandle::PrusaLink(p) => p.base(),
|
|
PrinterHandle::PrusaSerial(p) => p.base(),
|
|
PrinterHandle::Klipper(p) => p.base(),
|
|
}
|
|
}
|
|
|
|
async fn connect(&mut self) -> Result<(), PrinterError> {
|
|
match self {
|
|
PrinterHandle::Bambu(p) => p.connect().await,
|
|
PrinterHandle::PrusaLink(p) => p.connect().await,
|
|
PrinterHandle::PrusaSerial(p) => p.connect().await,
|
|
PrinterHandle::Klipper(p) => p.connect().await,
|
|
}
|
|
}
|
|
|
|
async fn publish(&mut self, payload: String) -> Result<(), PrinterError> {
|
|
match self {
|
|
PrinterHandle::Bambu(p) => p.publish(payload).await,
|
|
PrinterHandle::PrusaLink(p) => p.publish(payload).await,
|
|
PrinterHandle::PrusaSerial(p) => p.publish(payload).await,
|
|
PrinterHandle::Klipper(p) => p.publish(payload).await,
|
|
}
|
|
}
|
|
|
|
async fn set_fan_speed(&mut self, percent: u8) -> Result<(), PrinterError> {
|
|
match self {
|
|
PrinterHandle::Bambu(p) => p.set_fan_speed(percent).await,
|
|
PrinterHandle::PrusaLink(p) => p.set_fan_speed(percent).await,
|
|
PrinterHandle::PrusaSerial(p) => p.set_fan_speed(percent).await,
|
|
PrinterHandle::Klipper(p) => p.set_fan_speed(percent).await,
|
|
}
|
|
}
|
|
|
|
async fn upload_project_file(&mut self, file_name: String, data: Vec<u8>) -> Result<(), PrinterError> {
|
|
match self {
|
|
PrinterHandle::Bambu(p) => p.upload_project_file(file_name, data).await,
|
|
PrinterHandle::PrusaLink(p) => p.upload_project_file(file_name, data).await,
|
|
PrinterHandle::PrusaSerial(p) => p.upload_project_file(file_name, data).await,
|
|
PrinterHandle::Klipper(p) => p.upload_project_file(file_name, data).await,
|
|
}
|
|
}
|
|
|
|
async fn send_gcode_line(&mut self, gcode_line: String) -> Result<(), PrinterError> {
|
|
match self {
|
|
PrinterHandle::Bambu(p) => p.send_gcode_line(gcode_line).await,
|
|
PrinterHandle::PrusaLink(p) => p.send_gcode_line(gcode_line).await,
|
|
PrinterHandle::PrusaSerial(p) => p.send_gcode_line(gcode_line).await,
|
|
PrinterHandle::Klipper(p) => p.send_gcode_line(gcode_line).await,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Splits `gcode` into lines and sends each one through `printer`'s own
|
|
/// `send_gcode_line` - works for *any* vendor, not just serial ones, since
|
|
/// Bambu/PrusaLink/Klipper each have their own single-line injection
|
|
/// mechanism too (see each `send_gcode_line` impl). They just don't use
|
|
/// this for `upload_project_file` - they have real file storage to bulk-
|
|
/// upload to instead, which is a different, faster path than sending a
|
|
/// whole file one line at a time.
|
|
///
|
|
/// `P: GenericPrinter` makes this a genuine Rust generic: one function
|
|
/// body, compiled separately for whichever concrete printer type you call
|
|
/// it with - like a TS function typed `<P extends GenericPrinter>`, except
|
|
/// Rust generates real, separate machine code per type instead of erasing
|
|
/// the type at compile time. Call it with any of the five printer structs,
|
|
/// or with `&mut PrinterHandle` itself (it implements `GenericPrinter` too,
|
|
/// via the delegation above).
|
|
pub async fn upload_gcode<P: GenericPrinter>(printer: &mut P, gcode: &str) -> Result<(), PrinterError> {
|
|
for line in gcode.lines() {
|
|
let line = line.trim();
|
|
// Blank lines and full-line comments have nothing to send.
|
|
if line.is_empty() || line.starts_with(';') {
|
|
continue;
|
|
}
|
|
printer.send_gcode_line(line.to_string()).await?;
|
|
}
|
|
Ok(())
|
|
}
|