From 6e18215af5c452f53fd2908e3ee682a56f1e3aaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iwo=20Strzebo=C5=84ski?= Date: Fri, 28 Aug 2026 21:40:11 +0000 Subject: [PATCH] Add GenericPrinter::set_fan_speed() across all five printer kinds Same override pattern as connect(): required, no default body, five completely different bodies. - BambuGenericPrinter::set_fan_speed_impl() is shared by BambuV1Printer and BambuV2Printer (composition reuse, same as connect_impl) - both speak the same MQTT gcode-injection mechanism. - PrusaLinkPrinter and PrusaSerialPrinter both send the same M106 gcode but over different transports (HTTP command injection vs. raw serial bytes) - they share the percent->PWM conversion despite having unrelated connect() implementations. - KlipperPrinter uses a hypothetical Moonraker-native endpoint that takes a percentage directly - no PWM conversion at all, since that math only applies to the gcode-speaking vendors. percent_to_pwm() lives in mod.rs as a private free function rather than a trait default: it's genuine shared logic, but only for the subset of vendors that need it, which is exactly the case a trait default can't express cleanly. Visible to bambu.rs/prusa.rs via super:: because Rust's module privacy reaches into child modules. Verified with cargo check --all-targets and a full run of printer_polymorphism: 50% converges on the same PWM value (127) across all three gcode-based printers, Klipper's native path takes 0.50 directly. --- examples/printer_polymorphism.rs | 9 +++++++++ src/printer/bambu.rs | 19 +++++++++++++++++++ src/printer/klipper.rs | 9 +++++++++ src/printer/mod.rs | 31 +++++++++++++++++++++++++------ src/printer/prusa.rs | 18 ++++++++++++++++++ 5 files changed, 80 insertions(+), 6 deletions(-) diff --git a/examples/printer_polymorphism.rs b/examples/printer_polymorphism.rs index 274af0f..62acdb7 100644 --- a/examples/printer_polymorphism.rs +++ b/examples/printer_polymorphism.rs @@ -62,4 +62,13 @@ async fn main() { _ => {} } } + + // A second trait method, same uniform call site as connect(): every + // vendor sends "50%" a completely different way — an MQTT gcode + // command, Moonraker's native API, gcode over HTTP, gcode over serial + // — but the caller doesn't need to know or care which. + println!(); + for printer in &mut fleet { + let _ = printer.set_fan_speed(50).await; + } } diff --git a/src/printer/bambu.rs b/src/printer/bambu.rs index 2196c2a..71ba0b2 100644 --- a/src/printer/bambu.rs +++ b/src/printer/bambu.rs @@ -252,6 +252,17 @@ impl BambuGenericPrinter { } } + /// Shared by both `BambuV1Printer` and `BambuV2Printer` — same MQTT + /// gcode-injection mechanism regardless of protocol generation. Real + /// version would publish + /// `{"print":{"command":"gcode_line","param":"M106 P1 S"}}` to + /// `device/{sn}/request` over the MQTTS session. + async fn set_fan_speed_impl(&mut self, percent: u8) -> Result<(), PrinterError> { + let pwm = super::percent_to_pwm(percent); + println!("[{}] sending gcode: M106 P1 S{pwm}", self.base.name); + Ok(()) + } + /// Shared "connect" logic both V1 and V2 delegate to — the real /// version would open the MQTTS session here. Still a stub, but no /// longer duplicated across two structs. @@ -302,6 +313,10 @@ impl GenericPrinter for BambuV1Printer { async fn connect(&mut self) -> Result<(), PrinterError> { self.generic.connect_impl().await } + + async fn set_fan_speed(&mut self, percent: u8) -> Result<(), PrinterError> { + self.generic.set_fan_speed_impl(percent).await + } } /// Current-generation Bambu MQTT report schema (X1 Carbon/X1E, H2 series). @@ -341,4 +356,8 @@ impl GenericPrinter for BambuV2Printer { async fn connect(&mut self) -> Result<(), PrinterError> { self.generic.connect_impl().await } + + async fn set_fan_speed(&mut self, percent: u8) -> Result<(), PrinterError> { + self.generic.set_fan_speed_impl(percent).await + } } diff --git a/src/printer/klipper.rs b/src/printer/klipper.rs index 088b229..c7b89c2 100644 --- a/src/printer/klipper.rs +++ b/src/printer/klipper.rs @@ -28,4 +28,13 @@ impl GenericPrinter for KlipperPrinter { println!("[{}] connecting over WebSocket (Moonraker)", self.base.name); Ok(()) } + + /// OVERRIDE: Moonraker's own **hypothetical** native API, not gcode injection — no + /// percent-to-PWM conversion needed since it already takes a + /// percentage directly. Real version: + /// `POST /printer/gcode/script?script=SET_FAN_SPEED SPEED=0.NN`. + async fn set_fan_speed(&mut self, percent: u8) -> Result<(), PrinterError> { + println!("[{}] Moonraker: SET_FAN_SPEED SPEED={:.2}", self.base.name, percent as f32 / 100.0); + Ok(()) + } } diff --git a/src/printer/mod.rs b/src/printer/mod.rs index ff699be..0d6d1ac 100644 --- a/src/printer/mod.rs +++ b/src/printer/mod.rs @@ -67,6 +67,12 @@ pub trait GenericPrinter { /// into the multi-threaded daemon — safe to ignore for now. async fn connect(&mut self) -> 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>; + /// 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 @@ -76,14 +82,17 @@ pub trait GenericPrinter { } } -pub enum PrinterFlavour { - BambuV1Printer, - BambuV2Printer, - PrusaLinkPrinter, - PrusaSerialPrinter, - KlipperPrinter, +/// 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 { BambuV1(BambuV1Printer), @@ -117,4 +126,14 @@ impl GenericPrinter for PrinterHandle { PrinterHandle::Klipper(p) => p.connect().await, } } + + async fn set_fan_speed(&mut self, percent: u8) -> Result<(), PrinterError> { + match self { + PrinterHandle::BambuV1(p) => p.set_fan_speed(percent).await, + PrinterHandle::BambuV2(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, + } + } } diff --git a/src/printer/prusa.rs b/src/printer/prusa.rs index ff50dd7..49f1c6d 100644 --- a/src/printer/prusa.rs +++ b/src/printer/prusa.rs @@ -28,6 +28,15 @@ impl GenericPrinter for PrusaLinkPrinter { println!("[{}] connecting over HTTP (PrusaLink)", self.base.name); Ok(()) } + + /// OVERRIDE: gcode via PrusaLink's command-injection endpoint — same + /// underlying M106 as Bambu, reusing the shared percent->PWM + /// conversion, but sent over HTTP instead of MQTT. + async fn set_fan_speed(&mut self, percent: u8) -> Result<(), PrinterError> { + let pwm = super::percent_to_pwm(percent); + println!("[{}] POST /api/printer/command {{\"commands\":[\"M106 S{pwm}\"]}}", self.base.name); + Ok(()) + } } @@ -60,4 +69,13 @@ impl GenericPrinter for PrusaSerialPrinter { println!("[{}] connecting over serial (COM{})", self.base.name, self.com_port); Ok(()) } + + /// OVERRIDE: same gcode as PrusaLink (still M106, still the shared + /// conversion), but written straight to the serial port instead of + /// sent over HTTP — a third transport for the same underlying command. + async fn set_fan_speed(&mut self, percent: u8) -> Result<(), PrinterError> { + let pwm = super::percent_to_pwm(percent); + println!("[{}] writing to COM{}: M106 S{pwm}", self.base.name, self.com_port); + Ok(()) + } }