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.
This commit is contained in:
@@ -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.
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
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
|
||||||
|
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-----
|
||||||
@@ -2,27 +2,31 @@
|
|||||||
//!
|
//!
|
||||||
//! Demonstrates the trait+enum pattern that stands in for inheritance:
|
//! Demonstrates the trait+enum pattern that stands in for inheritance:
|
||||||
//! - `connect()` is called the same way regardless of vendor (polymorphism),
|
//! - `connect()` is called the same way regardless of vendor (polymorphism),
|
||||||
//! but each vendor's `impl Printer` runs completely different code
|
//! but each variant's `impl GenericPrinter` runs completely different
|
||||||
//! (override).
|
//! code (override).
|
||||||
//! - `dispatch_gcode()` only exists on `BambuPrinter` (extension) — you
|
//! - `dispatch_gcode()` only exists on `BambuPrinter` (extension) — you
|
||||||
//! have to `match` the enum back down to the concrete type to reach it,
|
//! have to `match` the enum back down to the concrete type to reach it,
|
||||||
//! which is the trade-off for not having implicit downcasting.
|
//! 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]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
let mut fleet: Vec<PrinterHandle> = vec![
|
let mut fleet: Vec<PrinterHandle> = vec![
|
||||||
PrinterHandle::Bambu(BambuPrinter::new("p1", "X1 Carbon", "192.168.1.50", "12345678")),
|
PrinterHandle::BambuV1(BambuPrinter::new("p1", "P1S", "192.168.1.49", "87654321", BambuTls::BundledCa)),
|
||||||
PrinterHandle::Prusa(PrusaPrinter::new("p2", "MK4", "192.168.1.51", "prusa-api-key")),
|
PrinterHandle::BambuV2(BambuPrinter::new("p2", "X1 Carbon", "192.168.1.50", "12345678", BambuTls::BundledCa)),
|
||||||
PrinterHandle::Klipper(KlipperPrinter::new("p3", "Voron 2.4", "192.168.1.52")),
|
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.
|
// 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
|
// Uniform call site: every printer connects the same way from the
|
||||||
// caller's point of view, even though the three `connect()` bodies
|
// caller's point of view, even though each variant's `connect()` body
|
||||||
// are unrelated implementations.
|
// is an unrelated implementation.
|
||||||
for printer in &mut fleet {
|
for printer in &mut fleet {
|
||||||
match printer.connect().await {
|
match printer.connect().await {
|
||||||
Ok(()) => println!(" -> connected: {}\n", printer.display_name()),
|
Ok(()) => println!(" -> connected: {}\n", printer.display_name()),
|
||||||
@@ -31,9 +35,10 @@ async fn main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Vendor-only extension: reachable only after matching the concrete
|
// 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 {
|
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");
|
bambu.dispatch_gcode("part.gcode.3mf");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use std::env;
|
use std::env;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
@@ -8,6 +9,16 @@ pub struct Config {
|
|||||||
pub uplink_url: String,
|
pub uplink_url: String,
|
||||||
pub go2rtc_bin: String,
|
pub go2rtc_bin: String,
|
||||||
pub go2rtc_config: 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<PathBuf>,
|
||||||
|
/// 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 {
|
impl Config {
|
||||||
@@ -19,6 +30,9 @@ impl Config {
|
|||||||
uplink_url: env_or("CONTINUUM_UPLINK_URL", "wss://api.continuum.local/ws/edge/v1"),
|
uplink_url: env_or("CONTINUUM_UPLINK_URL", "wss://api.continuum.local/ws/edge/v1"),
|
||||||
go2rtc_bin: env_or("CONTINUUM_GO2RTC_BIN", "go2rtc"),
|
go2rtc_bin: env_or("CONTINUUM_GO2RTC_BIN", "go2rtc"),
|
||||||
go2rtc_config: env_or("CONTINUUM_GO2RTC_CONFIG", "./go2rtc.yaml"),
|
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",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+68
-11
@@ -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<u8>),
|
||||||
|
/// 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<Self> {
|
||||||
|
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 {
|
pub struct BambuPrinter {
|
||||||
base: PrinterBase,
|
base: PrinterBase,
|
||||||
pub access_code: String,
|
pub access_code: String,
|
||||||
|
tls: BambuTls,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BambuPrinter {
|
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 {
|
Self {
|
||||||
base: PrinterBase { id: id.into(), name: name.into(), host: host.into() },
|
base: PrinterBase { id: id.into(), name: name.into(), host: host.into() },
|
||||||
access_code: access_code.into(),
|
access_code: access_code.into(),
|
||||||
|
tls,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Only `BambuPrinter` has this — it's not on the `Printer` trait at
|
/// Only `BambuPrinter` has this — it's not on the `GenericPrinter`
|
||||||
/// all, so `PrusaPrinter`/`KlipperPrinter` simply don't have it. This
|
/// trait at all, so the Prusa/Klipper structs simply don't have it.
|
||||||
/// is what "adding a printer-specific method" looks like: just define
|
/// This is what "adding a printer-specific method" looks like: just
|
||||||
/// it in this struct's own `impl` block.
|
/// define it in this struct's own `impl` block.
|
||||||
pub fn dispatch_gcode(&self, file_name: &str) {
|
pub fn dispatch_gcode(&self, file_name: &str) {
|
||||||
println!("[{}] uploading {file_name} over FTPS", self.base.name);
|
println!("[{}] uploading {file_name} over FTPS", self.base.name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Printer for BambuPrinter {
|
impl GenericPrinter for BambuPrinter {
|
||||||
fn base(&self) -> &PrinterBase {
|
fn base(&self) -> &PrinterBase {
|
||||||
&self.base
|
&self.base
|
||||||
}
|
}
|
||||||
|
|
||||||
/// OVERRIDE: Bambu's real connect logic opens an MQTTS session (see
|
/// OVERRIDE: Bambu's real connect logic opens an MQTTS session on
|
||||||
/// `adapters::bambu::run_telemetry` for that). Here we just print what
|
/// 8883, presenting `self.tls.ca_bytes()` to the TLS layer to verify
|
||||||
/// it would do, so the focus stays on how dispatch works.
|
/// (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> {
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use super::{Printer, PrinterBase, PrinterError};
|
use super::{GenericPrinter, PrinterBase, PrinterError};
|
||||||
|
|
||||||
pub struct KlipperPrinter {
|
pub struct KlipperPrinter {
|
||||||
base: PrinterBase,
|
base: PrinterBase,
|
||||||
@@ -12,7 +12,7 @@ impl KlipperPrinter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Printer for KlipperPrinter {
|
impl GenericPrinter for KlipperPrinter {
|
||||||
fn base(&self) -> &PrinterBase {
|
fn base(&self) -> &PrinterBase {
|
||||||
&self.base
|
&self.base
|
||||||
}
|
}
|
||||||
|
|||||||
+31
-17
@@ -1,5 +1,5 @@
|
|||||||
//! Printer hierarchy — the Rust answer to
|
//! 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.
|
//! inheritance, since Rust structs can't `extend` each other.
|
||||||
//!
|
//!
|
||||||
//! Three pieces, each doing one job an OOP base class would normally do:
|
//! 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
|
//! 1. `PrinterBase` — a plain struct holding the shared *fields*. Every
|
||||||
//! printer struct below *has* one of these (composition) instead of
|
//! printer struct below *has* one of these (composition) instead of
|
||||||
//! *extending* one.
|
//! *extending* one.
|
||||||
//! 2. `Printer` — a trait holding the shared *behavior*. `connect()` has no
|
//! 2. `GenericPrinter` — a trait holding the shared *behavior*. `connect()`
|
||||||
//! default body, so every printer type is forced to write its own —
|
//! has no default body, so every printer type is forced to write its
|
||||||
//! that's what "overriding" looks like when there's no inherited body to
|
//! own — that's what "overriding" looks like when there's no inherited
|
||||||
//! override in the first place.
|
//! body to override in the first place.
|
||||||
//! 3. `PrinterHandle` — an enum listing the closed set of printer kinds.
|
//! 3. `PrinterHandle` — an enum listing the closed set of printer kinds.
|
||||||
//! Stands in for "any subclass of GenericPrinter".
|
//! Stands in for "any subclass of GenericPrinter".
|
||||||
//!
|
//!
|
||||||
@@ -25,9 +25,9 @@ mod bambu;
|
|||||||
mod klipper;
|
mod klipper;
|
||||||
mod prusa;
|
mod prusa;
|
||||||
|
|
||||||
pub use bambu::BambuPrinter;
|
pub use bambu::{BambuPrinter, BambuTls};
|
||||||
pub use klipper::KlipperPrinter;
|
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
|
/// A tiny error type: just a message. `String` would work too — this exists
|
||||||
/// mainly so `?` has something concrete to convert into everywhere.
|
/// mainly so `?` has something concrete to convert into everywhere.
|
||||||
@@ -43,9 +43,9 @@ pub struct PrinterBase {
|
|||||||
pub host: String,
|
pub host: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The shared interface — think `interface Printer` (Java/TS) or an ABC
|
/// The shared interface — think `interface GenericPrinter` (Java/TS) or an
|
||||||
/// (Python). Every vendor's struct implements this.
|
/// ABC (Python). Every vendor's struct implements this.
|
||||||
pub trait Printer {
|
pub trait GenericPrinter {
|
||||||
/// Read-only access to the shared fields.
|
/// Read-only access to the shared fields.
|
||||||
fn base(&self) -> &PrinterBase;
|
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.
|
/// The closed set of printer kinds this fleet can talk to.
|
||||||
pub enum PrinterHandle {
|
pub enum PrinterHandle {
|
||||||
Bambu(BambuPrinter),
|
BambuV1(BambuPrinter),
|
||||||
Prusa(PrusaPrinter),
|
BambuV2(BambuPrinter),
|
||||||
|
PrusaLink(PrusaLinkPrinter),
|
||||||
|
PrusaSerial(PrusaSerialPrinter),
|
||||||
Klipper(KlipperPrinter),
|
Klipper(KlipperPrinter),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,19 +90,23 @@ pub enum PrinterHandle {
|
|||||||
// and forwards to that variant's own implementation. This one block is the
|
// and forwards to that variant's own implementation. This one block is the
|
||||||
// only "boilerplate tax" for not having inheritance — everything else reads
|
// only "boilerplate tax" for not having inheritance — everything else reads
|
||||||
// like normal code.
|
// like normal code.
|
||||||
impl Printer for PrinterHandle {
|
impl GenericPrinter for PrinterHandle {
|
||||||
fn base(&self) -> &PrinterBase {
|
fn base(&self) -> &PrinterBase {
|
||||||
match self {
|
match self {
|
||||||
PrinterHandle::Bambu(p) => p.base(),
|
PrinterHandle::BambuV1(p) => p.base(),
|
||||||
PrinterHandle::Prusa(p) => p.base(),
|
PrinterHandle::BambuV2(p) => p.base(),
|
||||||
|
PrinterHandle::PrusaLink(p) => p.base(),
|
||||||
|
PrinterHandle::PrusaSerial(p) => p.base(),
|
||||||
PrinterHandle::Klipper(p) => p.base(),
|
PrinterHandle::Klipper(p) => p.base(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn connect(&mut self) -> Result<(), PrinterError> {
|
async fn connect(&mut self) -> Result<(), PrinterError> {
|
||||||
match self {
|
match self {
|
||||||
PrinterHandle::Bambu(p) => p.connect().await,
|
PrinterHandle::BambuV1(p) => p.connect().await,
|
||||||
PrinterHandle::Prusa(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,
|
PrinterHandle::Klipper(p) => p.connect().await,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+38
-4
@@ -1,11 +1,13 @@
|
|||||||
use super::{Printer, PrinterBase, PrinterError};
|
use super::{GenericPrinter, PrinterBase, PrinterError};
|
||||||
|
|
||||||
pub struct PrusaPrinter {
|
/****************** PrusaLink ******************/
|
||||||
|
|
||||||
|
pub struct PrusaLinkPrinter {
|
||||||
base: PrinterBase,
|
base: PrinterBase,
|
||||||
pub api_key: String,
|
pub api_key: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PrusaPrinter {
|
impl PrusaLinkPrinter {
|
||||||
pub fn new(id: &str, name: &str, host: &str, api_key: &str) -> Self {
|
pub fn new(id: &str, name: &str, host: &str, api_key: &str) -> Self {
|
||||||
Self {
|
Self {
|
||||||
base: PrinterBase { id: id.into(), name: name.into(), host: host.into() },
|
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 {
|
fn base(&self) -> &PrinterBase {
|
||||||
&self.base
|
&self.base
|
||||||
}
|
}
|
||||||
@@ -27,3 +29,35 @@ impl Printer for PrusaPrinter {
|
|||||||
Ok(())
|
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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user