Files
continuum-proxy/src/fleet.rs
T
octoturge 8572d564b1 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.
2026-08-28 21:04:56 +00:00

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}'"))
}