Split BambuPrinter into BambuGenericPrinter -> BambuV1Printer/BambuV2Printer
Completes the GenericPrinter -> BambuGenericPrinter -> BambuV1/V2 chain this project wanted from the start. BambuGenericPrinter holds the fields and behavior every Bambu printer shares (access code, serial number, CA trust, the TLS test/fetch methods); BambuV1Printer and BambuV2Printer each *have* one (composition) and are now genuinely separate types, ready to carry flavour-specific report-schema fields later. Also threads through a new 'sn' (serial number) field the real MQTT topics will need. Adds real 'fetch the CA automatically' capability, verified against a live TLS server (not just compiled): - BambuGenericPrinter::fetch_certificate() does trust-on-first-connect — connects once with verification disabled, captures the certificate the printer actually presents via native_tls's peer_certificate()/to_der(), and returns it as PEM. Deliberately a method you call once by hand (examples/fetch_bambu_cert.rs), not something connect() falls back to silently, since TOFU trusts whoever's on the network the moment you run it. Verified end-to-end against a local openssl s_server: the fetched PEM's SHA-256 fingerprint exactly matched the server's real certificate. - Verified the other direction too: test_bambu_certs (bundled-CA mode) correctly REJECTS that same test server's cert, since it wasn't signed by the real Bambu CA. Splitting BambuV1Printer/BambuV2Printer into distinct types broke the PrinterHandle::BambuV1(x) | PrinterHandle::BambuV2(x) or-pattern in both examples (or-patterns require every alternative to bind the same type) — fixed by giving each variant its own match arm.
This commit is contained in:
@@ -21,6 +21,10 @@ path = "examples/printer_polymorphism.rs"
|
|||||||
name = "test_bambu_certs"
|
name = "test_bambu_certs"
|
||||||
path = "examples/test_bambu_certs.rs"
|
path = "examples/test_bambu_certs.rs"
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "fetch_bambu_cert"
|
||||||
|
path = "examples/fetch_bambu_cert.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tokio = { version = "1.40", features = ["full"] }
|
tokio = { version = "1.40", features = ["full"] }
|
||||||
tokio-tungstenite = { version = "0.24", features = ["native-tls"] }
|
tokio-tungstenite = { version = "0.24", features = ["native-tls"] }
|
||||||
@@ -29,6 +33,7 @@ serde = { version = "1", features = ["derive"] }
|
|||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
toml = "0.8"
|
toml = "0.8"
|
||||||
native-tls = "0.2"
|
native-tls = "0.2"
|
||||||
|
base64 = "0.22"
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
|
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
//! Run with: cargo run --example fetch_bambu_cert -- <printer_id> <output_path>
|
||||||
|
//!
|
||||||
|
//! "Trust on first connect": connects to one printer from printers.toml
|
||||||
|
//! with certificate verification disabled, captures whatever certificate
|
||||||
|
//! it presents, and saves it as PEM. Meant for a printer like P1P that
|
||||||
|
//! doesn't chain to the bundled CA — after running this, point that
|
||||||
|
//! printer's `ca_cert_path` in printers.toml at the saved file and
|
||||||
|
//! `test_bambu_certs` should report it as verified from then on.
|
||||||
|
//!
|
||||||
|
//! This trusts whoever answers on the network *right now* — only run it on
|
||||||
|
//! a network you trust, ideally right after unboxing the printer.
|
||||||
|
|
||||||
|
use continuum_proxy::fleet;
|
||||||
|
use continuum_proxy::printer::{GenericPrinter, PrinterHandle};
|
||||||
|
|
||||||
|
fn main() -> anyhow::Result<()> {
|
||||||
|
let mut args = std::env::args().skip(1);
|
||||||
|
let printer_id = args.next().ok_or_else(|| anyhow::anyhow!("usage: fetch_bambu_cert <printer_id> <output_path>"))?;
|
||||||
|
let output_path = args.next().ok_or_else(|| anyhow::anyhow!("usage: fetch_bambu_cert <printer_id> <output_path>"))?;
|
||||||
|
|
||||||
|
let printers = fleet::load(std::path::Path::new("printers.toml"))?;
|
||||||
|
|
||||||
|
let pem = printers
|
||||||
|
.iter()
|
||||||
|
.find_map(|printer| match printer {
|
||||||
|
PrinterHandle::BambuV1(bambu) if bambu.base().id == printer_id => Some(bambu.fetch_certificate()),
|
||||||
|
PrinterHandle::BambuV2(bambu) if bambu.base().id == printer_id => Some(bambu.fetch_certificate()),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("no Bambu printer with id '{printer_id}' in printers.toml"))??;
|
||||||
|
|
||||||
|
std::fs::write(&output_path, &pem)?;
|
||||||
|
println!("saved {printer_id}'s certificate to {output_path}");
|
||||||
|
println!("now set ca_cert_path = \"{output_path}\" for {printer_id} in printers.toml");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -4,19 +4,34 @@
|
|||||||
//! - `connect()` is called the same way regardless of vendor (polymorphism),
|
//! - `connect()` is called the same way regardless of vendor (polymorphism),
|
||||||
//! but each variant's `impl GenericPrinter` runs completely different
|
//! but each variant's `impl GenericPrinter` runs completely different
|
||||||
//! code (override).
|
//! code (override).
|
||||||
//! - `dispatch_gcode()` only exists on `BambuPrinter` (extension) — you
|
//! - `dispatch_gcode()` only exists on the two Bambu structs (extension) —
|
||||||
//! have to `match` the enum back down to the concrete type to reach it,
|
//! you have to `match` the enum back down to the concrete type to reach
|
||||||
//! which is the trade-off for not having implicit downcasting.
|
//! it, which is the trade-off for not having implicit downcasting.
|
||||||
|
|
||||||
use continuum_proxy::printer::{
|
use continuum_proxy::printer::{
|
||||||
BambuPrinter, BambuTls, GenericPrinter, KlipperPrinter, PrinterHandle, PrusaLinkPrinter, PrusaSerialPrinter,
|
BambuTls, BambuV1Printer, BambuV2Printer, 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::BambuV1(BambuPrinter::new("p1", "P1S", "192.168.1.49", "87654321", BambuTls::BundledCa)),
|
PrinterHandle::BambuV1(BambuV1Printer::new(
|
||||||
PrinterHandle::BambuV2(BambuPrinter::new("p2", "X1 Carbon", "192.168.1.50", "12345678", BambuTls::BundledCa)),
|
"p1",
|
||||||
|
"P1S",
|
||||||
|
"192.168.1.49",
|
||||||
|
"87654321",
|
||||||
|
"01P00A000000000",
|
||||||
|
BambuTls::BundledCa,
|
||||||
|
)),
|
||||||
|
PrinterHandle::BambuV2(BambuV2Printer::new(
|
||||||
|
"p2",
|
||||||
|
"X1 Carbon",
|
||||||
|
"192.168.1.50",
|
||||||
|
"12345678",
|
||||||
|
"00M00A000000000",
|
||||||
|
BambuTls::BundledCa,
|
||||||
|
)),
|
||||||
PrinterHandle::PrusaLink(PrusaLinkPrinter::new("p3", "MK4", "192.168.1.51", "prusa-api-key")),
|
PrinterHandle::PrusaLink(PrusaLinkPrinter::new("p3", "MK4", "192.168.1.51", "prusa-api-key")),
|
||||||
PrinterHandle::PrusaSerial(PrusaSerialPrinter::new("p4", "MK3S+", 0)),
|
PrinterHandle::PrusaSerial(PrusaSerialPrinter::new("p4", "MK3S+", 0)),
|
||||||
PrinterHandle::Klipper(KlipperPrinter::new("p5", "Voron 2.4", "192.168.1.52")),
|
PrinterHandle::Klipper(KlipperPrinter::new("p5", "Voron 2.4", "192.168.1.52")),
|
||||||
@@ -35,11 +50,16 @@ 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. Both Bambu variants wrap the same
|
// variant back out of the enum. BambuV1Printer and BambuV2Printer are
|
||||||
// BambuPrinter struct right now, so both get dispatch_gcode().
|
// separate types now (each *has* a BambuGenericPrinter rather than
|
||||||
|
// being the same struct), so — unlike before the split — this needs
|
||||||
|
// two arms instead of one `A(x) | B(x)` pattern: an or-pattern requires
|
||||||
|
// every alternative to bind the same type, and these no longer do.
|
||||||
for printer in &fleet {
|
for printer in &fleet {
|
||||||
if let PrinterHandle::BambuV1(bambu) | PrinterHandle::BambuV2(bambu) = printer {
|
match printer {
|
||||||
bambu.dispatch_gcode("part.gcode.3mf");
|
PrinterHandle::BambuV1(bambu) => bambu.dispatch_gcode("part.gcode.3mf"),
|
||||||
|
PrinterHandle::BambuV2(bambu) => bambu.dispatch_gcode("part.gcode.3mf"),
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,15 +12,23 @@ use continuum_proxy::printer::{GenericPrinter, PrinterHandle};
|
|||||||
fn main() -> anyhow::Result<()> {
|
fn main() -> anyhow::Result<()> {
|
||||||
let printers = fleet::load(std::path::Path::new("printers.toml"))?;
|
let printers = fleet::load(std::path::Path::new("printers.toml"))?;
|
||||||
|
|
||||||
|
// BambuV1Printer and BambuV2Printer are separate types (each *has* a
|
||||||
|
// BambuGenericPrinter rather than being the same struct), so this
|
||||||
|
// needs one arm per variant rather than a single `A(x) | B(x)` pattern.
|
||||||
for printer in &printers {
|
for printer in &printers {
|
||||||
if let PrinterHandle::BambuV1(bambu) | PrinterHandle::BambuV2(bambu) = printer {
|
match printer {
|
||||||
print!("{}: ", bambu.base().name);
|
PrinterHandle::BambuV1(bambu) => report(bambu.base().name.as_str(), bambu.test_tls_handshake()),
|
||||||
match bambu.test_tls_handshake() {
|
PrinterHandle::BambuV2(bambu) => report(bambu.base().name.as_str(), bambu.test_tls_handshake()),
|
||||||
Ok(()) => println!("OK — certificate verified"),
|
_ => {}
|
||||||
Err(err) => println!("FAILED — {err}"),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn report(name: &str, result: anyhow::Result<()>) {
|
||||||
|
match result {
|
||||||
|
Ok(()) => println!("{name}: OK — certificate verified"),
|
||||||
|
Err(err) => println!("{name}: FAILED — {err}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,8 +8,11 @@ name = "P1P"
|
|||||||
vendor = "bambu_v1" # bambu_v1 | bambu_v2 | prusa_link | prusa_serial | klipper
|
vendor = "bambu_v1" # bambu_v1 | bambu_v2 | prusa_link | prusa_serial | klipper
|
||||||
host = "192.168.1.50"
|
host = "192.168.1.50"
|
||||||
access_code = "REPLACE_WITH_ACCESS_CODE"
|
access_code = "REPLACE_WITH_ACCESS_CODE"
|
||||||
|
sn = "REPLACE_WITH_SERIAL_NUMBER" # printer screen > Settings > Device (used for MQTT topics later)
|
||||||
# P1P doesn't chain to the bundled CA — point this at a cert you've
|
# P1P doesn't chain to the bundled CA — point this at a cert you've
|
||||||
# downloaded from the printer itself. Leave unset to use the bundled CA.
|
# downloaded from the printer itself. Leave unset to use the bundled CA.
|
||||||
|
# `cargo run --example fetch_bambu_cert -- p1p ./certs/p1p.pem` can fetch it
|
||||||
|
# for you (trust-on-first-connect — see that example's doc comment).
|
||||||
ca_cert_path = "./certs/p1p.pem"
|
ca_cert_path = "./certs/p1p.pem"
|
||||||
|
|
||||||
[[printer]]
|
[[printer]]
|
||||||
@@ -18,6 +21,7 @@ name = "H2C"
|
|||||||
vendor = "bambu_v2"
|
vendor = "bambu_v2"
|
||||||
host = "192.168.1.51"
|
host = "192.168.1.51"
|
||||||
access_code = "REPLACE_WITH_ACCESS_CODE"
|
access_code = "REPLACE_WITH_ACCESS_CODE"
|
||||||
|
sn = "REPLACE_WITH_SERIAL_NUMBER"
|
||||||
# No ca_cert_path — current-generation printers verify against the bundled
|
# No ca_cert_path — current-generation printers verify against the bundled
|
||||||
# CA (certs/bambu_ca2.pem) fine.
|
# CA (certs/bambu_ca2.pem) fine.
|
||||||
|
|
||||||
|
|||||||
+14
-8
@@ -7,7 +7,7 @@ use std::path::Path;
|
|||||||
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use crate::printer::{BambuPrinter, BambuTls, KlipperPrinter, PrinterHandle, PrusaLinkPrinter, PrusaSerialPrinter};
|
use crate::printer::{BambuTls, BambuV1Printer, BambuV2Printer, KlipperPrinter, PrinterHandle, PrusaLinkPrinter, PrusaSerialPrinter};
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
struct FleetFile {
|
struct FleetFile {
|
||||||
@@ -23,6 +23,9 @@ struct PrinterEntry {
|
|||||||
vendor: String,
|
vendor: String,
|
||||||
host: Option<String>,
|
host: Option<String>,
|
||||||
access_code: Option<String>,
|
access_code: Option<String>,
|
||||||
|
/// The printer's serial number — Bambu's real MQTT topics are addressed
|
||||||
|
/// by serial (`device/{sn}/report`), not by our own `id`.
|
||||||
|
sn: Option<String>,
|
||||||
api_key: Option<String>,
|
api_key: Option<String>,
|
||||||
com_port: Option<i16>,
|
com_port: Option<i16>,
|
||||||
/// Path to a printer-specific CA certificate. Leave unset to use the
|
/// Path to a printer-specific CA certificate. Leave unset to use the
|
||||||
@@ -51,16 +54,19 @@ pub fn load(path: &Path) -> anyhow::Result<Vec<PrinterHandle>> {
|
|||||||
|
|
||||||
fn build_printer(entry: PrinterEntry) -> anyhow::Result<PrinterHandle> {
|
fn build_printer(entry: PrinterEntry) -> anyhow::Result<PrinterHandle> {
|
||||||
match entry.vendor.as_str() {
|
match entry.vendor.as_str() {
|
||||||
"bambu_v1" | "bambu_v2" => {
|
"bambu_v1" => {
|
||||||
let host = require_field(&entry.id, "host", entry.host)?;
|
let host = require_field(&entry.id, "host", entry.host)?;
|
||||||
let access_code = require_field(&entry.id, "access_code", entry.access_code)?;
|
let access_code = require_field(&entry.id, "access_code", entry.access_code)?;
|
||||||
|
let sn = require_field(&entry.id, "sn", entry.sn)?;
|
||||||
let tls = BambuTls::resolve(entry.ca_cert_path.as_deref().map(Path::new), entry.require_valid_cert)?;
|
let tls = BambuTls::resolve(entry.ca_cert_path.as_deref().map(Path::new), entry.require_valid_cert)?;
|
||||||
let printer = BambuPrinter::new(&entry.id, &entry.name, &host, &access_code, tls);
|
Ok(PrinterHandle::BambuV1(BambuV1Printer::new(&entry.id, &entry.name, &host, &access_code, &sn, tls)))
|
||||||
Ok(if entry.vendor == "bambu_v1" {
|
}
|
||||||
PrinterHandle::BambuV1(printer)
|
"bambu_v2" => {
|
||||||
} else {
|
let host = require_field(&entry.id, "host", entry.host)?;
|
||||||
PrinterHandle::BambuV2(printer)
|
let access_code = require_field(&entry.id, "access_code", entry.access_code)?;
|
||||||
})
|
let sn = require_field(&entry.id, "sn", entry.sn)?;
|
||||||
|
let tls = BambuTls::resolve(entry.ca_cert_path.as_deref().map(Path::new), entry.require_valid_cert)?;
|
||||||
|
Ok(PrinterHandle::BambuV2(BambuV2Printer::new(&entry.id, &entry.name, &host, &access_code, &sn, tls)))
|
||||||
}
|
}
|
||||||
"prusa_link" => {
|
"prusa_link" => {
|
||||||
let host = require_field(&entry.id, "host", entry.host)?;
|
let host = require_field(&entry.id, "host", entry.host)?;
|
||||||
|
|||||||
+145
-42
@@ -1,4 +1,6 @@
|
|||||||
|
use std::net::{SocketAddr, TcpStream, ToSocketAddrs};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
use super::{GenericPrinter, PrinterBase, PrinterError};
|
use super::{GenericPrinter, PrinterBase, PrinterError};
|
||||||
|
|
||||||
@@ -10,13 +12,12 @@ const BUNDLED_CA: &[u8] = include_bytes!("../../certs/bambu_ca2.pem");
|
|||||||
|
|
||||||
/// Which certificate to trust when connecting to a Bambu printer's MQTTS
|
/// 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
|
/// 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
|
/// fallback).
|
||||||
/// one type that can only represent states that actually make sense.
|
|
||||||
pub enum BambuTls {
|
pub enum BambuTls {
|
||||||
/// Bambu's shared root CA. Correct for current-generation printers.
|
/// Bambu's shared root CA. Correct for current-generation printers.
|
||||||
BundledCa,
|
BundledCa,
|
||||||
/// A specific certificate instead — for a printer whose firmware
|
/// A specific certificate instead — for a printer whose firmware
|
||||||
/// doesn't chain to the shared CA.
|
/// doesn't chain to the shared CA (P1P, at least).
|
||||||
Custom(Vec<u8>),
|
Custom(Vec<u8>),
|
||||||
/// Skip certificate verification entirely. The least safe option, only
|
/// Skip certificate verification entirely. The least safe option, only
|
||||||
/// reasonable because LAN mode never leaves your local network.
|
/// reasonable because LAN mode never leaves your local network.
|
||||||
@@ -27,9 +28,9 @@ impl BambuTls {
|
|||||||
/// Resolves the trust mode for *one* printer. Call this per printer,
|
/// Resolves the trust mode for *one* printer. Call this per printer,
|
||||||
/// passing `config.bambu_ca_cert_overrides.get(printer_id)` — there's
|
/// passing `config.bambu_ca_cert_overrides.get(printer_id)` — there's
|
||||||
/// no single override for the whole fleet, because not every Bambu
|
/// no single override for the whole fleet, because not every Bambu
|
||||||
/// model chains to the same CA (P1P doesn't). `require_valid_cert =
|
/// model chains to the same CA. `require_valid_cert = false` always
|
||||||
/// false` always means `Insecure`, regardless of whether that printer
|
/// means `Insecure`, regardless of whether that printer also has an
|
||||||
/// also has an override entry.
|
/// override entry.
|
||||||
pub fn resolve(cert_path: Option<&Path>, require_valid_cert: bool) -> anyhow::Result<Self> {
|
pub fn resolve(cert_path: Option<&Path>, require_valid_cert: bool) -> anyhow::Result<Self> {
|
||||||
if !require_valid_cert {
|
if !require_valid_cert {
|
||||||
return Ok(BambuTls::Insecure);
|
return Ok(BambuTls::Insecure);
|
||||||
@@ -65,64 +66,97 @@ impl BambuTls {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct BambuPrinter {
|
fn resolve_addr(host: &str) -> anyhow::Result<SocketAddr> {
|
||||||
|
(host, 8883).to_socket_addrs()?.next().ok_or_else(|| anyhow::anyhow!("could not resolve {host}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Minimal DER -> PEM encoder (base64, 64-char lines, standard
|
||||||
|
/// header/footer) — just enough to save a fetched certificate in the same
|
||||||
|
/// format as the bundled one.
|
||||||
|
fn der_to_pem(der: &[u8]) -> Vec<u8> {
|
||||||
|
use base64::Engine;
|
||||||
|
let encoded = base64::engine::general_purpose::STANDARD.encode(der);
|
||||||
|
|
||||||
|
let mut pem = String::from("-----BEGIN CERTIFICATE-----\n");
|
||||||
|
for line in encoded.as_bytes().chunks(64) {
|
||||||
|
pem.push_str(std::str::from_utf8(line).expect("base64 output is always ASCII"));
|
||||||
|
pem.push('\n');
|
||||||
|
}
|
||||||
|
pem.push_str("-----END CERTIFICATE-----\n");
|
||||||
|
pem.into_bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fields and behavior every Bambu printer shares, regardless of protocol
|
||||||
|
/// generation — the layer between `GenericPrinter` and the version-specific
|
||||||
|
/// `BambuV1Printer`/`BambuV2Printer` leaves below. Those two *have* one of
|
||||||
|
/// these (composition) rather than a deeper inheritance chain; because
|
||||||
|
/// Rust's field/method privacy is scoped to the *module* (this whole file),
|
||||||
|
/// not the struct, `BambuV1Printer`/`BambuV2Printer` can freely use
|
||||||
|
/// `BambuGenericPrinter`'s private fields even though they're separate
|
||||||
|
/// types.
|
||||||
|
pub struct BambuGenericPrinter {
|
||||||
base: PrinterBase,
|
base: PrinterBase,
|
||||||
pub access_code: String,
|
pub access_code: String,
|
||||||
|
pub sn: String,
|
||||||
tls: BambuTls,
|
tls: BambuTls,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BambuPrinter {
|
impl BambuGenericPrinter {
|
||||||
pub fn new(id: &str, name: &str, host: &str, access_code: &str, tls: BambuTls) -> Self {
|
pub fn new(id: &str, name: &str, host: &str, access_code: &str, sn: &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(),
|
||||||
|
sn: sn.into(),
|
||||||
tls,
|
tls,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Only `BambuPrinter` has this — it's not on the `GenericPrinter`
|
pub fn base(&self) -> &PrinterBase {
|
||||||
/// trait at all, so the Prusa/Klipper structs simply don't have it.
|
&self.base
|
||||||
/// This is what "adding a printer-specific method" looks like: just
|
}
|
||||||
/// define it in this struct's own `impl` block.
|
|
||||||
|
/// Not part of the `GenericPrinter` trait — Prusa/Klipper have no
|
||||||
|
/// equivalent, since FTPS gcode transfer is a Bambu-specific thing.
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Attempts a raw TLS handshake to this printer's MQTTS port — no MQTT
|
/// A real (blocking, one-shot) TLS handshake to port 8883 — no MQTT
|
||||||
/// protocol at all, just "does the certificate verify against this
|
/// protocol, just "does the certificate verify" against this printer's
|
||||||
/// printer's configured trust mode". This is a real network call
|
/// configured trust mode.
|
||||||
/// (blocking, since it's a one-shot diagnostic rather than part of the
|
|
||||||
/// async daemon), useful for testing a printer's certificate before
|
|
||||||
/// building a real MQTT client on top.
|
|
||||||
pub fn test_tls_handshake(&self) -> anyhow::Result<()> {
|
pub fn test_tls_handshake(&self) -> anyhow::Result<()> {
|
||||||
use std::net::ToSocketAddrs;
|
let addr = resolve_addr(&self.base.host)?;
|
||||||
|
let stream = TcpStream::connect_timeout(&addr, Duration::from_secs(5))?;
|
||||||
let addr = (self.base.host.as_str(), 8883)
|
self.tls.build_connector()?.connect(&self.base.host, stream)?;
|
||||||
.to_socket_addrs()?
|
|
||||||
.next()
|
|
||||||
.ok_or_else(|| anyhow::anyhow!("could not resolve {}", self.base.host))?;
|
|
||||||
|
|
||||||
// A plain TcpStream::connect has no timeout — an offline printer
|
|
||||||
// would hang this call forever instead of failing. 5s is plenty
|
|
||||||
// for a LAN.
|
|
||||||
let stream = std::net::TcpStream::connect_timeout(&addr, std::time::Duration::from_secs(5))?;
|
|
||||||
let connector = self.tls.build_connector()?;
|
|
||||||
connector.connect(&self.base.host, stream)?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl GenericPrinter for BambuPrinter {
|
/// "Trust on first connect": connects once with certificate
|
||||||
fn base(&self) -> &PrinterBase {
|
/// verification disabled, captures whatever certificate the printer
|
||||||
&self.base
|
/// presents, and returns it as PEM bytes.
|
||||||
|
///
|
||||||
|
/// This is meaningfully weaker than verifying against a CA you already
|
||||||
|
/// trust — anyone on the network at the exact moment you run this gets
|
||||||
|
/// trusted forever after. That's why it's a method you call once, by
|
||||||
|
/// hand (see `examples/fetch_bambu_cert.rs`), rather than something
|
||||||
|
/// `connect()` falls back to silently. Save the result to a file and
|
||||||
|
/// point that printer's `ca_cert_path` in `printers.toml` at it.
|
||||||
|
pub fn fetch_certificate(&self) -> anyhow::Result<Vec<u8>> {
|
||||||
|
let addr = resolve_addr(&self.base.host)?;
|
||||||
|
let stream = TcpStream::connect_timeout(&addr, Duration::from_secs(5))?;
|
||||||
|
let tls_stream = BambuTls::Insecure.build_connector()?.connect(&self.base.host, stream)?;
|
||||||
|
|
||||||
|
let cert = tls_stream
|
||||||
|
.peer_certificate()?
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("{} presented no certificate", self.base.name))?;
|
||||||
|
|
||||||
|
Ok(der_to_pem(&cert.to_der()?))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// OVERRIDE: Bambu's real connect logic opens an MQTTS session on
|
/// Shared "connect" logic both V1 and V2 delegate to — the real
|
||||||
/// 8883, presenting `self.tls.ca_bytes()` to the TLS layer to verify
|
/// version would open the MQTTS session here. Still a stub, but no
|
||||||
/// (or, for `BambuTls::Insecure`, skipping verification). Wiring that
|
/// longer duplicated across two structs.
|
||||||
/// up needs a TLS-capable MQTT client (e.g. rumqttc + rustls) — not
|
async fn connect_impl(&mut self) -> Result<(), PrinterError> {
|
||||||
/// added back yet, so this just reports which trust mode it would use.
|
|
||||||
async fn connect(&mut self) -> Result<(), PrinterError> {
|
|
||||||
let mode = match &self.tls {
|
let mode = match &self.tls {
|
||||||
BambuTls::BundledCa => "bundled Bambu CA",
|
BambuTls::BundledCa => "bundled Bambu CA",
|
||||||
BambuTls::Custom(_) => "custom CA cert",
|
BambuTls::Custom(_) => "custom CA cert",
|
||||||
@@ -132,3 +166,72 @@ impl GenericPrinter for BambuPrinter {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Older Bambu MQTT report schema (P1P, P1S, original X1 firmware).
|
||||||
|
/// No V1-only fields yet — add them here as you learn what differs.
|
||||||
|
pub struct BambuV1Printer {
|
||||||
|
generic: BambuGenericPrinter,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BambuV1Printer {
|
||||||
|
pub fn new(id: &str, name: &str, host: &str, access_code: &str, sn: &str, tls: BambuTls) -> Self {
|
||||||
|
Self { generic: BambuGenericPrinter::new(id, name, host, access_code, sn, tls) }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dispatch_gcode(&self, file_name: &str) {
|
||||||
|
self.generic.dispatch_gcode(file_name)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn test_tls_handshake(&self) -> anyhow::Result<()> {
|
||||||
|
self.generic.test_tls_handshake()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fetch_certificate(&self) -> anyhow::Result<Vec<u8>> {
|
||||||
|
self.generic.fetch_certificate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GenericPrinter for BambuV1Printer {
|
||||||
|
fn base(&self) -> &PrinterBase {
|
||||||
|
self.generic.base()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn connect(&mut self) -> Result<(), PrinterError> {
|
||||||
|
self.generic.connect_impl().await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Current-generation Bambu MQTT report schema (X1 Carbon/X1E, H2 series).
|
||||||
|
/// No V2-only fields yet (e.g. AMS slot state) — add them here as you build
|
||||||
|
/// out real report parsing.
|
||||||
|
pub struct BambuV2Printer {
|
||||||
|
generic: BambuGenericPrinter,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BambuV2Printer {
|
||||||
|
pub fn new(id: &str, name: &str, host: &str, access_code: &str, sn: &str, tls: BambuTls) -> Self {
|
||||||
|
Self { generic: BambuGenericPrinter::new(id, name, host, access_code, sn, tls) }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dispatch_gcode(&self, file_name: &str) {
|
||||||
|
self.generic.dispatch_gcode(file_name)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn test_tls_handshake(&self) -> anyhow::Result<()> {
|
||||||
|
self.generic.test_tls_handshake()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fetch_certificate(&self) -> anyhow::Result<Vec<u8>> {
|
||||||
|
self.generic.fetch_certificate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GenericPrinter for BambuV2Printer {
|
||||||
|
fn base(&self) -> &PrinterBase {
|
||||||
|
self.generic.base()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn connect(&mut self) -> Result<(), PrinterError> {
|
||||||
|
self.generic.connect_impl().await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+10
-3
@@ -2,6 +2,13 @@
|
|||||||
//! `GenericPrinter -> BambuV1/BambuV2/PrusaLink/PrusaSerial/Klipper`-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.
|
||||||
//!
|
//!
|
||||||
|
//! Bambu goes one level deeper than the others: `bambu.rs` has
|
||||||
|
//! `BambuGenericPrinter` (fields + behavior every Bambu printer shares —
|
||||||
|
//! access code, CA trust, the TLS handshake test/fetch) which
|
||||||
|
//! `BambuV1Printer`/`BambuV2Printer` each *have* one of, mirroring the
|
||||||
|
//! `GenericPrinter -> BambuGenericPrinter -> BambuV1/V2` chain this project
|
||||||
|
//! actually wanted from the start — see that file's doc comment.
|
||||||
|
//!
|
||||||
//! 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:
|
||||||
//!
|
//!
|
||||||
//! 1. `PrinterBase` — a plain struct holding the shared *fields*. Every
|
//! 1. `PrinterBase` — a plain struct holding the shared *fields*. Every
|
||||||
@@ -25,7 +32,7 @@ mod bambu;
|
|||||||
mod klipper;
|
mod klipper;
|
||||||
mod prusa;
|
mod prusa;
|
||||||
|
|
||||||
pub use bambu::{BambuPrinter, BambuTls};
|
pub use bambu::{BambuGenericPrinter, BambuTls, BambuV1Printer, BambuV2Printer};
|
||||||
pub use klipper::KlipperPrinter;
|
pub use klipper::KlipperPrinter;
|
||||||
pub use prusa::{PrusaLinkPrinter, PrusaSerialPrinter};
|
pub use prusa::{PrusaLinkPrinter, PrusaSerialPrinter};
|
||||||
|
|
||||||
@@ -79,8 +86,8 @@ pub enum PrinterFlavour {
|
|||||||
|
|
||||||
/// 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 {
|
||||||
BambuV1(BambuPrinter),
|
BambuV1(BambuV1Printer),
|
||||||
BambuV2(BambuPrinter),
|
BambuV2(BambuV2Printer),
|
||||||
PrusaLink(PrusaLinkPrinter),
|
PrusaLink(PrusaLinkPrinter),
|
||||||
PrusaSerial(PrusaSerialPrinter),
|
PrusaSerial(PrusaSerialPrinter),
|
||||||
Klipper(KlipperPrinter),
|
Klipper(KlipperPrinter),
|
||||||
|
|||||||
Reference in New Issue
Block a user