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.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-07 16:17:15 +00:00
co-authored by Claude Sonnet 5
parent 11177db2ad
commit 94484bb190
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_BIN=/usr/local/bin/go2rtc
CONTINUUM_GO2RTC_CONFIG=./go2rtc.yaml CONTINUUM_GO2RTC_CONFIG=./go2rtc.yaml
# Bambu MQTTS certificate trust. Most printers verify fine against the # Fleet config - see printers.example.toml. Bambu MQTTS certificate trust
# bundled shared CA (certs/bambu_ca2.pem) and need nothing here. For a # (per-printer ca_cert_path/require_valid_cert, and automatic
# printer that doesn't chain to it (P1P, at least), add "printer_id=path" # trust-on-first-connect pinning otherwise) is configured per printer in
# entries, comma-separated: CONTINUUM_BAMBU_CA_CERT_OVERRIDES=p1=./certs/p1p.pem # that file, not here.
CONTINUUM_BAMBU_CA_CERT_OVERRIDES= CONTINUUM_PRINTERS_PATH=printers.toml
CONTINUUM_BAMBU_REQUIRE_VALID_CERT=true
RUST_LOG=info,continuum_proxy=debug RUST_LOG=info,continuum_proxy=debug
-4
View File
@@ -21,10 +21,6 @@ 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"
[[example]] [[example]]
name = "auto_connect_bambu" name = "auto_connect_bambu"
path = "examples/auto_connect_bambu.rs" path = "examples/auto_connect_bambu.rs"
+63 -35
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 SBC on-site, talks to printers over the LAN, and keeps a connection to the
cloud control plane (`continuum-backend`). 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** - a real client for both protocol generations (MQTT
(Bambu MQTT+FTPS, PrusaLink REST, Klipper/Moonraker WebSocket) are **not** v3.1.1+BambuMQTT v1 on older firmware, MQTT v5+BambuMQTT v2 on current
implemented yet. Each vendor is a stub that just prints what it would do firmware), auto-detected per printer on connect (a firmware update can
(`src/printer/bambu.rs`, `prusa.rs`, `klipper.rs`). The idea is to learn move the same physical printer between them, so this isn't something
Rust's polymorphism pattern (trait + enum, since Rust has no class config can hard-code once and forget - see `printer/bambu.rs`'s
inheritance) on something simple before adding real networking on top. `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 ## Getting started
```bash ```bash
cp .env.example .env cp .env.example .env
cargo run # the daemon: cloud uplink + go2rtc watchdog cargo run # the daemon: connects every configured printer, then runs the cloud uplink + go2rtc watchdog
cargo run --example printer_polymorphism # standalone demo, no network/env needed
cp printers.example.toml printers.toml # fill in your real printers (gitignored) 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 ## Structure
``` ```
src/ src/
main.rs Runs the uplink and the go2rtc watchdog side by side main.rs Loads+connects the printer fleet, then runs the uplink and go2rtc watchdog side by side
config.rs Loads settings from environment variables config.rs Loads settings from environment variables
fleet.rs Loads printers.toml into ready-to-use PrinterHandles fleet.rs Loads printers.toml into ready-to-use PrinterHandles
uplink/ WebSocket client to continuum-backend: connect, heartbeat, reconnect on drop 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 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/ examples/
printer_polymorphism.rs Runs all five printer stubs through one `connect()` call site auto_connect_bambu.rs Checks/auto-pins every Bambu printer's certificate (no MQTT)
test_bambu_certs.rs Loads printers.toml, does a real TLS handshake to each Bambu printer 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 `printers.toml` (gitignored - copy from `printers.example.toml`) holds real
per-printer connection details: host, access code, and - since not every per-printer connection details: host, access code/API key/serial port
Bambu printer trusts the same certificate (see `certs/README.md`) - an depending on vendor, and - for Bambu - an optional per-printer CA
optional per-printer CA override. `src/fleet.rs` loads it; nothing in certificate override (see `certs/README.md`; most printers don't need one,
`main.rs` uses it yet, but `test_bambu_certs` does, as a real (if narrow - since `connect()` auto-pins automatically).
just the TLS handshake, no MQTT) way to check a printer's certificate
without needing the full MQTT client built yet.
## What's not here yet (on purpose) ## Learning Rust through this repo
- Real MQTT/FTPS/HTTP/WebSocket printer clients - `src/printer/*.rs` has a `src/printer/mod.rs`'s doc comment is where the "how do I do inheritance
`println!` where each of these will go. without classes" question lives - trait (shared behavior) + struct
- Local SQLite buffering for telemetry across connectivity gaps. (shared fields) + enum (the closed set of concrete types), instead of
- LAN printer discovery (SSDP/mDNS). `GenericPrinter -> BambuPrinter/PrusaLinkPrinter/...` class inheritance.
- The mechanical plate-changer interface (serial/GPIO). Run `printer_polymorphism` to see it work end to end without needing any
real printer on the network.
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.
+13
View File
@@ -28,3 +28,16 @@ certificates at the same time.
If a printer's connection ever fails certificate verification against this If a printer's connection ever fails certificate verification against this
file, re-fetch from the source URL above (BambuStudio ships whatever the file, re-fetch from the source URL above (BambuStudio ships whatever the
current fleet needs) and update the fingerprint here. 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 //! Run with: cargo run --example auto_connect_bambu
//! //!
//! The "smooth, no manual steps" version of test_bambu_certs + //! For each Bambu printer in printers.toml, makes sure it has a working
//! fetch_bambu_cert combined: for each Bambu printer in printers.toml, //! certificate, auto-pinning one via trust-on-first-connect if the
//! makes sure it has a working certificate, auto-pinning one via //! bundled CA doesn't verify - the exact same `ensure_trusted` step
//! trust-on-first-connect if the bundled CA doesn't verify. Safe to run //! `BambuPrinter::connect()` runs before opening MQTT, isolated here so
//! repeatedly - a printer that's already pinned (certs/pinned/<serial //! you can check/establish trust without needing a full MQTT handshake to
//! number>.pem) just gets re-verified against its pin, no network trust //! succeed too. Safe to run repeatedly - a printer that's already pinned
//! decision is made again. //! (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::fleet;
use continuum_proxy::printer::PrinterHandle; 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 // Vendor-only extension: reachable only after matching the concrete
// variant back out of the enum - one arm now that Bambu is a single // variant back out of the enum - one arm now that Bambu is a single
// type regardless of which protocol version `connect()` settled on. // 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 { for printer in &fleet {
match printer { 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" 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) 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 # No ca_cert_path needed even though P1P doesn't chain to the bundled CA -
# downloaded from the printer itself. Leave unset to use the bundled CA. # `connect()` auto-pins a certificate via trust-on-first-connect the first
# `cargo run --example fetch_bambu_cert -- p1p ./certs/p1p.pem` can fetch it # time it can't verify against the bundled one, and saves it under
# for you (trust-on-first-connect - see that example's doc comment). # certs/pinned/<sn>.pem for every run after that (see BambuPrinter::
ca_cert_path = "./certs/p1p.pem" # 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]] [[printer]]
id = "h2c" id = "h2c"
+5 -28
View File
@@ -1,6 +1,4 @@
use std::collections::HashMap;
use std::env; use std::env;
use std::path::PathBuf;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Config { pub struct Config {
@@ -11,21 +9,12 @@ pub struct Config {
pub go2rtc_bin: String, pub go2rtc_bin: String,
pub go2rtc_config: String, pub go2rtc_config: String,
/// Path to the fleet config file - see `fleet.rs` and /// 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, 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 { impl Config {
@@ -38,22 +27,10 @@ impl Config {
go2rtc_bin: env_or("CONTINUUM_GO2RTC_BIN", "go2rtc"), go2rtc_bin: env_or("CONTINUUM_GO2RTC_BIN", "go2rtc"),
go2rtc_config: env_or("CONTINUUM_GO2RTC_CONFIG", "./go2rtc.yaml"), go2rtc_config: env_or("CONTINUUM_GO2RTC_CONFIG", "./go2rtc.yaml"),
printers_path: env_or("CONTINUUM_PRINTERS_PATH", "printers.toml"), 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> { fn require(key: &str) -> anyhow::Result<String> {
env::var(key).map_err(|_| anyhow::anyhow!("missing required env var {key}")) 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 { 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 that printer's own `ca_cert_path`/`require_valid_cert` from
/// no single override for the whole fleet, because not every Bambu /// `printers.toml` (see `fleet.rs::build_bambu`) - there's no single
/// model chains to the same CA. `require_valid_cert = false` always /// override for the whole fleet, because not every Bambu model chains
/// means `Insecure`, regardless of whether that printer also has an /// to the same CA, and most printers don't need this at all (see
/// override entry. /// `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> { 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);
@@ -287,8 +289,8 @@ impl BambuPrinter {
if presented != pem_to_der(pinned_pem)? { if presented != pem_to_der(pinned_pem)? {
anyhow::bail!( anyhow::bail!(
"{} presented a certificate different from the one pinned - could be a legitimate \ "{} presented a certificate different from the one pinned - could be a legitimate \
certificate rotation, could be something worse; re-run fetch_bambu_cert deliberately \ certificate rotation, could be something worse; delete its file under certs/pinned/ \
if you're sure it's the former", and reconnect to re-pin deliberately if you're sure it's the former",
self.base.name self.base.name
); );
} }
@@ -306,11 +308,14 @@ impl BambuPrinter {
/// presents, and returns it as PEM bytes. /// presents, and returns it as PEM bytes.
/// ///
/// This is meaningfully weaker than verifying against a CA you already /// This is meaningfully weaker than verifying against a CA you already
/// trust - anyone on the network at the exact moment you run this gets /// trust - anyone on the network at the exact moment this runs gets
/// trusted forever after. That's why it's a method you call once, by /// trusted forever after. `ensure_trusted` below is the only caller -
/// hand (see `examples/fetch_bambu_cert.rs`), rather than something /// it only reaches this path when the bundled CA didn't verify, saves
/// `connect()` falls back to silently. Save the result to a file and /// the result under `certs/pinned/`, and every connection after this
/// point that printer's `ca_cert_path` in `printers.toml` at it. */ /// 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>> { pub fn fetch_certificate(&self) -> anyhow::Result<Vec<u8>> {
let addr = resolve_addr(&self.base.host)?; let addr = resolve_addr(&self.base.host)?;
let stream = TcpStream::connect_timeout(&addr, Duration::from_secs(5))?; 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, /* Makes sure this printer has a *working* trust configuration,
/// auto-pinning a certificate via trust-on-first-connect if needed - /// auto-pinning a certificate via trust-on-first-connect if needed -
/// the "smooth, no manual steps" version of the fetch_bambu_cert /// no manual step required, built the way SSH handles host keys:
/// workflow, built the way SSH handles host keys:
/// ///
/// - A pinned certificate already on disk (`cert_dir/<sn>.pem`) is used /// - A pinned certificate already on disk (`cert_dir/<sn>.pem`) is used
/// directly. No new trust decision gets made on every run - that /// 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. //! 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 //! Real MQTT is built now - `bambu.rs`/`bambu_mqtt.rs` for Bambu's
//! idea at a time: real MQTT/HTTP/FTPS calls (that's `src/adapters/`) and //! protocol, `marlin_serial.rs` for a real Marlin serial connection - see
//! `Send`-bound futures (matters once this is wired into a multi-threaded //! each module's own doc comment. PrusaLink/Klipper's HTTP clients are
//! `tokio::spawn`, not yet). //! still stubs; that's the next piece to build past this module.
mod bambu; mod bambu;
mod bambu_commands; mod bambu_commands;