Update docs and prune examples superseded by real functionality

README/certs/README/.env.example/printers.example.toml were all still
describing this as a learning-stage stub with no real printer protocol
clients - rewritten to reflect the real Bambu MQTT client, cert
auto-pinning, and marlin serial connection that now exist.

- Remove examples/fetch_bambu_cert.rs: its manual fetch-and-save workflow
  is now what BambuPrinter::connect()'s ensure_trusted does automatically
  on every connect, so it has no remaining use case.
- Fix examples/printer_polymorphism.rs, which no longer compiled -
  dispatch_gcode was replaced by real methods earlier this session;
  swapped in print_status() as the vendor-only-extension example.
- Remove Config::bambu_ca_cert_overrides/bambu_require_valid_cert and the
  .env vars documenting them - confirmed dead (nothing read them),
  fully superseded by printers.toml's per-printer ca_cert_path/
  require_valid_cert plus automatic trust-on-first-connect pinning.
- Fix stale doc comments in printer/mod.rs and printer/bambu.rs that
  described certificate trust and MQTT as not built yet, or referencing
  the now-removed example.

No behavior change in the daemon itself - verified with cargo build
--examples and a live run of printer_polymorphism and auto_connect_bambu
against the real P1P/H2C printers.
This commit is contained in:
2026-09-07 16:17:15 +00:00
parent 23f157d07a
commit 305c41993d
11 changed files with 131 additions and 144 deletions
+5 -6
View File
@@ -10,11 +10,10 @@ CONTINUUM_UPLINK_URL=wss://api.continuum.local/ws/edge/v1
CONTINUUM_GO2RTC_BIN=/usr/local/bin/go2rtc
CONTINUUM_GO2RTC_CONFIG=./go2rtc.yaml
# Bambu MQTTS certificate trust. Most printers verify fine against the
# bundled shared CA (certs/bambu_ca2.pem) and need nothing here. For a
# printer that doesn't chain to it (P1P, at least), add "printer_id=path"
# entries, comma-separated: CONTINUUM_BAMBU_CA_CERT_OVERRIDES=p1=./certs/p1p.pem
CONTINUUM_BAMBU_CA_CERT_OVERRIDES=
CONTINUUM_BAMBU_REQUIRE_VALID_CERT=true
# Fleet config - see printers.example.toml. Bambu MQTTS certificate trust
# (per-printer ca_cert_path/require_valid_cert, and automatic
# trust-on-first-connect pinning otherwise) is configured per printer in
# that file, not here.
CONTINUUM_PRINTERS_PATH=printers.toml
RUST_LOG=info,continuum_proxy=debug
-4
View File
@@ -21,10 +21,6 @@ path = "examples/printer_polymorphism.rs"
name = "test_bambu_certs"
path = "examples/test_bambu_certs.rs"
[[example]]
name = "fetch_bambu_cert"
path = "examples/fetch_bambu_cert.rs"
[[example]]
name = "auto_connect_bambu"
path = "examples/auto_connect_bambu.rs"
+67 -39
View File
@@ -4,62 +4,90 @@ Edge gateway daemon for the Continuum print farm platform. Runs on a Linux
SBC on-site, talks to printers over the LAN, and keeps a connection to the
cloud control plane (`continuum-backend`).
## This is a learning-stage boilerplate
## What's real right now
This repo is deliberately minimal right now - real printer protocol clients
(Bambu MQTT+FTPS, PrusaLink REST, Klipper/Moonraker WebSocket) are **not**
implemented yet. Each vendor is a stub that just prints what it would do
(`src/printer/bambu.rs`, `prusa.rs`, `klipper.rs`). The idea is to learn
Rust's polymorphism pattern (trait + enum, since Rust has no class
inheritance) on something simple before adding real networking on top.
- **Bambu MQTT** - a real client for both protocol generations (MQTT
v3.1.1+BambuMQTT v1 on older firmware, MQTT v5+BambuMQTT v2 on current
firmware), auto-detected per printer on connect (a firmware update can
move the same physical printer between them, so this isn't something
config can hard-code once and forget - see `printer/bambu.rs`'s
`BambuProtocolPreference::Auto`). Request/reply correlation via
`sequence_id`, an in-memory `print.push_status` cache that merges v1's
partial updates and replaces on v2's full ones, and TLS trust that
auto-pins a printer's certificate via trust-on-first-connect the first
time the bundled CA doesn't verify (`printer/bambu.rs`'s
`ensure_trusted`) - see `printer/bambu_mqtt.rs`'s doc comment for the
full protocol write-up.
- **Marlin/RepRap serial** - a real held-open serial connection that paces
gcode lines by Marlin's own "ok" acknowledgment (`printer/marlin_serial.rs`).
- **Wire shapes from schemas, not hand duplication** - Bambu's raw MQTT
JSON is built straight from `continuum-schemas`' JSON Schema files via
`continuum-schema-instance`, instead of a second hand-written copy of
every request shape drifting out of sync with the schema.
- **Cloud uplink** - a WebSocket client to `continuum-backend` that
connects, sends a heartbeat on a timer, and reconnects on drop
(`src/uplink/`).
- **go2rtc watchdog** - restarts the camera-restreaming subprocess if it
dies (`src/go2rtc.rs`).
## What's still a stub
PrusaLink (REST) and Klipper/Moonraker (WebSocket) - `printer/prusa.rs`
and `printer/klipper.rs` just `println!` what they'd send. Also not built
yet: dispatching a cloud-issued command to a connected printer (the uplink
and the printer fleet run side by side in `main.rs`, but nothing routes
between them yet), local buffering for telemetry across connectivity gaps,
LAN printer discovery (SSDP/mDNS), and the mechanical plate-changer
interface.
## Getting started
```bash
cp .env.example .env
cargo run # the daemon: cloud uplink + go2rtc watchdog
cargo run --example printer_polymorphism # standalone demo, no network/env needed
cargo run # the daemon: connects every configured printer, then runs the cloud uplink + go2rtc watchdog
cp printers.example.toml printers.toml # fill in your real printers (gitignored)
cargo run --example test_bambu_certs # real TLS handshake test against each Bambu printer
cargo run --example auto_connect_bambu # checks/auto-pins each Bambu printer's certificate, no MQTT needed
cargo run --example test_bambu_certs # narrower: just the raw TLS handshake, no auto-pinning
cargo run --example printer_polymorphism # standalone trait+enum demo, fake data, no network/env needed
cargo run --example bambu_commands_demo # round-trips hand-written structs against real captured Bambu JSON
```
## Structure
```
src/
main.rs Runs the uplink and the go2rtc watchdog side by side
config.rs Loads settings from environment variables
fleet.rs Loads printers.toml into ready-to-use PrinterHandles
uplink/ WebSocket client to continuum-backend: connect, heartbeat, reconnect on drop
printer/ GenericPrinter trait + PrinterBase + PrinterHandle enum + one stub per vendor
go2rtc.rs Restarts the go2rtc camera-restreaming process if it dies
main.rs Loads+connects the printer fleet, then runs the uplink and go2rtc watchdog side by side
config.rs Loads settings from environment variables
fleet.rs Loads printers.toml into ready-to-use PrinterHandles
uplink/ WebSocket client to continuum-backend: connect, heartbeat, reconnect on drop
go2rtc.rs Restarts the go2rtc camera-restreaming process if it dies
printer/
mod.rs GenericPrinter trait + PrinterBase + PrinterHandle enum - the trait+enum
"inheritance" pattern this project uses instead of class inheritance
bambu.rs The real Bambu printer type: cert trust, protocol version auto-detect, commands
bambu_mqtt.rs The real MQTT wiring underneath bambu.rs - see its doc comment for the protocol details
bambu_commands/ Hand-written serde structs mirroring continuum-schemas' bambulab/ shapes
marlin_serial.rs A real held-open Marlin/RepRap serial connection
prusa.rs, klipper.rs Still stubs - see "What's still a stub" above
examples/
printer_polymorphism.rs Runs all five printer stubs through one `connect()` call site
test_bambu_certs.rs Loads printers.toml, does a real TLS handshake to each Bambu printer
auto_connect_bambu.rs Checks/auto-pins every Bambu printer's certificate (no MQTT)
test_bambu_certs.rs Narrower: just a raw TLS handshake per Bambu printer
printer_polymorphism.rs Standalone trait+enum demo, five fake printers, no network needed
bambu_commands_demo.rs Round-trips bambu_commands' structs against real captured Bambu JSON
```
`src/printer/` is where the "inheritance" question lives - see that
module's doc comment for the trait+enum pattern this project uses instead
of class inheritance, and run `printer_polymorphism` to see it work.
`printers.toml` (gitignored - copy from `printers.example.toml`) holds real
per-printer connection details: host, access code, and - since not every
Bambu printer trusts the same certificate (see `certs/README.md`) - an
optional per-printer CA override. `src/fleet.rs` loads it; nothing in
`main.rs` uses it yet, but `test_bambu_certs` does, as a real (if narrow -
just the TLS handshake, no MQTT) way to check a printer's certificate
without needing the full MQTT client built yet.
per-printer connection details: host, access code/API key/serial port
depending on vendor, and - for Bambu - an optional per-printer CA
certificate override (see `certs/README.md`; most printers don't need one,
since `connect()` auto-pins automatically).
## What's not here yet (on purpose)
## Learning Rust through this repo
- Real MQTT/FTPS/HTTP/WebSocket printer clients - `src/printer/*.rs` has a
`println!` where each of these will go.
- Local SQLite buffering for telemetry across connectivity gaps.
- LAN printer discovery (SSDP/mDNS).
- The mechanical plate-changer interface (serial/GPIO).
Add these back in one at a time as you get comfortable with the Rust
underneath them - each is its own small lesson (async I/O, a new crate's
API, error handling for a real protocol) rather than something to absorb
all at once.
`src/printer/mod.rs`'s doc comment is where the "how do I do inheritance
without classes" question lives - trait (shared behavior) + struct
(shared fields) + enum (the closed set of concrete types), instead of
`GenericPrinter -> BambuPrinter/PrusaLinkPrinter/...` class inheritance.
Run `printer_polymorphism` to see it work end to end without needing any
real printer on the network.
+13
View File
@@ -28,3 +28,16 @@ certificates at the same time.
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.
## `pinned/`
Not checked in (gitignored) - where `BambuPrinter::ensure_trusted` saves a
certificate it auto-pinned via trust-on-first-connect, one file per
printer, named `<serial number>.pem`. This only happens for a printer
whose certificate doesn't verify against `bambu_ca2.pem` above (P1P, at
least) and that doesn't already have an explicit `ca_cert_path` set in
`printers.toml` - see that method's doc comment in `src/printer/bambu.rs`
for the full trust-on-first-connect flow, and the SSH-host-key analogy it's
built on. Delete a printer's file here to force it to re-pin (a real
error, not silent, if the certificate it captures the second time still
doesn't verify) the next time it connects.
+8 -7
View File
@@ -1,12 +1,13 @@
//! Run with: cargo run --example auto_connect_bambu
//!
//! The "smooth, no manual steps" version of test_bambu_certs +
//! fetch_bambu_cert combined: for each Bambu printer in printers.toml,
//! makes sure it has a working certificate, auto-pinning one via
//! trust-on-first-connect if the bundled CA doesn't verify. Safe to run
//! repeatedly - a printer that's already pinned (certs/pinned/<serial
//! number>.pem) just gets re-verified against its pin, no network trust
//! decision is made again.
//! For each Bambu printer in printers.toml, makes sure it has a working
//! certificate, auto-pinning one via trust-on-first-connect if the
//! bundled CA doesn't verify - the exact same `ensure_trusted` step
//! `BambuPrinter::connect()` runs before opening MQTT, isolated here so
//! you can check/establish trust without needing a full MQTT handshake to
//! succeed too. Safe to run repeatedly - a printer that's already pinned
//! (certs/pinned/<serial number>.pem) just gets re-verified against its
//! pin, no network trust decision is made again.
use continuum_proxy::fleet;
use continuum_proxy::printer::PrinterHandle;
-36
View File
@@ -1,36 +0,0 @@
//! 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::Bambu(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(())
}
+5 -1
View File
@@ -56,9 +56,13 @@ async fn main() {
// Vendor-only extension: reachable only after matching the concrete
// variant back out of the enum - one arm now that Bambu is a single
// type regardless of which protocol version `connect()` settled on.
// `print_status()` is real Bambu-only behavior with no equivalent on
// `GenericPrinter` (no other vendor here has an in-memory report
// cache) - none of these connected above (fake IPs), so it's always
// `None`, but the point is the downcast, not a live reading.
for printer in &fleet {
match printer {
PrinterHandle::Bambu(bambu) => bambu.dispatch_gcode("part.gcode.3mf"),
PrinterHandle::Bambu(bambu) => println!(" {}: cached print_status = {:?}", bambu.sn, bambu.print_status().await),
_ => {}
}
}
+6 -5
View File
@@ -11,11 +11,12 @@ vendor = "bambu_v1"
host = "192.168.1.50"
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
# 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"
# No ca_cert_path needed even though P1P doesn't chain to the bundled CA -
# `connect()` auto-pins a certificate via trust-on-first-connect the first
# time it can't verify against the bundled one, and saves it under
# certs/pinned/<sn>.pem for every run after that (see BambuPrinter::
# ensure_trusted's doc comment). Only set ca_cert_path explicitly if you
# want to point at a certificate you've captured/vetted some other way.
[[printer]]
id = "h2c"
+5 -28
View File
@@ -1,6 +1,4 @@
use std::collections::HashMap;
use std::env;
use std::path::PathBuf;
#[derive(Debug, Clone)]
pub struct Config {
@@ -11,21 +9,12 @@ pub struct Config {
pub go2rtc_bin: String,
pub go2rtc_config: String,
/// Path to the fleet config file - see `fleet.rs` and
/// `printers.example.toml` for the shape.
/// `printers.example.toml` for the shape. Bambu certificate trust
/// (per-printer `ca_cert_path`/`require_valid_cert`, and automatic
/// trust-on-first-connect pinning otherwise - see
/// `BambuPrinter::ensure_trusted`) lives entirely in that file now,
/// not as a separate fleet-wide env-configured policy here.
pub printers_path: String,
/// Per-printer CA certificate overrides, keyed by printer ID. Most
/// Bambu printers chain to the bundled shared CA (`certs/bambu_ca2.pem`)
/// and don't need an entry here - but some models (P1P, at least) ship
/// their own certificate instead of one signed by that shared CA, with
/// no single file covering all of them. Look up a printer's own entry
/// (if any) when constructing it; there's no fleet-wide "the" override.
pub bambu_ca_cert_overrides: HashMap<String, PathBuf>,
/// When `false`, Bambu MQTTS connections skip certificate verification
/// entirely instead of checking against a CA. `true` (the default) is
/// the secure choice; only flip this fleet-wide policy if you have
/// printers you can't get to verify any other way.
pub bambu_require_valid_cert: bool,
}
impl Config {
@@ -38,22 +27,10 @@ impl Config {
go2rtc_bin: env_or("CONTINUUM_GO2RTC_BIN", "go2rtc"),
go2rtc_config: env_or("CONTINUUM_GO2RTC_CONFIG", "./go2rtc.yaml"),
printers_path: env_or("CONTINUUM_PRINTERS_PATH", "printers.toml"),
bambu_ca_cert_overrides: parse_cert_overrides(&env_or("CONTINUUM_BAMBU_CA_CERT_OVERRIDES", "")),
bambu_require_valid_cert: env_or("CONTINUUM_BAMBU_REQUIRE_VALID_CERT", "true") == "true",
})
}
}
/// Parses `"printer_id=path,printer_id2=path2"` into a lookup map. Empty
/// input (the common case - no overrides needed) yields an empty map.
fn parse_cert_overrides(raw: &str) -> HashMap<String, PathBuf> {
raw.split(',')
.filter_map(|entry| entry.split_once('='))
.map(|(id, path)| (id.trim().to_string(), PathBuf::from(path.trim())))
.collect()
}
fn require(key: &str) -> anyhow::Result<String> {
env::var(key).map_err(|_| anyhow::anyhow!("missing required env var {key}"))
}
+18 -14
View File
@@ -51,11 +51,13 @@ pub enum BambuTls {
impl BambuTls {
/// Resolves the trust mode for *one* printer. Call this per printer,
/// passing `config.bambu_ca_cert_overrides.get(printer_id)` - there's
/// no single override for the whole fleet, because not every Bambu
/// model chains to the same CA. `require_valid_cert = false` always
/// means `Insecure`, regardless of whether that printer also has an
/// override entry.
/// passing that printer's own `ca_cert_path`/`require_valid_cert` from
/// `printers.toml` (see `fleet.rs::build_bambu`) - there's no single
/// override for the whole fleet, because not every Bambu model chains
/// to the same CA, and most printers don't need this at all (see
/// `BambuPrinter::ensure_trusted` for the automatic path).
/// `require_valid_cert = false` always means `Insecure`, regardless of
/// whether that printer also has a `cert_path`.
pub fn resolve(cert_path: Option<&Path>, require_valid_cert: bool) -> anyhow::Result<Self> {
if !require_valid_cert {
return Ok(BambuTls::Insecure);
@@ -287,8 +289,8 @@ impl BambuPrinter {
if presented != pem_to_der(pinned_pem)? {
anyhow::bail!(
"{} presented a certificate different from the one pinned - could be a legitimate \
certificate rotation, could be something worse; re-run fetch_bambu_cert deliberately \
if you're sure it's the former",
certificate rotation, could be something worse; delete its file under certs/pinned/ \
and reconnect to re-pin deliberately if you're sure it's the former",
self.base.name
);
}
@@ -306,11 +308,14 @@ impl BambuPrinter {
/// 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. */
/// trust - anyone on the network at the exact moment this runs gets
/// trusted forever after. `ensure_trusted` below is the only caller -
/// it only reaches this path when the bundled CA didn't verify, saves
/// the result under `certs/pinned/`, and every connection after this
/// one re-verifies against that pinned file instead of trusting the
/// network again. Not exposed as a manual step for the caller to run
/// ahead of time - there's no longer a reason to, now that `connect()`
/// does this automatically the first time a printer needs 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))?;
@@ -327,8 +332,7 @@ impl BambuPrinter {
/* Makes sure this printer has a *working* trust configuration,
/// auto-pinning a certificate via trust-on-first-connect if needed -
/// the "smooth, no manual steps" version of the fetch_bambu_cert
/// workflow, built the way SSH handles host keys:
/// no manual step required, built the way SSH handles host keys:
///
/// - A pinned certificate already on disk (`cert_dir/<sn>.pem`) is used
/// directly. No new trust decision gets made on every run - that
+4 -4
View File
@@ -21,10 +21,10 @@
//!
//! Run `cargo run --example printer_polymorphism` to see it in action.
//!
//! Deliberately left out of this module for now, to keep it focused on one
//! idea at a time: real MQTT/HTTP/FTPS calls (that's `src/adapters/`) and
//! `Send`-bound futures (matters once this is wired into a multi-threaded
//! `tokio::spawn`, not yet).
//! Real MQTT is built now - `bambu.rs`/`bambu_mqtt.rs` for Bambu's
//! protocol, `marlin_serial.rs` for a real Marlin serial connection - see
//! each module's own doc comment. PrusaLink/Klipper's HTTP clients are
//! still stubs; that's the next piece to build past this module.
mod bambu;
mod bambu_commands;