//! 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) -> 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) -> 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 `

`, 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(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(()) }