octoturge 0ab602ed79 Add real FTPS upload to Bambu printers
`upload_project_file` was a `println!`. MQTT can start a print but can
only name a file the printer already has, so moving the bytes there is a
separate protocol: implicit FTPS on port 990, user `bblp`, password the
same access code MQTT uses, into the printer's own `cache/` directory.

Confirmed live against a P1P: 102 bytes uploaded, byte count matched
exactly, first attempt.

Deliberately suppaftp's *blocking* client on `tokio::task::spawn_blocking`
rather than its async one. The blocking client takes a plain
`native_tls::TlsConnector`, so this reuses `BambuTls::build_connector()`
unchanged and every TLS trust decision for a printer stays in that one
method; the async client needs a second TLS stack (`async-native-tls`)
whose futures-vs-tokio `AsyncRead` mismatch has already cost this project
a debugging session. `connect()`'s doc comment already pointed at
`spawn_blocking` as the eventual answer for blocking work, and a
multi-megabyte upload earns it more obviously than a handshake does.

`connect_secure_implicit` sits behind suppaftp's `deprecated` feature
because implicit FTPS is what the RFCs deprecated in favour of explicit
AUTH TLS. The printers only speak implicit, so the feature is on.

Split into `upload_file` (FTPS only) and `start_uploaded_file` (the MQTT
command that begins printing), because the two halves have very different
consequences: moving a file onto a printer is inert and reversible, while
starting a print commits the machine to hours of physical work. That split
is what made the transfer testable against real hardware at all.

`start_uploaded_file` dispatches on file type - a `.3mf` is a container so
`print.project_file` needs both a `url` and a `param` naming the gcode
inside it, while a bare `.gcode` has nothing to select and
`print.gcode_file` takes just a path. It has deliberately never been
fired; its doc comment lists the three guesses to confirm first.

suppaftp is MIT OR Apache-2.0, checked against docs/LICENSES.md before
being added, along with all 22 crates it pulled in.
2026-10-03 10:11:36 +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).

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

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%