octoturge df6dfca3ca Refuse a truncated project, and drive the whole path from the probe
`upload_project_file` already existed - as a `GenericPrinter` trait method,
which is why a grep for `pub async fn` missed it and this commit nearly
reimplemented it. It logged `written` and `byte_count` next to each other and
never compared them, so a short write was something you could only notice by
reading the logs afterwards, and the next line printed the truncated file
anyway. A `.3mf` is a zip: cut the end off and the central directory goes with
it, so the printer gets an archive it cannot open. That is also the failure a
slow transfer invites, and this transfer is slow - 4.96 MB took 31.3s, so a
62 MB project is minutes of exposure.

`print-project <local> [name]` runs that method against a real printer, so
the harness tests the production entry point rather than the three pieces in
sequence. It deliberately takes no `--ams`: the whole point is that the
mapping comes out of the archive. Gated like `start`, on a terminal state and
`--yes-start-a-real-print`.

Confirmed end to end on the P1P (2026-10-15, with the owner's go-ahead), with
the same project the 2026-10-11 run used: the derivation read one PETG/GFG99/
`#2850E0` filament out of `Metadata/slice_info.config` and produced
`mapping=[3]`, padded to five trays; 297,227 bytes went up with an exact byte
count; `IDLE -> PREPARE -> RUNNING`. The AMS on that machine has slot 2 in
black and slot 3 in blue, same product and same `tray_info_idx`, so picking
slot 3 is the colour tie-break doing real work rather than a one-candidate
match.
2026-10-15 22:32:31 +00:00

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/).
  • go2rtc watchdog - restarts the camera-restreaming subprocess if it dies (src/go2rtc.rs).
  • 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

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 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/
  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.

S
Description
Continuum platform: continuum-proxy
Readme
914 KiB
Languages
Rust 100%