//! 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, } #[derive(Debug, Deserialize)] struct PrinterEntry { id: String, name: String, /// One of: "bambu_v1", "bambu_v2", "prusa_link", "prusa_serial", "klipper". vendor: String, host: Option, access_code: Option, /// The printer's serial number — Bambu's real MQTT topics are addressed /// by serial (`device/{sn}/report`), not by our own `id`. sn: Option, api_key: Option, com_port: Option, /// 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, #[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> { 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 { 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) -> anyhow::Result { value.ok_or_else(|| anyhow::anyhow!("{printer_id}: missing required field '{field}'")) }