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:
@@ -129,6 +129,52 @@ pub enum PrinterHandle {
|
||||
Klipper(KlipperPrinter),
|
||||
}
|
||||
|
||||
impl PrinterHandle {
|
||||
/// The latest status this process has for the printer, if it has one.
|
||||
///
|
||||
/// An inherent method rather than a `GenericPrinter` trait method,
|
||||
/// because right now only Bambu has a real status path - the other three
|
||||
/// return `None` not because they have nothing to say but because their
|
||||
/// protocol clients are still stubs. Putting it on the trait would make
|
||||
/// every vendor claim to implement something three of them don't. Move it
|
||||
/// onto the trait once a second vendor can genuinely answer.
|
||||
pub async fn print_status(&self) -> Option<PrintReport> {
|
||||
match self {
|
||||
PrinterHandle::Bambu(p) => p.print_status().await,
|
||||
PrinterHandle::PrusaLink(_) | PrinterHandle::PrusaSerial(_) | PrinterHandle::Klipper(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Asks the printer to report its full state, for the printers that only
|
||||
/// report when asked.
|
||||
///
|
||||
/// This exists because of one specific vendor quirk that should not leak
|
||||
/// any further than this: a Bambu on protocol v1 pushes only *partial*
|
||||
/// `print.push_status` objects unprompted - often a single temperature -
|
||||
/// and sends a complete one only in response to `pushing.pushall`. Without
|
||||
/// a periodic call here, `print_status()` on a v1 printer stays stuck on
|
||||
/// whatever fragments it happened to volunteer. Found exactly that way: a
|
||||
/// live P1P reached the cloud as a row of nulls with one real bed
|
||||
/// temperature in it, while a v2 H2C beside it was fully populated.
|
||||
///
|
||||
/// Nothing arrives by the time this returns - the refreshed status comes
|
||||
/// back over MQTT as an ordinary push some time later, so a caller that
|
||||
/// refreshes and immediately reads gets the *previous* refresh's data. On
|
||||
/// a timer that is simply a one-tick lag, which is why this is
|
||||
/// fire-and-forget rather than something to await a result from.
|
||||
///
|
||||
/// A no-op for everyone else: `BambuPrinter::pushall` already no-ops on
|
||||
/// v2 (which pushes complete objects unprompted), and the other three
|
||||
/// vendors are still stubs. Same reasoning as `print_status` for why this
|
||||
/// is inherent rather than a `GenericPrinter` method.
|
||||
pub async fn refresh_status(&self) -> Result<(), PrinterError> {
|
||||
match self {
|
||||
PrinterHandle::Bambu(p) => p.pushall().await,
|
||||
PrinterHandle::PrusaLink(_) | PrinterHandle::PrusaSerial(_) | PrinterHandle::Klipper(_) => Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Hand-written delegation: each trait method just matches on the variant
|
||||
// and forwards to that variant's own implementation. This one block is the
|
||||
// only "boilerplate tax" for not having inheritance - everything else reads
|
||||
|
||||
Reference in New Issue
Block a user