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.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two changes, both following from the print that paused at 1% because
`use_ams: false` pointed an AMS-sliced project at the external spool.
## upload_project_file no longer guesses
It now reads the `.3mf` it already holds and works out where each filament
should come from. `Metadata/slice_info.config` names every filament's
`tray_info_idx`, type and colour; the AMS report names the same three per
loaded tray; matching them is the whole job. `src/printer/project_file.rs`
does the reading and matching and is free of printer types, so it is
testable without a printer - which is the only way this gets tested at all,
the alternative being to notice a mapping bug by watching a machine print in
the wrong colour.
The mapping is derived *before* the upload: a project that cannot be matched
should not result in a file being moved onto the machine that cannot then be
printed.
It never falls back to the external spool to make an unmatched project
"work". The one case it answers `ExternalSpool` for is a printer reporting no
AMS trays at all. A printer *with* an AMS whose project wants the spool is an
error, not a fallback, because `vt_tray` is not modelled on `PrintReport` -
there is no way to check the spool holds the right material, and claiming a
match without checking is how this went wrong the first time.
Two fixtures carry the real cases: `cube_slice_info.3mf` is the project that
cost the print (deflated on purpose - every other test archive is Stored, so
the decompression path and the flate2 backend behind zip's `deflate-flate2`
would otherwise never run), and the existing `push_status_v1_p1p_idle.json`
supplies the AMS side. The end-to-end test asserts they agree on
`[3,-1,-1,-1,-1]`: both slots hold PETG GFG99 and only the colour tells them
apart, so a matcher stopping at `tray_info_idx` would print the cube in
black.
A malformed `<filament>` element is an error rather than a skip. A skipped
filament leaves a hole at its index, the hole becomes `-1`, and the printer
is told that material is unused - so the plate prints in whatever remains
mapped.
New dependency: `zip` 9.0.3 (MIT) with `deflate-flate2`, plus `flate2` for a
backend, which zip's feature does not select on its own. All six new crates
are MIT, MIT/Apache-2.0, 0BSD/MIT/Apache-2.0, MIT/Zlib/Apache-2.0 or Zlib,
and `cargo deny check licenses` passes. No bzip2, lzma, xz or zstd entered
the graph.
## Lights on completion
`completion::run` is a third daemon task that notices a print finishing and
blinks that printer's chamber light for ten seconds, then leaves it off:
500ms on, 500ms off, twenty commands, ending dark.
Its own task rather than a branch in the uplink's telemetry loop, because
that loop only ticks while the cloud connection is up - and a light that only
works when the internet does is not much of a notification for someone
standing next to the printer.
Three rules, each a false celebration avoided and each tested: only a
*transition* into `FINISH` fires, because `FINISH` persists until the next
print starts; a printer seen for the first time never fires, so restarting
the gateway does not announce prints that finished yesterday; and `FAILED`
never fires, because that is what a Bambu reports for a cancellation,
including every `print.stop` this software sends.
The blink is driven from `On`/`Off` rather than the firmware's `flashing`
mode. `flashing` is in the schema but the meaning of `loop_times` versus
`interval_time` is unconfirmed, so "blink for ten seconds" could not be
predicted; two obvious commands give an exact duration. The light is left off
even when a blink fails part-way, since a machine abandoned with its light on
looks like one still working.
`set_light` is **unverified against hardware** - the P1P was powered off when
this was written, and the doc comment says so. The shape is built from
`schemas/bambulab/{v1,v2}/request/system/ledctrl.schema.json` rather than by
hand, and it goes out as a correlated request so a rejection is visible.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`cargo check --all-targets` emitted 32 warnings; it now emits none. Almost all
of them came from one cause: main.rs declared `mod config/fleet/printer/uplink`
while lib.rs declared the same modules, so every file was compiled twice, once
per crate. In the binary copy nothing is externally reachable, so every
`pub use` re-export and every method that only an example or a test calls was
genuinely dead - and the warnings were real about the binary while being wrong
about the library's actual public API. main.rs now uses the library crate and
keeps only `mod go2rtc`, which nothing else needs.
The rest, individually:
- `ModuleInfo` was re-exported from bambu_commands but not from printer, so it
was unreachable even though `GetVersionResponse.module` is a public field of
type `Vec<ModuleInfo>` - a caller could not name the type of a field it can
read. Re-exported rather than dropped.
- Three stub `publish` impls take a payload they do not use yet; renamed to
`_payload`.
- One unused import in examples/test_bambu_certs.rs.
That leaves the `async_fn_in_trait` lint on GenericPrinter, which is now
`allow`ed with the reasoning written down rather than left to fire on every
build. The old comment said it "only matters once this is wired into the
multi-threaded daemon", which has since happened - so the question got
answered instead of deferred: the futures cannot be required to be `Send`, and
that is fine because every printer is reached through one Arc<Mutex<Vec<..>>>
from the uplink's own task, so they are created and polled in the same place.
The note says what to do if a printer ever gets its own task.
No behaviour change. Tests still pass and the daemon was re-verified against
the live P1P and H2C through the full backend path.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
main.rs never actually loaded printers.toml or connected anything -
fleet/printer were only ever exercised by standalone examples. Now
main() loads the fleet (path configurable via CONTINUUM_PRINTERS_PATH,
default "printers.toml") and connects every printer concurrently at
startup, logging success/failure per printer rather than failing
startup over one bad printer.
That surfaced a real bug the instant it ran against a live printer:
complete_pending() was sending the whole {"info": {...}} wrapper through
the pending oneshot, not the unwrapped body - BambuPrinter::get_version
deserializing that into GetVersionResponse failed with "missing field
module" even though module was right there, just one level deeper than
expected. Fixed by unwrapping the single category key before completing
the pending request, symmetric with how a request gets wrapped on the
way out.
Verified live against a real H2C (MQTT v5/BambuMQTT v2): connect,
get_version, and the full pending-request correlation all worked
end-to-end, returning real firmware versions for every module (main
board 01.01.05.00, both AMS units, AMS HT, etc). The other configured
printer (P1P, forced v1) failed with a real, expected TLS error - it
doesn't chain to the bundled CA, matching the already-documented P1P
quirk; needs the ca_cert_path/fetch_bambu_cert workflow, not a code fix.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MWQVtBGxHYLkVw2kBupd5g
Removed adapters/ (rumqttc MQTT, suppaftp FTPS, reqwest HTTP), plate_changer/
(tokio-serial), discovery/, and cache.rs (rusqlite) — src/printer/ already
covers 'talk to a printer' as simple stubs, so keeping a second, more complex
version alongside it was redundant. main.rs goes from 6 concurrent tasks to
2 (uplink + go2rtc). uplink/ drops channels, jitter, and Send-bound futures.
Verified with cargo build + cargo run --example printer_polymorphism.