8572d564b1
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.
91 lines
3.9 KiB
Rust
91 lines
3.9 KiB
Rust
//! Loads real printer connection details from a TOML file — see
|
|
//! `printers.example.toml` for the shape. The actual file (`printers.toml`
|
|
//! by default) is gitignored: it holds real hostnames and access codes for
|
|
//! your printers, which don't belong in version control.
|
|
|
|
use std::path::Path;
|
|
|
|
use serde::Deserialize;
|
|
|
|
use crate::printer::{BambuTls, BambuV1Printer, BambuV2Printer, KlipperPrinter, PrinterHandle, PrusaLinkPrinter, PrusaSerialPrinter};
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
struct FleetFile {
|
|
#[serde(rename = "printer", default)]
|
|
printers: Vec<PrinterEntry>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
struct PrinterEntry {
|
|
id: String,
|
|
name: String,
|
|
/// One of: "bambu_v1", "bambu_v2", "prusa_link", "prusa_serial", "klipper".
|
|
vendor: String,
|
|
host: 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>,
|
|
com_port: Option<i16>,
|
|
/// Path to a printer-specific CA certificate. Leave unset to use the
|
|
/// bundled Bambu CA — only needed for a printer that doesn't chain to
|
|
/// it (P1P, at least; see certs/README.md).
|
|
ca_cert_path: Option<String>,
|
|
#[serde(default = "default_true")]
|
|
require_valid_cert: bool,
|
|
}
|
|
|
|
fn default_true() -> bool {
|
|
true
|
|
}
|
|
|
|
/// Reads and parses a printers file into ready-to-use `PrinterHandle`s.
|
|
pub fn load(path: &Path) -> anyhow::Result<Vec<PrinterHandle>> {
|
|
let text = std::fs::read_to_string(path).map_err(|err| {
|
|
anyhow::anyhow!(
|
|
"failed to read {path:?}: {err} (copy printers.example.toml to printers.toml and fill in your printers)"
|
|
)
|
|
})?;
|
|
let file: FleetFile = toml::from_str(&text)?;
|
|
|
|
file.printers.into_iter().map(build_printer).collect()
|
|
}
|
|
|
|
fn build_printer(entry: PrinterEntry) -> anyhow::Result<PrinterHandle> {
|
|
match entry.vendor.as_str() {
|
|
"bambu_v1" => {
|
|
let host = require_field(&entry.id, "host", entry.host)?;
|
|
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::BambuV1(BambuV1Printer::new(&entry.id, &entry.name, &host, &access_code, &sn, tls)))
|
|
}
|
|
"bambu_v2" => {
|
|
let host = require_field(&entry.id, "host", entry.host)?;
|
|
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" => {
|
|
let host = require_field(&entry.id, "host", entry.host)?;
|
|
let api_key = require_field(&entry.id, "api_key", entry.api_key)?;
|
|
Ok(PrinterHandle::PrusaLink(PrusaLinkPrinter::new(&entry.id, &entry.name, &host, &api_key)))
|
|
}
|
|
"prusa_serial" => {
|
|
let com_port = entry.com_port.ok_or_else(|| anyhow::anyhow!("{}: prusa_serial needs com_port", entry.id))?;
|
|
Ok(PrinterHandle::PrusaSerial(PrusaSerialPrinter::new(&entry.id, &entry.name, com_port)))
|
|
}
|
|
"klipper" => {
|
|
let host = require_field(&entry.id, "host", entry.host)?;
|
|
Ok(PrinterHandle::Klipper(KlipperPrinter::new(&entry.id, &entry.name, &host)))
|
|
}
|
|
other => Err(anyhow::anyhow!("{}: unknown vendor '{other}'", entry.id)),
|
|
}
|
|
}
|
|
|
|
fn require_field(printer_id: &str, field: &str, value: Option<String>) -> anyhow::Result<String> {
|
|
value.ok_or_else(|| anyhow::anyhow!("{printer_id}: missing required field '{field}'"))
|
|
}
|