main.rs never actually loaded printers.toml or connected anything -
fleet/printer were only ever exercised by standalone examples. Now
main() loads the fleet (path configurable via CONTINUUM_PRINTERS_PATH,
default "printers.toml") and connects every printer concurrently at
startup, logging success/failure per printer rather than failing
startup over one bad printer.
That surfaced a real bug the instant it ran against a live printer:
complete_pending() was sending the whole {"info": {...}} wrapper through
the pending oneshot, not the unwrapped body - BambuPrinter::get_version
deserializing that into GetVersionResponse failed with "missing field
module" even though module was right there, just one level deeper than
expected. Fixed by unwrapping the single category key before completing
the pending request, symmetric with how a request gets wrapped on the
way out.
Verified live against a real H2C (MQTT v5/BambuMQTT v2): connect,
get_version, and the full pending-request correlation all worked
end-to-end, returning real firmware versions for every module (main
board 01.01.05.00, both AMS units, AMS HT, etc). The other configured
printer (P1P, forced v1) failed with a real, expected TLS error - it
doesn't chain to the bundled CA, matching the already-documented P1P
quirk; needs the ca_cert_path/fetch_bambu_cert workflow, not a code fix.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MWQVtBGxHYLkVw2kBupd5g
continuum-proxy
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
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.
Getting started
cp .env.example .env
cargo run # the daemon: 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)
cargo run --example test_bambu_certs # real TLS handshake test against each Bambu printer
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
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
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.
What's not here yet (on purpose)
- Real MQTT/FTPS/HTTP/WebSocket printer clients -
src/printer/*.rshas aprintln!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.