# 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`). ## What's real right now - **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/`). - **Camera restreaming** - generates go2rtc's configuration from the camera URLs the printers report, and restarts go2rtc when it changes (`src/camera.rs` + `src/go2rtc.rs`). Only the X1/H2 class publishes an RTSP URL: confirmed against two real machines, an H2C reports `rtsps://:322/streaming/live/1` and a P1P reports a camera with no URL for it. A P1/A1 camera needs Bambu's proprietary port-6000 JPEG stream, which this crate does not speak, so those printers advertise no stream rather than one that will not play. Note the stream is reachable on the gateway's LAN only - there is no relay. - **Bambu file upload** - implicit FTPS (port 990) into the printer's own `cache/` directory (`src/printer/bambu_ftps.rs`, via `BambuPrinter::upload_file`). **Confirmed live against a P1P on 2026-10-03**: login, binary mode, and the transfer all work, byte count matched exactly. - **Print status** - `print.push_status` parsed into `PrintReport` with real job progress, temperatures, fans and HMS entries. Confirmed live on both generations (idle P1P on v1, H2C 20% into a job on v2); regression fixtures from those captures are in `tests/fixtures/`. ## Examples worth knowing - `cargo run --example live_reports [printer-id]` - **read-only**, safe against a printer that is mid-print. Connects, reports firmware, and watches the status stream. Redirect stderr to capture raw payloads. - `cargo run --example bambu_idle_checks -- ` - **writes** (FTPS upload, fan speed). Requires an explicit printer id and refuses to run unless that printer reports `gcode_state: "IDLE"`. ## What routes between the cloud and the printers The uplink and the printer fleet are wired together, in both directions (`uplink/mod.rs`): - **Up:** every 5s, one `PrinterTelemetry` snapshot per printer that has reported, batched into a single `Telemetry` message. Verified live against a P1P and an H2C. - **Down:** a `Command` from the cloud is looked up by our own printer id, executed, and answered with a `CommandResult`. The set of commands is deliberately small - a fan speed and a single gcode line - and deliberately has no "start a print" variant; see `PrinterCommand`. One quirk worth knowing: a Bambu on protocol v1 only sends a *complete* status when asked, so each telemetry tick also fires a `pushing.pushall` for the next one. A v1 printer's telemetry is therefore one tick behind, which is why it shows up on the second frame rather than the first. ## What's still a stub `start_uploaded_file` - the MQTT command that actually *starts* a print from an uploaded file - is written but has deliberately never been fired, since that commits a machine to hours of work. Its doc comment lists the three details to confirm on the first real attempt. `upload_file` is proven, but only with a small text file, not a multi-megabyte sliced `.3mf`. PrusaLink (REST) and Klipper/Moonraker (WebSocket) - `printer/prusa.rs` and `printer/klipper.rs` just `println!` what they'd send. Also not built yet: local buffering for telemetry across connectivity gaps (so telemetry produced while the uplink is down is simply lost), LAN printer discovery (SSDP/mDNS), and the mechanical plate-changer interface. ## Getting started ```bash cp .env.example .env 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 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 Loads+connects the printer fleet, then runs the uplink, camera config, watchdog and completion watcher 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 camera.rs Writes go2rtc's config from the camera URLs the printers report go2rtc.rs Restarts the go2rtc camera-restreaming process if it dies, or when the config changes 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/ 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 ``` `printers.toml` (gitignored - copy from `printers.example.toml`) holds real 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). ## Learning Rust through this repo `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.