From 9eab5e5a04edcbebd2ba49ea719c42b612b5ecfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iwo=20Strzebo=C5=84ski?= Date: Fri, 28 Aug 2026 20:38:10 +0000 Subject: [PATCH] Bambu TLS trust config + finish PrusaLink/PrusaSerial split Certs: - Vendor Bambu's shared LAN-mode root CA (certs/bambu_ca2.pem, verified self-signed CA:TRUE, see certs/README.md for provenance/fingerprint). - Config gains bambu_ca_cert_path (per-printer override) and bambu_require_valid_cert (the allow/reject flag); printer::bambu::BambuTls turns those into BundledCa/Custom/Insecure. connect() doesn't perform a real handshake yet (no TLS-capable MQTT client wired in), just reports which trust mode it would use. Also reconciles a rename in flight (Printer -> GenericPrinter trait) and finishes the PrusaPrinter -> PrusaLinkPrinter/PrusaSerialPrinter split: added the missing GenericPrinter impl for PrusaSerialPrinter, fixed PrinterHandle's variant payload types, updated mod.rs's pub use list and doc comments, and updated the example to the new 5-variant shape. Verified with cargo check --all-targets (0 errors) and a full run of cargo run --example printer_polymorphism. --- certs/README.md | 24 +++++++ certs/bambu_ca2.pem | 109 +++++++++++++++++++++++++++++++ examples/printer_polymorphism.rs | 27 ++++---- src/config.rs | 14 ++++ src/printer/bambu.rs | 79 ++++++++++++++++++---- src/printer/klipper.rs | 4 +- src/printer/mod.rs | 48 +++++++++----- src/printer/prusa.rs | 42 ++++++++++-- 8 files changed, 302 insertions(+), 45 deletions(-) create mode 100644 certs/README.md create mode 100644 certs/bambu_ca2.pem diff --git a/certs/README.md b/certs/README.md new file mode 100644 index 0000000..88df54c --- /dev/null +++ b/certs/README.md @@ -0,0 +1,24 @@ +# certs/ + +## bambu_ca2.pem + +Bambu Lab's shared root CA ("BBL CA2 RSA") used to verify a printer's LAN-mode +MQTTS (port 8883) certificate. Current-generation Bambu printers present a +certificate signed by (a chain rooted at) this CA — trust this one file +instead of fetching/pinning a certificate per printer. + +- Source: https://github.com/bambulab/BambuStudio/blob/master/resources/cert/printer.cer +- Fetched: 2026-08-28 +- SHA-256 fingerprint: `E9:8F:19:57:8B:3F:12:4A:CE:6B:8A:24:7F:FE:DA:52:DC:99:C8:9F:D4:E7:D2:0C:82:82:99:77:B7:F3:35:02` +- Verified with `openssl x509 -noout -text`: self-signed (`Issuer == Subject`), + `Basic Constraints: CA:TRUE`, `Key Usage: Certificate Sign, CRL Sign` — a + genuine root CA, not a per-device leaf certificate. Valid until 2050. + +The "CA2" name implies there was a CA1 generation before it — some +older printers/firmware may not chain to this root and need their own +certificate instead. `CONTINUUM_BAMBU_CA_CERT_PATH` (see `.env.example`) +overrides this default with a specific file for exactly that case. + +If a printer's connection ever fails certificate verification against this +file, re-fetch from the source URL above (BambuStudio ships whatever the +current fleet needs) and update the fingerprint here. diff --git a/certs/bambu_ca2.pem b/certs/bambu_ca2.pem new file mode 100644 index 0000000..6d8dbcb --- /dev/null +++ b/certs/bambu_ca2.pem @@ -0,0 +1,109 @@ +-----BEGIN CERTIFICATE----- +MIIFfzCCA2egAwIBAgIUXtzR6tRiL/RHBRXOoyFU0+XrliowDQYJKoZIhvcNAQEL +BQAwRjELMAkGA1UEBhMCQ04xITAfBgNVBAoMGEJCTCBUZWNobm9sb2dpZXMgQ28u +IEx0ZDEUMBIGA1UEAwwLQkJMIENBMiBSU0EwIBcNMjUwNjE3MDEzODA4WhgPMjA1 +MDA2MTcwMTM4MDhaMEYxCzAJBgNVBAYTAkNOMSEwHwYDVQQKDBhCQkwgVGVjaG5v +bG9naWVzIENvLiBMdGQxFDASBgNVBAMMC0JCTCBDQTIgUlNBMIICIjANBgkqhkiG +9w0BAQEFAAOCAg8AMIICCgKCAgEAo4550G4c42gTKzQqixwKT089RizIdZpyOcGA +679rPaOdWsMqVwnYPP2FpMqXKkjFbedE+SpGloi2NKCuiPNVRbq9PHOOZwTs7YLo +bOwf53FJuO6vRFpzFfX1tlc9zlFqJvZnYO9NgHpMysidocWcgrDN/SIDywgPB5CV +bYg3Vvzua9fwZx9e5KT9xd5IpTqdTrWS47jQOVKLhdQCbJFIlMrblOwLBAx+fHok +wqh6tkI6Ktuyyjw8Dysebi1ndWjKtZ2mW47r8xZ/J+z3EZqcyJMY6MRtx/zb1jBF +uHtkjrb5Kv1DMzSKlkaNJIbvC+Mk+hI97W+SjLSRuIdC7+oJUzWaSzgu9cjXCVfm +q8t4IL/35hP69PK95LgLectIrP96CYAT/aVMG19FrFW0QWEyfT+kzG4jkumfPbHq +Y2nNkEN0+tjj3h4WdzrWgQEojK/lhfcRFVkts74+aZoMpQP+vmL17CKmSzXk5o/e +K21xgxJdzMbdztfTpibiXk0abfOpN+1VR+3NYa+bROAKNyGaReEGsyW2bjcjNx51 +5Vqzj3SVxhMSp5vfF9E4A1jE99M/l9jQDM6RzkT0lMccGAd5tUSdNvDlrqtQaQiK +v/ZsXPgXLTWfOpvaLNEgwdMgZMuhjpkwvAZyoYfeF9kyydjDh7bvrX//cz/VopAU +lxUtQtMCAwEAAaNjMGEwHQYDVR0OBBYEFNVJgQad1sNTN0jxVkwbJ/XM1an1MB8G +A1UdIwQYMBaAFNVJgQad1sNTN0jxVkwbJ/XM1an1MA8GA1UdEwEB/wQFMAMBAf8w +DgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUAA4ICAQBFZDKMJfp/N4gBeFHh +MiFehaUyMS6e9mzrTfMLJLJoj6Jopa9V9jIfcCEBGZuRThqFcATV+UdFHSINpUcH +upcCYnazTRC4dn1hnxnQ1ojQcHxdGp9xGw/YclAKD97d8bPShfBMT1to9zbMK7T5 +L8zgqg01YIOKjQk0Hcd0+0iUr6m8zQ5P8Rl3QXqAyeWgqmYQrrjTWwPsgdfHNXKX +vDrx7/cqry5lKU802hUplKMBxelv4W8407Ytj1lfJOwvxqxxsFU5jSwcUG3zo2vk +QtjRs8m5BKup5K1OPYkkPu7Ld89X0XpU073/dNDG11uxb1eDKrtNP6vZuZjNE2Pq +8HCoI1EtP+ItyqtUMvHi6Z2zsmlA25broVioeUKxjlIecpQ9JR/FhDu9CWNF/nDW +LSORNaMMzgsMSzI+HCiUhqN+qMIvVP6rzGTJzwqz/lc5Lf+ZPCnGA9WJTT4uPIhf +ufbZmnUJ35WuWKHxovDsqBh88zQ9sZ+ei4Hi4vVzOhUgfG3aLoSQEYqRoqaboANh +wCwzyuW2Rv54u5QSBbd6Gx1OpvsWmLPWd2/iL2kISl5wfmLGVydvSJa+rbOfuAy7 +ycVQacVDQCAnbhoVrQy7+454QsKSW3ZV6BcyRrorewCyCYgd7nyxflxHZTBEykXX +haGNe/KFNvJBMOIuIUzknRRmiQ== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIB8zCCAZmgAwIBAgIUe61jGQ4RzIC8k+sNuqbI/CaNqPIwCgYIKoZIzj0EAwIw +RjELMAkGA1UEBhMCQ04xITAfBgNVBAoMGEJCTCBUZWNobm9sb2dpZXMgQ28uIEx0 +ZDEUMBIGA1UEAwwLQkJMIENBMiBFQ0MwIBcNMjUwNjE3MDEzODM1WhgPMjA1MDA2 +MTcwMTM4MzVaMEYxCzAJBgNVBAYTAkNOMSEwHwYDVQQKDBhCQkwgVGVjaG5vbG9n +aWVzIENvLiBMdGQxFDASBgNVBAMMC0JCTCBDQTIgRUNDMFkwEwYHKoZIzj0CAQYI +KoZIzj0DAQcDQgAEpKTF7wRSty4DXpGJzgCPwRh8ghLlxUC3qJbyEgLqTvJgbiwY +APPHK7kVbVmerkqhHOT4QeWRlTG3dOQGLA2VpaNjMGEwHQYDVR0OBBYEFKuRpsjY +REOyIKH7HwOE6jhGBd6NMB8GA1UdIwQYMBaAFKuRpsjYREOyIKH7HwOE6jhGBd6N +MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMCA0gA +MEUCIErBiUm3VdtP3rz4kb8aLpI5p+BzL7M9vElBGWWJxpHMAiEA3r5tJWVGwuxi +YCrB1c40KYFRFyahGrhOJZAj/YhRdnU= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEeTCCA2GgAwIBAgIUOq+lNIaC2xsswkFqj5JPyVBl45cwDQYJKoZIhvcNAQEL +BQAwQjELMAkGA1UEBhMCQ04xIjAgBgNVBAoMGUJCTCBUZWNobm9sb2dpZXMgQ28u +LCBMdGQxDzANBgNVBAMMBkJCTCBDQTAeFw0yNTA2MTcwMjAxMjdaFw0zNTA2MTUw +MjAxMjdaMEYxCzAJBgNVBAYTAkNOMSEwHwYDVQQKDBhCQkwgVGVjaG5vbG9naWVz +IENvLiBMdGQxFDASBgNVBAMMC0JCTCBDQTIgUlNBMIICIjANBgkqhkiG9w0BAQEF +AAOCAg8AMIICCgKCAgEAo4550G4c42gTKzQqixwKT089RizIdZpyOcGA679rPaOd +WsMqVwnYPP2FpMqXKkjFbedE+SpGloi2NKCuiPNVRbq9PHOOZwTs7YLobOwf53FJ +uO6vRFpzFfX1tlc9zlFqJvZnYO9NgHpMysidocWcgrDN/SIDywgPB5CVbYg3Vvzu +a9fwZx9e5KT9xd5IpTqdTrWS47jQOVKLhdQCbJFIlMrblOwLBAx+fHokwqh6tkI6 +Ktuyyjw8Dysebi1ndWjKtZ2mW47r8xZ/J+z3EZqcyJMY6MRtx/zb1jBFuHtkjrb5 +Kv1DMzSKlkaNJIbvC+Mk+hI97W+SjLSRuIdC7+oJUzWaSzgu9cjXCVfmq8t4IL/3 +5hP69PK95LgLectIrP96CYAT/aVMG19FrFW0QWEyfT+kzG4jkumfPbHqY2nNkEN0 ++tjj3h4WdzrWgQEojK/lhfcRFVkts74+aZoMpQP+vmL17CKmSzXk5o/eK21xgxJd +zMbdztfTpibiXk0abfOpN+1VR+3NYa+bROAKNyGaReEGsyW2bjcjNx515Vqzj3SV +xhMSp5vfF9E4A1jE99M/l9jQDM6RzkT0lMccGAd5tUSdNvDlrqtQaQiKv/ZsXPgX +LTWfOpvaLNEgwdMgZMuhjpkwvAZyoYfeF9kyydjDh7bvrX//cz/VopAUlxUtQtMC +AwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0O +BBYEFNVJgQad1sNTN0jxVkwbJ/XM1an1MB8GA1UdIwQYMBaAFI80QmjcZ06PxCKe +xXxJ5avdRL4eMA0GCSqGSIb3DQEBCwUAA4IBAQAvS8tyfagaGsFf9YncA2ko/Na5 +9BVF+8TlUo+32oznwIVpS1AhSgLP6rNVekXNFKbuP5htudLQ17ZRBJI/UMVyYEDq +IN7xv7Zj+zJwF6W6haYrjb2Vk8igw1XvNULZfvVNNKIkvJUiVqEslWrC+k74crk/ +Wv8ChVf+zqvfIN6LV3esaGRL02J3AprQGb7DDhR1EefQMScDkNpGJMUmvCmfknrl +iK8qgvQN1SWO7JRf6fNKHsN1ZQvyP0pgLWxpT3V0/0/WttqX3cMGuJF+jVUzm/Nh +xYhFewG8vc3KzTjnwQApMA6CW554FOJWFyOD2jn5yJLT3Vue+aYDQRp4bKMx +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICrjCCAZagAwIBAgIUOq+lNIaC2xsswkFqj5JPyVBl45gwDQYJKoZIhvcNAQEL +BQAwQjELMAkGA1UEBhMCQ04xIjAgBgNVBAoMGUJCTCBUZWNobm9sb2dpZXMgQ28u +LCBMdGQxDzANBgNVBAMMBkJCTCBDQTAeFw0yNTA2MTcwMjAxNDdaFw0zNTA2MTUw +MjAxNDdaMEYxCzAJBgNVBAYTAkNOMSEwHwYDVQQKDBhCQkwgVGVjaG5vbG9naWVz +IENvLiBMdGQxFDASBgNVBAMMC0JCTCBDQTIgRUNDMFkwEwYHKoZIzj0CAQYIKoZI +zj0DAQcDQgAEpKTF7wRSty4DXpGJzgCPwRh8ghLlxUC3qJbyEgLqTvJgbiwYAPPH +K7kVbVmerkqhHOT4QeWRlTG3dOQGLA2VpaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAO +BgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFKuRpsjYREOyIKH7HwOE6jhGBd6NMB8G +A1UdIwQYMBaAFI80QmjcZ06PxCKexXxJ5avdRL4eMA0GCSqGSIb3DQEBCwUAA4IB +AQCg6PjUSSZV+4bvejcVMvgXmKzfD95osWn0ctnoMBxPDa+m+Gg+BcLT2IlFAe3E +KYMvu4T295WQc92rjKYqW6cirFppng9uEFW2mZLimxaSmutsTftE3sbMVMJ/SLYN +PV7TFv6mcBSIFWXwmBOIpbh4BUcVfONTvdSfIqfyAVxsq4xzc2nc6hPBpAm21Ayj +ToC1ev/TbDJ8VllFZiEVmWWlIP3aNzAm8S2mOpxPB2WnanaZHSrvXLFhstyzwrjD +yO1/isOZ7wtr7rcuTJdEvvvCimOZlkfRhaDoTew9tQ0E2FVpzzSinw02qmQ1xIE9 +5/H5ZzJSPkpeAHWEPnKkxg0v +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDZTCCAk2gAwIBAgIUV1FckwXElyek1onFnQ9kL7Bk4N8wDQYJKoZIhvcNAQEL +BQAwQjELMAkGA1UEBhMCQ04xIjAgBgNVBAoMGUJCTCBUZWNobm9sb2dpZXMgQ28u +LCBMdGQxDzANBgNVBAMMBkJCTCBDQTAeFw0yMjA0MDQwMzQyMTFaFw0zMjA0MDEw +MzQyMTFaMEIxCzAJBgNVBAYTAkNOMSIwIAYDVQQKDBlCQkwgVGVjaG5vbG9naWVz +IENvLiwgTHRkMQ8wDQYDVQQDDAZCQkwgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQDL3pnDdxGOk5Z6vugiT4dpM0ju+3Xatxz09UY7mbj4tkIdby4H +oeEdiYSZjc5LJngJuCHwtEbBJt1BriRdSVrF6M9D2UaBDyamEo0dxwSaVxZiDVWC +eeCPdELpFZdEhSNTaT4O7zgvcnFsfHMa/0vMAkvE7i0qp3mjEzYLfz60axcDoJLk +p7n6xKXI+cJbA4IlToFjpSldPmC+ynOo7YAOsXt7AYKY6Glz0BwUVzSJxU+/+VFy +/QrmYGNwlrQtdREHeRi0SNK32x1+bOndfJP0sojuIrDjKsdCLye5CSZIvqnbowwW +1jRwZgTBR29Zp2nzCoxJYcU9TSQp/4KZuWNVAgMBAAGjUzBRMB0GA1UdDgQWBBSP +NEJo3GdOj8QinsV8SeWr3US+HjAfBgNVHSMEGDAWgBSPNEJo3GdOj8QinsV8SeWr +3US+HjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQABlBIT5ZeG +fgcK1LOh1CN9sTzxMCLbtTPFF1NGGA13mApu6j1h5YELbSKcUqfXzMnVeAb06Htu +3CoCoe+wj7LONTFO++vBm2/if6Jt/DUw1CAEcNyqeh6ES0NX8LJRVSe0qdTxPJuA +BdOoo96iX89rRPoxeed1cpq5hZwbeka3+CJGV76itWp35Up5rmmUqrlyQOr/Wax6 +itosIzG0MfhgUzU51A2P/hSnD3NDMXv+wUY/AvqgIL7u7fbDKnku1GzEKIkfH8hm +Rs6d8SCU89xyrwzQ0PR853irHas3WrHVqab3P+qNwR0YirL0Qk7Xt/q3O1griNg2 +Blbjg3obpHo9 +-----END CERTIFICATE----- \ No newline at end of file diff --git a/examples/printer_polymorphism.rs b/examples/printer_polymorphism.rs index 7b39638..58e5ddb 100644 --- a/examples/printer_polymorphism.rs +++ b/examples/printer_polymorphism.rs @@ -2,27 +2,31 @@ //! //! Demonstrates the trait+enum pattern that stands in for inheritance: //! - `connect()` is called the same way regardless of vendor (polymorphism), -//! but each vendor's `impl Printer` runs completely different code -//! (override). +//! but each variant's `impl GenericPrinter` runs completely different +//! code (override). //! - `dispatch_gcode()` only exists on `BambuPrinter` (extension) — you //! have to `match` the enum back down to the concrete type to reach it, //! which is the trade-off for not having implicit downcasting. -use continuum_proxy::printer::{BambuPrinter, KlipperPrinter, Printer, PrinterHandle, PrusaPrinter}; +use continuum_proxy::printer::{ + BambuPrinter, BambuTls, GenericPrinter, KlipperPrinter, PrinterHandle, PrusaLinkPrinter, PrusaSerialPrinter, +}; #[tokio::main] async fn main() { let mut fleet: Vec = vec![ - PrinterHandle::Bambu(BambuPrinter::new("p1", "X1 Carbon", "192.168.1.50", "12345678")), - PrinterHandle::Prusa(PrusaPrinter::new("p2", "MK4", "192.168.1.51", "prusa-api-key")), - PrinterHandle::Klipper(KlipperPrinter::new("p3", "Voron 2.4", "192.168.1.52")), + PrinterHandle::BambuV1(BambuPrinter::new("p1", "P1S", "192.168.1.49", "87654321", BambuTls::BundledCa)), + PrinterHandle::BambuV2(BambuPrinter::new("p2", "X1 Carbon", "192.168.1.50", "12345678", BambuTls::BundledCa)), + PrinterHandle::PrusaLink(PrusaLinkPrinter::new("p3", "MK4", "192.168.1.51", "prusa-api-key")), + PrinterHandle::PrusaSerial(PrusaSerialPrinter::new("p4", "MK3S+", 0)), + PrinterHandle::Klipper(KlipperPrinter::new("p5", "Voron 2.4", "192.168.1.52")), // No host configured — this one will hit the Err path. - PrinterHandle::Klipper(KlipperPrinter::new("p4", "Broken Voron", "")), + PrinterHandle::Klipper(KlipperPrinter::new("p6", "Broken Voron", "")), ]; // Uniform call site: every printer connects the same way from the - // caller's point of view, even though the three `connect()` bodies - // are unrelated implementations. + // caller's point of view, even though each variant's `connect()` body + // is an unrelated implementation. for printer in &mut fleet { match printer.connect().await { Ok(()) => println!(" -> connected: {}\n", printer.display_name()), @@ -31,9 +35,10 @@ async fn main() { } // Vendor-only extension: reachable only after matching the concrete - // variant back out of the enum. + // variant back out of the enum. Both Bambu variants wrap the same + // BambuPrinter struct right now, so both get dispatch_gcode(). for printer in &fleet { - if let PrinterHandle::Bambu(bambu) = printer { + if let PrinterHandle::BambuV1(bambu) | PrinterHandle::BambuV2(bambu) = printer { bambu.dispatch_gcode("part.gcode.3mf"); } } diff --git a/src/config.rs b/src/config.rs index b1c471d..2845b33 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,4 +1,5 @@ use std::env; +use std::path::PathBuf; #[derive(Debug, Clone)] pub struct Config { @@ -8,6 +9,16 @@ pub struct Config { pub uplink_url: String, pub go2rtc_bin: String, pub go2rtc_config: String, + + /// Overrides the bundled Bambu root CA (see `certs/bambu_ca2.pem`) with + /// a specific certificate file — for a printer whose firmware doesn't + /// chain to that shared CA. Most setups leave this unset. + pub bambu_ca_cert_path: Option, + /// When `false`, Bambu MQTTS connections skip certificate verification + /// entirely instead of checking against the CA above. `true` (the + /// default) is the secure choice; only flip this for a printer whose + /// certificate you can't get to verify any other way. + pub bambu_require_valid_cert: bool, } impl Config { @@ -19,6 +30,9 @@ impl Config { uplink_url: env_or("CONTINUUM_UPLINK_URL", "wss://api.continuum.local/ws/edge/v1"), go2rtc_bin: env_or("CONTINUUM_GO2RTC_BIN", "go2rtc"), go2rtc_config: env_or("CONTINUUM_GO2RTC_CONFIG", "./go2rtc.yaml"), + + bambu_ca_cert_path: env::var("CONTINUUM_BAMBU_CA_CERT_PATH").ok().map(PathBuf::from), + bambu_require_valid_cert: env_or("CONTINUUM_BAMBU_REQUIRE_VALID_CERT", "true") == "true", }) } } diff --git a/src/printer/bambu.rs b/src/printer/bambu.rs index c8fd23d..e0a39d7 100644 --- a/src/printer/bambu.rs +++ b/src/printer/bambu.rs @@ -1,37 +1,94 @@ -use super::{Printer, PrinterBase, PrinterError}; +use std::path::Path; + +use super::{GenericPrinter, PrinterBase, PrinterError}; + +/// Bambu's shared LAN-mode root CA ("BBL CA2 RSA") — see certs/README.md +/// for how this was obtained and verified. `include_bytes!` embeds it into +/// the compiled binary at build time, so there's no file to ship alongside +/// the executable. +const BUNDLED_CA: &[u8] = include_bytes!("../../certs/bambu_ca2.pem"); + +/// Which certificate to trust when connecting to a Bambu printer's MQTTS +/// port (8883 — Bambu's LAN mode is TLS-only, there's no unencrypted +/// fallback). This replaces a pair of booleans (`use_tls`/`use_ca`) with +/// one type that can only represent states that actually make sense. +pub enum BambuTls { + /// Bambu's shared root CA. Correct for current-generation printers. + BundledCa, + /// A specific certificate instead — for a printer whose firmware + /// doesn't chain to the shared CA. + Custom(Vec), + /// Skip certificate verification entirely. The least safe option, only + /// reasonable because LAN mode never leaves your local network. + Insecure, +} + +impl BambuTls { + /// Turns `Config`'s two raw settings into one of the three states + /// above. `require_valid_cert = false` always means `Insecure`, + /// regardless of whether a custom cert path was also given. + pub fn from_config(cert_path: Option<&Path>, require_valid_cert: bool) -> anyhow::Result { + if !require_valid_cert { + return Ok(BambuTls::Insecure); + } + match cert_path { + Some(path) => Ok(BambuTls::Custom(std::fs::read(path)?)), + None => Ok(BambuTls::BundledCa), + } + } + + /// The CA certificate bytes to hand to the TLS layer — empty when + /// `Insecure`, since there's nothing to verify against. + pub fn ca_bytes(&self) -> &[u8] { + match self { + BambuTls::BundledCa => BUNDLED_CA, + BambuTls::Custom(bytes) => bytes, + BambuTls::Insecure => &[], + } + } +} pub struct BambuPrinter { base: PrinterBase, pub access_code: String, + tls: BambuTls, } impl BambuPrinter { - pub fn new(id: &str, name: &str, host: &str, access_code: &str) -> Self { + pub fn new(id: &str, name: &str, host: &str, access_code: &str, tls: BambuTls) -> Self { Self { base: PrinterBase { id: id.into(), name: name.into(), host: host.into() }, access_code: access_code.into(), + tls, } } - /// Only `BambuPrinter` has this — it's not on the `Printer` trait at - /// all, so `PrusaPrinter`/`KlipperPrinter` simply don't have it. This - /// is what "adding a printer-specific method" looks like: just define - /// it in this struct's own `impl` block. + /// Only `BambuPrinter` has this — it's not on the `GenericPrinter` + /// trait at all, so the Prusa/Klipper structs simply don't have it. + /// This is what "adding a printer-specific method" looks like: just + /// define it in this struct's own `impl` block. pub fn dispatch_gcode(&self, file_name: &str) { println!("[{}] uploading {file_name} over FTPS", self.base.name); } } -impl Printer for BambuPrinter { +impl GenericPrinter for BambuPrinter { fn base(&self) -> &PrinterBase { &self.base } - /// OVERRIDE: Bambu's real connect logic opens an MQTTS session (see - /// `adapters::bambu::run_telemetry` for that). Here we just print what - /// it would do, so the focus stays on how dispatch works. + /// OVERRIDE: Bambu's real connect logic opens an MQTTS session on + /// 8883, presenting `self.tls.ca_bytes()` to the TLS layer to verify + /// (or, for `BambuTls::Insecure`, skipping verification). Wiring that + /// up needs a TLS-capable MQTT client (e.g. rumqttc + rustls) — not + /// added back yet, so this just reports which trust mode it would use. async fn connect(&mut self) -> Result<(), PrinterError> { - println!("[{}] connecting over MQTT with access code {}", self.base.name, self.access_code); + let mode = match &self.tls { + BambuTls::BundledCa => "bundled Bambu CA", + BambuTls::Custom(_) => "custom CA cert", + BambuTls::Insecure => "no certificate verification", + }; + println!("[{}] connecting over MQTTS:8883 ({mode})", self.base.name); Ok(()) } } diff --git a/src/printer/klipper.rs b/src/printer/klipper.rs index a41aea7..088b229 100644 --- a/src/printer/klipper.rs +++ b/src/printer/klipper.rs @@ -1,4 +1,4 @@ -use super::{Printer, PrinterBase, PrinterError}; +use super::{GenericPrinter, PrinterBase, PrinterError}; pub struct KlipperPrinter { base: PrinterBase, @@ -12,7 +12,7 @@ impl KlipperPrinter { } } -impl Printer for KlipperPrinter { +impl GenericPrinter for KlipperPrinter { fn base(&self) -> &PrinterBase { &self.base } diff --git a/src/printer/mod.rs b/src/printer/mod.rs index 932f06b..01417a5 100644 --- a/src/printer/mod.rs +++ b/src/printer/mod.rs @@ -1,5 +1,5 @@ //! Printer hierarchy — the Rust answer to -//! `GenericPrinter -> BambuPrinter/PrusaPrinter/KlipperPrinter`-style +//! `GenericPrinter -> BambuV1/BambuV2/PrusaLink/PrusaSerial/Klipper`-style //! inheritance, since Rust structs can't `extend` each other. //! //! Three pieces, each doing one job an OOP base class would normally do: @@ -7,10 +7,10 @@ //! 1. `PrinterBase` — a plain struct holding the shared *fields*. Every //! printer struct below *has* one of these (composition) instead of //! *extending* one. -//! 2. `Printer` — 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. +//! 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". //! @@ -25,9 +25,9 @@ mod bambu; mod klipper; mod prusa; -pub use bambu::BambuPrinter; +pub use bambu::{BambuPrinter, BambuTls}; pub use klipper::KlipperPrinter; -pub use prusa::PrusaPrinter; +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. @@ -43,9 +43,9 @@ pub struct PrinterBase { pub host: String, } -/// The shared interface — think `interface Printer` (Java/TS) or an ABC -/// (Python). Every vendor's struct implements this. -pub trait Printer { +/// 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; @@ -69,10 +69,20 @@ pub trait Printer { } } +pub enum PrinterFlavour { + BambuV1Printer, + BambuV2Printer, + PrusaLinkPrinter, + PrusaSerialPrinter, + KlipperPrinter, +} + /// The closed set of printer kinds this fleet can talk to. pub enum PrinterHandle { - Bambu(BambuPrinter), - Prusa(PrusaPrinter), + BambuV1(BambuPrinter), + BambuV2(BambuPrinter), + PrusaLink(PrusaLinkPrinter), + PrusaSerial(PrusaSerialPrinter), Klipper(KlipperPrinter), } @@ -80,19 +90,23 @@ pub enum PrinterHandle { // 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 Printer for PrinterHandle { +impl GenericPrinter for PrinterHandle { fn base(&self) -> &PrinterBase { match self { - PrinterHandle::Bambu(p) => p.base(), - PrinterHandle::Prusa(p) => p.base(), + PrinterHandle::BambuV1(p) => p.base(), + PrinterHandle::BambuV2(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::Prusa(p) => p.connect().await, + PrinterHandle::BambuV1(p) => p.connect().await, + PrinterHandle::BambuV2(p) => p.connect().await, + PrinterHandle::PrusaLink(p) => p.connect().await, + PrinterHandle::PrusaSerial(p) => p.connect().await, PrinterHandle::Klipper(p) => p.connect().await, } } diff --git a/src/printer/prusa.rs b/src/printer/prusa.rs index 16caac1..ff50dd7 100644 --- a/src/printer/prusa.rs +++ b/src/printer/prusa.rs @@ -1,11 +1,13 @@ -use super::{Printer, PrinterBase, PrinterError}; +use super::{GenericPrinter, PrinterBase, PrinterError}; -pub struct PrusaPrinter { +/****************** PrusaLink ******************/ + +pub struct PrusaLinkPrinter { base: PrinterBase, pub api_key: String, } -impl PrusaPrinter { +impl PrusaLinkPrinter { pub fn new(id: &str, name: &str, host: &str, api_key: &str) -> Self { Self { base: PrinterBase { id: id.into(), name: name.into(), host: host.into() }, @@ -14,7 +16,7 @@ impl PrusaPrinter { } } -impl Printer for PrusaPrinter { +impl GenericPrinter for PrusaLinkPrinter { fn base(&self) -> &PrinterBase { &self.base } @@ -27,3 +29,35 @@ impl Printer for PrusaPrinter { Ok(()) } } + + +/****************** PrusaSerial ******************/ + +pub struct PrusaSerialPrinter { + base: PrinterBase, + pub com_port: i16, +} + +impl PrusaSerialPrinter { + pub fn new(id: &str, name: &str, com_port: i16) -> Self { + Self { + base: PrinterBase { id: id.into(), name: name.into(), host: "127.0.0.1".to_string() }, + com_port, + } + } +} + +impl GenericPrinter for PrusaSerialPrinter { + fn base(&self) -> &PrinterBase { + &self.base + } + + /// OVERRIDE: a fourth, again-different body — no network host at all, + /// just a local serial port (real version: open the port, e.g. via the + /// `serialport`/`tokio-serial` crate, and speak Prusa's G-code-over- + /// serial protocol). + async fn connect(&mut self) -> Result<(), PrinterError> { + println!("[{}] connecting over serial (COM{})", self.base.name, self.com_port); + Ok(()) + } +}