Files
continuum-proxy/README.md
T
octoturge 6a0d80a381 Generate go2rtc's config from what the printers report
go2rtc has been supervised since before this commit, with nothing to
serve: `CONTINUUM_GO2RTC_CONFIG` pointed at `./go2rtc.yaml`, and that
file did not exist in the repository or anywhere else. A watchdog was
restarting a process that had no cameras configured.

`camera.rs` writes it, from the `rtsp_url` a printer publishes in its own
`print.push_status` `ipcam` block rather than from `printers.toml`. The
URL is already something the machine tells us, so asking an operator to
keep a second copy of it - and to update it by hand when a printer's
address changes - would be asking for drift.

Two tasks, not one: the builder decides what should be served, the
watchdog keeps the thing serving it alive. They meet at one `Notify`,
because go2rtc reads its configuration only at startup, so a changed
config needs a restart. The watchdog now selects the child's exit against
that notification and skips its backoff on a deliberate change.

What this does *not* cover is most printers, and that is the honest
limit. Only the X1/H2 class publishes an RTSP URL - confirmed against two
real machines: the H2C reports
`rtsps://192.168.0.21:322/streaming/live/1`, the P1P reports an `ipcam`
block with `ipcam_dev: "1"` and no URL at all. "A camera exists" and "a
camera you can connect to" are different facts. A P1/A1 needs Bambu's
proprietary port-6000 JPEG stream, which this crate does not speak, so
those printers report no `camera_stream` and a UI should offer a player
only when one is present.

Details worth keeping:

- The config is rendered sorted so an unchanged fleet is byte-identical,
  and on the first pass it is compared against the file on disk - without
  that, every daemon restart rewrites an identical file and drops every
  live viewer.
- Written through a `.tmp` and renamed. The watchdog can restart go2rtc at
  any moment, and a half-written config fails to parse, which takes every
  working stream down rather than one.
- Never logged, and gitignored: the credentials go in the URL because
  that is the only place go2rtc takes them for an RTSP source, so the file
  holds printer access codes.
- A reported URL with no scheme is refused rather than repaired. One bad
  entry makes go2rtc fail to load the whole file.
- No `rtsp:` or `webrtc:` block. The API listener is what serves WebRTC to
  a browser, which is the only consumer here; opening go2rtc's own RTSP
  server would publish every printer's camera on the LAN unauthenticated.
- `stream_base_url` is reported in `hello`, not derived from the listen
  address: `:1984` is a bind address and not something a browser can open.
  Unset means "advertise nothing", which is the right default, because a
  wrong URL makes the UI offer a player that cannot connect. It is a LAN
  address either way - there is no relay, so this works for an operator in
  the workshop and not for anyone outside it.

63 tests, clippy unchanged at the two deliberate variant-size warnings,
`cargo deny check licenses` ok. go2rtc itself stays operator-installed and
is not distributed.
2026-10-15 23:03:02 +00:00

142 lines
7.8 KiB
Markdown

# 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://<ip>: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 -- <printer-id>` - **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.