Route telemetry up and commands down over the cloud uplink

The proxy did not actually proxy. `fleet` was loaded and connected in
main.rs and then never touched again, while the uplink said hello and sent
heartbeats into the void - two halves of the program running side by side
with nothing between them. Both directions are wired now.

Up: every 5s, one vendor-neutral `PrinterTelemetry` snapshot per printer
that has reported, batched into a single `Telemetry` message. The snapshot
is a translation rather than a forwarded `print.push_status`, because it
has to describe a Prusa too, and because Bambu's wire format has traps
(0-15 fan gears, ints-or-floats temperatures) that should not reach the
rest of the system.

Down: a `Command` is looked up by our own printer id, executed, and
answered with a `CommandResult`. `PrinterCommand` is deliberately small and
deliberately has no "start a print" variant - `start_uploaded_file` has
never been run against hardware, and the first thing to exercise it should
be a human watching the machine, not a message from the internet.

The fleet becomes `Arc<Mutex<Vec<PrinterHandle>>>` so main and the uplink
can share it. One lock for the whole Vec is coarse - a slow command to one
printer stalls telemetry for all of them - which is fine at a handful of
printers and is the shape to revisit (a lock per printer, or an actor task
each) before a farm gets large.

Two things found by running this against the live P1P and H2C:

- A v1 Bambu reached the cloud as a row of nulls with one real bed
  temperature in it, while the v2 H2C beside it was fully populated. v1
  only sends a *complete* status when asked, and `pushall`'s own doc
  comment said to call it periodically - nothing ever did. Each telemetry
  tick now collects, then requests the next refresh, so v1 telemetry runs
  one tick behind instead of staying stuck on fragments. The quirk lives in
  `PrinterHandle::refresh_status` so the uplink does not have to know it.
- The third instance of the silent-swallow parse bug, at the old
  uplink/mod.rs:63: `if let Ok(server_msg) = serde_json::from_str(..)` with
  no else. Same shape that hid broken `print.push_status` parsing for
  weeks, except here it would mean commands vanishing with the cloud never
  being told. Now a `match` with a loud `Err` arm.

Verified end to end against both printers with a throwaway WebSocket
server: H2C `RUNNING 88% layer 48/50 13min nozzle 230/230 bed 35/35`, P1P
`IDLE 0% nozzle 23.5/0 bed 22.5/0`, both fan commands `ok=true`, zero
warnings over 24s.
This commit is contained in:
2026-10-03 10:22:22 +00:00
parent 9a314b2b9f
commit 91fbcaf665
5 changed files with 316 additions and 35 deletions
+24 -9
View File
@@ -48,22 +48,37 @@ cloud control plane (`continuum-backend`).
(FTPS upload, fan speed). Requires an explicit printer id and refuses to
run unless that printer reports `gcode_state: "IDLE"`.
## What's still a stub (cont.)
## 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.
## What's still a stub
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: 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.
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