35d2d96f05a11e03044660089db8f3c4e4d0ac7d
44
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
35d2d96f05
|
Say when a filament source is being ignored, and what a real failure looked like
Two things a real end-to-end run on the P1P taught, neither of them a fix.
`start_uploaded_file` silently discarded its `filament` argument for a bare
`.gcode`. That is protocol-correct - the v1 `gcode_file` request has only
`command` and `param`, with no `use_ams`/`ams_mapping` to put it in, so a
bare gcode draws whatever its own tool changes select. But the argument is
required, and a caller passing `FilamentSource::ams([3, ..])` would
reasonably believe it took effect. "A filament choice quietly overridden" is
the precise bug `FilamentSource` was introduced to stop, so this now warns
instead of dropping it.
The other is the whole-path confirmation, which is worth having written down
because it ended in a *physical* failure rather than a clean success. The
derivation read the project's one PETG/GFG99 filament and produced slot 3;
the printer echoed `tray_tar: 3`, so it agreed; 297,227 bytes went up with an
exact byte count; `IDLE -> PREPARE -> RUNNING`; it heated to 255C - and then
paused itself at stage 54 with `print_error: 0x07008003` and
`hms: [{attr: 0x07002300, code: 0x00020004}]`, a broken-filament fault on the
slot it was loading.
Every line of this code did the right thing and the print still did not
happen. That is the argument for the thing this commit documents rather than
changes: `command_outcome` can only report what the printer said about the
command, and "accepted" has now been shown to be three distinct steps away
from "printing". Anything that reports job status has to watch `gcode_state`,
`print_error` and `hms` after the fact - a job dispatcher that trusts the
start reply will report a successful print that never ran.
Also noted: `tray_now` is not a usable confirmation on its own. It went
255 -> 3 -> 254 over the run, so only the transition through the real slot
number is evidence; a single read can land on either sentinel.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
790c951cb4
|
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. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
3d907d0443
|
Confirm the light and the upload on a real P1P
Four things were shipped unverified because the printer was off when they
were written. Three are now confirmed first-hand (P1P, 2026-10-15), and one
is settled in the opposite direction from what was assumed.
**`set_light` had never once reached a printer**, and `completion::run`
blinks a finished print's chamber light with it. It works - but "accepted" is
not "it changed": `ledctrl` answers `result: "success"` as soon as the
command parses, exactly like the print-control verbs. So `lights_report` is
now modelled on `PrintReport`, which is the only read-back there is, and the
probe polls it rather than printing a reassuring message. Setting the light
on and off reads `[{"node":"chamber_light","mode":"on"}]` / `"off"` back,
about 3s later. The field was observed on v1 even though only the v2 schema
declares it.
**The blink itself works at the rate it ships at.** `completion::celebrate`
is now public so the probe runs the real function instead of a copy: twenty
`ledctrl` commands half a second apart, which a printer is entitled to
refuse. None were refused, it returned after 10.35s, and the light ended
`off` as specified - a blink that leaves the light on would look like a
printer still working.
**The firmware's own `flashing` mode is not the better option.** It is
accepted, and the reply echoes all four parameters back
(`led_on_time`, `led_off_time`, `loop_times`, `interval_time`), so the
printer does parse them. But `lights_report` never reports `flashing` - it
stayed on the previous mode for 12s of polling. A firmware blink is therefore
unobservable: nothing can confirm it happened or tell when it ended, while
the self-driven blink is built from a call that reads back. That is a better
reason to keep `celebrate` than the one in the comment it replaces, which was
that `loop_times` semantics were unconfirmed. Whether the light physically
blinks is still unwitnessed - nobody was watching the machine.
**Multi-MB FTPS upload works, and is slow.** A 4.96 MB project archive went
up with an exact byte count and came back byte-identical (sha256). But it
took 31.3s - about 0.2 MiB/s - so the 62 MB project already on that card
would take six and a half minutes. Anything that uploads a job needs a
timeout budgeted in minutes, not seconds. The claim in `download`'s comment
that these files are "a few hundred KB" is gone; the card disproves it.
Also adds `delete`, because the probe left a 5 MB file on a real SD card and
there was no way to remove it. One explicit path, no globbing: the convenient
version of this deletes something somebody printed from last week. Used to
clean up, and the card is back as it was.
New probe steps: `light on|off|flash`, `blink`, `upload <local> [name]`,
`delete <path>`. 51 tests still pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
6766ebbe80
|
Clear the clippy warnings, except the two that are not cosmetic
Down from 29 warnings to 2, in the spirit of
|
||
|
|
aa49cbba49
|
Model vt_tray and the stage fields, and use them
## vt_tray closes a hole in the filament decision
`filament_source_for` used to refuse a project that wanted the external
spool, on a printer that had an AMS, purely because there was no way to see
what the spool held - and claiming a match without looking is how the
`use_ams: false` bug happened. Now that `vt_tray` is on `PrintReport`, the
question can be answered: `external_spool_satisfies` reuses `map_to_trays`
with the spool as the single candidate tray, so an `Ok` means every filament
matched it.
The decision is now: the AMS first when there is one, because a project
sliced for the AMS should use the AMS even if the spool happens to hold the
same filament; the spool if nothing in the AMS fits and the spool does; and
an error otherwise, reporting the AMS mismatch rather than the spool one
since that names what was wanted against what was loaded. A reported spool
that does not match is a real negative now and is refused.
One case stays unverifiable: a v2 printer reporting neither AMS trays nor
`vt_tray`. v2 omits the key entirely, so absence means "not reported", not
"empty". That one is logged as an unverified assumption instead of being
passed off as a match - and it is the same reason `EXTERNAL_SPOOLS` is still
a constant. Counting reported spools would give 0 on exactly the printer the
ams_mapping schema says has two.
Tests use the real P1P capture's own spool (PETG P9599a25 in D3C5A3FF): it
satisfies a project asking for that filament, and does not satisfy one asking
for the AMS's blue GFG99. Those two were indistinguishable before.
## stage reaches the cloud
`stg_cur`/`stg` are modelled, and `stage` is now on `PrinterTelemetry`,
through `continuum-schemas` and the ts-types mirror. Without it progress is
ambiguous in a way that reads as a fault: on the confirmed P1P run `percent`
climbed to 14% while `layer` stayed 0 for minutes, the printer walking
`stg_cur` 54 -> 14 -> 13 -> 0 before laying the first layer. A console
showing 14% at layer 0 with no stage looks stuck.
A zero stage means "printing" and has its own test on both sides, because
`skip_serializing_if = "Option::is_none"` plus one `unwrap_or_default` is all
it takes to turn "printing" into "no stage".
The backend test asserts a *wrongly typed* stage is rejected, not just that a
valid one is accepted: `PrinterTelemetrySchema` permits unknown properties -
verified - so `{ stage: 54 }` validates whether or not the field is declared.
The negative is what fails if someone removes it.
The live capture fixture is left alone. It predates the field, the field is
optional, and editing a capture by hand would defeat the point of having one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
6ff0c9a649
|
Derive the AMS mapping from the project, and blink on completion
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>
|
||
|
|
7d68e8e069
|
Make the filament source explicit, and print from the AMS
`start_uploaded_file` hardcoded `use_ams: false` / `ams_mapping: ""`, which silently overrode whatever the slicer had chosen. On a P1P with an AMS and a project sliced for it, that is not a missing feature - it points the print at the external spool. Confirmed on 2026-10-11: the print started, heated to 250C, reached 1%, then the printer paused itself with `print_error: 50364420` and `tray_now: 255`. Nothing in the command's reply said so; `result` was `success`, because the command had been accepted. So `FilamentSource` is now a required argument with no default, because there is no safe default - picking wrong draws the wrong filament or none. The `ExternalSpool` variant sends the empty *string* the schema's trailing `oneOf` demands, and `ams` takes slots per project filament index and pads to every tray the printer has, per the padding rule in `schemas/bambulab/v1/common/ams/ams_mapping.schema.json`. It refuses an empty mapping (padding `[]` would mean `use_ams: true` with nothing mapped), one longer than the printer has trays, and any negative that is not -1. Confirmed live with the real mapping: `ams_mapping: [3,-1,-1,-1,-1]`, `use_ams: true` took the printer to RUNNING, `tray_now` became 3 - the slot the project was sliced for - and it ran past 1% to 13% with `print_error: 0`, where the external-spool attempt had paused. Finding the mapping needed reading the project back off the card, so FTPS gains `download`/`download_file` and the probe a `fetch` step. The archive's `Metadata/slice_info.config` lists each plate's filaments with `tray_info_idx` and colour - here one PETG GFG99 at `#2850E0`, matching AMS slot 3's `2850E0FF` - and `Metadata/plate_1.gcode` is present, which also confirms the hardcoded `param`. `upload_project_file` is left on the external spool and is the one remaining place that can pick wrong, because it is vendor-generic and an AMS mapping is a Bambu concept. It is unreachable - there is no start-print command on the uplink - and the fix is for it to derive the mapping from the `.3mf` it already holds, which needs a zip reader and so a dependency decision. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
79df3e7153
|
Record what a real sliced project did, and widen the start guard
A second live run, this time against a project the owner sliced in Bambu Studio rather than a file already on the card, and through `start_uploaded_file` itself rather than a hand-built body - so the root-vs-cache fix is confirmed end to end. `gcode_file` in the printer's own status came back as `cube-test-2026-10-11.gcode.3mf`, 150 layers, print_type `local`. It also found a defect the first run could not. The print heated to 250C, reached 1%, and then the printer paused itself: `print_error: 50364420` (`0x03008004`), `hms: []`, `tray_now: 255`. 255 is the external spool, which is exactly what the hardcoded `use_ams: false` selects - so that pair overrode the slicer's own filament choice and pointed an AMS-sliced project at an external spool. The first run looked fine only because it was stopped during PREPARE, before filament mattered. The doc comment said that pair was "accepted, and the print ran"; it now says what it actually does, because silently changing which filament a print uses is worse than not supporting AMS at all. The `start` guard demanded IDLE, which meant the harness refused every print following a cancelled one - a Bambu reports an operator cancellation as FAILED. IDLE, FINISH and FAILED are all terminal; PREPARE, RUNNING, PAUSE and SLICING mean the machine is committed, and anything unrecognised is still refused rather than assumed idle. `bambu_cache_list` now prints nozzle/bed temperatures and remaining time. State plus layer number cannot tell a print that is working from one that is stalled: a P1P sits at layer 0 for minutes during bed levelling and purge, and the only thing moving in that window is the heat. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
4d644937af
|
Confirm the print path on a real P1P, and stop discarding its answer
Print control (pause/resume/stop) and print start are now both confirmed
against the P1P on the LAN, and doing that turned up three real defects.
`start_uploaded_file` published fire-and-forget, so the printer's reply was
thrown away. Asking it to print a file it does not have returned `Ok(())`,
and watching `print.push_status` could not tell that apart from a successful
start, since both leave the machine IDLE for a while. The printer does in
fact answer - `{"command":"gcode_file","param":"...","reason":"error
string","result":"fail"}` - and it echoes the `sequence_id` we send, so
`request` correlates it. Both this and `control_print` now go out as
correlated requests and run the reply through `print_command_outcome`.
The upload and the print command disagreed about where files live. `upload`
cwd'd into `cache/` while the printable projects are in the card's root: the
root holds 43 `<name>.gcode.3mf` archives, `cache/` holds the printer's own
extracted `<project>_plate_N.gcode` working copies with `.gcode.bbl`
sidecars. Those extracted copies are not independently printable -
`gcode_file` with `param: "/cache/Cube_plate_1.gcode"` was rejected for a
file the FTPS listing shows is right there - while `project_file` with
`url: "ftp:///Cube.gcode.3mf"` started a print. So the destination is now a
parameter and both halves point at `SD_ROOT`.
What the hardware settled, recorded in the doc comments: the `ftp:///` url
spelling is right, `param: "Metadata/plate_1.gcode"` works for a
single-plate project, `use_ams: false` + `ams_mapping: ""` is accepted, and
a stop during PREPARE/RUNNING moves the printer to FAILED, which is how
Bambu reports an operator cancellation. What it did not settle: all three
control verbs answer `result: "success"` on an *idle* printer, so the reply
means "accepted", not "the state changed" - callers still have to read
`gcode_state` back.
`PrinterCommand::ControlPrint` makes this reachable from the cloud at all;
until now pause/resume/stop existed in the proxy with no way to send them.
Still no start-print variant, now for a different reason: the path around it
is missing, not the hardware proof.
Also: `list_dir` for any directory (how the layout above was found, without
sending a print command to guess), `request_raw` for probing an unconfirmed
command shape, and two examples - `bambu_cache_list` (read-only) and
`bambu_print_probe` (gated steps, `start` needing both IDLE and
--yes-start-a-real-print).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
4ab95ff7ce
|
Compile the modules once, and clear the warning backlog
`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> |
||
|
|
8f7a332956
|
Make the uplink match the written protocol, and test that it does
Adds tests/uplink_protocol.rs, the gateway half of a contract test whose other half is continuum-backend/tests/edge_protocol.test.ts. It checks that every message the backend sends is one this gateway can read, and that what this gateway writes is what the schema says it writes. It found two things immediately. Telemetry was serializing empty fields as explicit nulls. serde emits `"job_name": null` for an empty Option unless told otherwise, and the backend's validator treats a missing key and an explicit null as different things - so every batch containing an idle printer, which by definition has no job name, would have been rejected whole. Every optional field now has skip_serializing_if, confirmed against a live capture: an idle P1P's snapshot has no nulls in it at all. ServerMessage had no variant for the handshake being rejected. A bad or expired token would have arrived as "unparseable message from cloud, ignoring" and then been retried every five seconds forever, which in the logs is indistinguishable from a network fault. HelloError is now a real variant, logged at error level naming the env var to go fix, and it stops the connection attempt instead of looping on it. Also exposes config and uplink from lib.rs so the test can reach them, same reason fleet and printer are already there. Verified live against a P1P and an H2C for the whole run, including the H2C finishing its print: RUNNING 98% -> FINISH 100% layer 50/50 with the nozzle cooling 84C -> 76C, all of it arriving through the uplink, no warnings. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
d25614b313
|
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. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
1d95451c4c
|
Fix print.push_status parsing, and stop losing reports silently
Pointed at a real H2C that was 20% into a job, `print_status()` returned
nothing at all. The printer was in fact pushing a full status object every
second; every one was arriving, failing to deserialize, and being dropped
invisibly by an `if let Ok(report) = ..` with no `else`.
That silence produced two confidently-wrong claims in our own comments,
both corrected here:
- `pushall`'s doc said whether v2 emits `push_status` unprompted was "an
open question". It is not: v2 pushes a full 97-field object roughly once
a second from the moment you subscribe, no trigger needed. There is no
missing "other trigger" to hunt for.
- The "sequence gap" warning blamed the network for dropped v1 updates.
Those updates were not dropped in transit - they failed to parse, so
they never reached the counter, making the next one that did look like a
skip. Confirmed: seq 57, 62, 64 and 69 all arrived. After the fix, zero
gaps and zero parse failures across a 25s run against both printers.
Two concrete causes, both found with captured payloads:
- v1 trays carry `tray_temp`/`tray_time`; v2 trays carry
`drying_temp`/`drying_time`. All four were required, so *every* v1 push
carrying AMS data failed.
- A third tray shape existed that nothing modelled: an empty v2 slot sends
`{"id": "2", "state": 0}`, which matched neither `EmptyTray` (it denies
unknown fields, deliberately) nor `LoadedTray` (16 fields short).
A parse failure now reports itself, naming the printer, the sequence id and
the serde error. A report this process cannot parse is a defect in
`PrintReport`, and the only way to fix it is to be told which field.
`PrintReport` itself modelled 9 of 97 top-level fields, and none of them
were progress or temperature - it was built from a truncated example. It
now carries gcode_state, percent, layer counts, remaining time, nozzle/bed
temperatures, fan gears, wifi and HMS entries. `merge_from`'s two dozen
four-line blocks collapse to `newer.x.or(self.x.take())` one-liners: the
old shape made adding a field a three-place edit, and forgetting the merge
half produced a field that silently never survived a partial update.
Two scale traps documented where they bite: reported fan speeds are 0-15
gears, not percent and not the 0-255 PWM we send (`set_fan_speed(50)`
reads back as "7"); and v2 pins `sequence_id` to one value for minutes
while contents change, so it orders and identifies nothing there.
Two examples, and the distinction between them is the point. `live_reports`
is read-only and safe against a printer mid-job. `bambu_idle_checks`
writes, so it requires an explicit printer id and refuses to run unless
that printer reports `gcode_state: "IDLE"`.
Regression fixtures are verbatim captures off both printers, hardware
identifiers scrubbed - the only way to keep this from regressing without a
printer plugged in.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
704b65ac26
|
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. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
8b971e78e5
|
Declare proprietary licensing and enforce permissive-only dependencies
Continuum is intended to be sold, so every dependency has to permit distribution in closed-source commercial software. This crate carried `license = "UNLICENSED"`, which is not valid SPDX and made `cargo deny` fail outright; `LicenseRef-Proprietary` with `publish = false` is what marks it first-party instead. The note in deny.toml is the part that earns its keep: `src/go2rtc.rs` spawns go2rtc from PATH and expects the operator to have installed it, so we currently distribute nothing. go2rtc shells out to ffmpeg, and ffmpeg is LGPL-2.1+ only when built without `--enable-gpl` - most prebuilt binaries are GPL builds because they bundle x264. If we ever ship either binary, those build flags become our problem. Policy and full audit: continuum-common's docs/LICENSES.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
94484bb190
|
Update docs and prune examples superseded by real functionality
README/certs/README/.env.example/printers.example.toml were all still describing this as a learning-stage stub with no real printer protocol clients - rewritten to reflect the real Bambu MQTT client, cert auto-pinning, and marlin serial connection that now exist. - Remove examples/fetch_bambu_cert.rs: its manual fetch-and-save workflow is now what BambuPrinter::connect()'s ensure_trusted does automatically on every connect, so it has no remaining use case. - Fix examples/printer_polymorphism.rs, which no longer compiled - dispatch_gcode was replaced by real methods earlier this session; swapped in print_status() as the vendor-only-extension example. - Remove Config::bambu_ca_cert_overrides/bambu_require_valid_cert and the .env vars documenting them - confirmed dead (nothing read them), fully superseded by printers.toml's per-printer ca_cert_path/ require_valid_cert plus automatic trust-on-first-connect pinning. - Fix stale doc comments in printer/mod.rs and printer/bambu.rs that described certificate trust and MQTT as not built yet, or referencing the now-removed example. No behavior change in the daemon itself - verified with cargo build --examples and a live run of printer_polymorphism and auto_connect_bambu against the real P1P/H2C printers. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
11177db2ad
|
Prefix generated sequence_id instead of offsetting it
A printer's own free-running counter for unsolicited messages is always
a plain numeric string, so a non-numeric prefix ("cxproxy-") rules out
collision with it outright, rather than a large numeric starting offset
merely making collision improbable. Confirmed sequence_id need not be
numeric (live test, ISO-timestamp-prefixed value round-tripped fine) and
re-verified the new prefixed IDs round-trip through get_version on both
P1P (v1) and H2C (v2).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
||
|
|
1fd6529376
|
Add get_calibration; fix a real 10KB packet-size ceiling; revert bad correlation-field experiment
Real bug found and fixed live: both rumqttc clients default to a 10KB max packet size in both directions. A real H2C's get_version reply (25625 bytes - it reports a lot of modules: dual AMS, AMS HT, six WTM units, ...) blew straight through that on the v5 client and killed the whole connection outright (a real MQTT protocol error, not a timeout). Raised to 1MB on both clients - print.push_status reports can get large too. Also tried, and reverted after live-testing disproved it: replacing request()'s sequence_id correlation with a homemade continuum_correlation_id field, on the understanding that most replies echo back arbitrary extra request fields. True in general, false for get_version specifically - a real H2C's reply had exactly ["command", "module", "sequence_id"], nothing extra survived, so every get_version call started timing out. Reverted to sequence_id, which is what's actually proven (twice now, on both protocol versions) to round-trip for this command. Added BambuPrinter::get_calibration(option) - print.calibration gets a real, correlated reply (confirmed live: it does echo sequence_id) on both protocol versions, per the schema fixes in continuum-schemas. `option` is the request's own bitmask, left to the caller rather than defaulled to "run everything" - this triggers a real physical calibration sequence on the printer, not a read-only query like get_version, so it hasn't been live-tested here without explicit confirmation first. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MWQVtBGxHYLkVw2kBupd5g |
||
|
|
133db9e4aa
|
Add pushall + print_status cache; fix envelope, race, and matching bugs
PrintReport now flattens CommandEnvelope (sequence_id/command) - push_status.schema.json composes the shared envelope like every other Bambu message; the struct's old doc comment concluded otherwise from a capture that turned out to be an incomplete example (also missing dozens of other real fields), not proof the message lacks them. Added BambuMqttSession's PrintStatusCache: the in-memory print.push_status object, replaced wholesale on v2 (always full) or on a v1 message with msg != 1 (forced/full), merged field-by-field otherwise (v1's organic partial updates) via PrintReport::merge_from. Tracks last_sequence_id to warn on a gap in the organic stream. BambuPrinter gets pushall() (v1-only; pushing.pushall doesn't exist in v2's schema at all) and print_status(). Three real bugs found and fixed via live testing against an actual P1P, none of them guessable from the schema alone: - pushall's reply doesn't echo the sequence_id we send - the printer stamps its own free-running counter regardless of what triggered it. request()'s ID correlation silently can't work here; first fix attempt added request_next_of_category (wait for the next message under a category, no ID needed). - That undershot too: matching by category alone let an unrelated "print" message (that category carries many commands - gcode_line, pause, push_status, ...) satisfy the wait before the real push_status reply arrived, so pushall() returned Ok having consumed the wrong message - now keyed by (category, command). - A real ordering race: handle_incoming_report woke a waiting oneshot before updating the print_status cache, so a caller could resume and check print_status() before the write landed - pushall() returned Ok immediately followed by print_status() still showing None. Fixed by applying the cache update before sending on any matched oneshot. Superseded by real protocol knowledge before any of the above needed to ship, though: push_status carries its own msg field (1 = automatic/ partial, 0 = forced/full) - the actual, reliable, self-describing signal for which kind a message is, regardless of timing or which client (if any) triggered it. PrintStatusCache now reads that directly instead of inferring it from request correlation, and pushall() is plain fire-and- forget (publish, no wait) since there's nothing left to correlate for - confirmed live: msg reads correctly off a real P1P, and the cache populates/updates correctly with no gap warnings across several organic pushes. request_next_of_category is kept (a future case may still need "no correlation ID at all" for real), just no longer used here. Also: the poll loop now logs when it exits instead of returning silently - that silence is exactly what made a real dropped MQTT connection to a live H2C indistinguishable from "nothing happening" during testing. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MWQVtBGxHYLkVw2kBupd5g |
||
|
|
ce6a4c3806
|
Auto-pin certificates on connect; fix Custom TLS mode for real MQTT
connect() now calls ensure_trusted() before opening the MQTT session, so a newly-added printer with no ca_cert_path configured just works: bundled CA if it verifies, trust-on-first-connect auto-pin if it doesn't (P1P, at least) - the same thing examples/auto_connect_bambu.rs already did by hand, now automatic on every real connect. Wiring that in immediately surfaced a real, second instance of a bug the codebase already knew about: build_connector()'s BambuTls::Custom branch used add_root_certificate on the pinned certificate - but ensure_trusted pins a *leaf* cert (CN = the printer's serial number, issued by Bambu's own CA, not a root), for byte-exact fingerprint comparison, not chain validation. Handing that leaf to add_root_certificate asks OpenSSL to treat it as its own trust anchor - exactly the "self-signed certificate in certificate chain" failure test_tls_handshake's doc comment already described, just not fixed at this second call site (the one the real MQTT connection actually uses) until now. native_tls has no per- certificate verification callback hook (unlike rustls), so Custom now accepts any certificate at the TLS layer instead, relying on ensure_trusted having already fingerprint-verified this exact printer moments before - weaker than continuous pinning on every reconnect, but not a fresh regression: pinning a leaf as a root here never worked. Verified live: deleted P1P's existing pin entirely, ran the daemon, and watched it auto-fetch, pin, and connect on the very first attempt - the freshly-pinned certificate came back byte-identical to the one that was there before. Both configured printers (P1P forced v1, H2C auto-detected v2) now connect successfully from a cold start with zero manual steps. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MWQVtBGxHYLkVw2kBupd5g |
||
|
|
db9b575b55
|
Wire fleet::load into main.rs; fix request() reply unwrapping
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
|
||
|
|
5d6e2e9e03
|
Add BambuPrinter::get_version, the first real request/response round trip
Documents request()'s real constraint on BambuMqttSession directly (not all Bambu commands map 1:1 to a reply, and even where one exists its shape can differ by protocol version - e.g. pushing.pushall triggers an ongoing print.push_status stream rather than a direct reply, and print.calibration data lives inside push_status on BambuMQTT v1 but gets its own report on v2 - so request() is only correct for genuinely one-shot, sequence_id-correlated commands like info.get_version). get_version itself is Bambu-only (no GenericPrinter equivalent, matching test_tls_handshake/fetch_certificate/ensure_trusted's existing pattern): builds the request via continuum-schema-instance, sends it through BambuMqttSession::request, and parses the reply into the existing hand-written GetVersionResponse - reusing the struct already validated against real captured Bambu JSON in bambu_commands_demo.rs, rather than introducing a second response representation. Verified structurally (no real broker reachable from here - confirmed this sandbox has no route into the printer's LAN at all, not just a blocked port): connect fails gracefully against an unreachable host, and get_version correctly reports "not connected" rather than panicking when called without a live session. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MWQVtBGxHYLkVw2kBupd5g |
||
|
|
773898b682
|
Real MQTT wiring for Bambu: connect, auto-detect v5/v3.1.1, request/reply
Removes GenericPrinter::receive (two disconnected fire-and-forget stubs that didn't actually give request/response) in favor of a real mechanism in the new src/printer/bambu_mqtt.rs: - BambuMqttSession wraps whichever rumqttc client a connection ended up using (v3.1.1's AsyncClient/EventLoop for BambuMQTT v1, v5's for BambuMQTT v2 - different types, different event/error shapes) behind one interface. `open()` respects ForceV1/ForceV2, and for Auto tries v5 first, falling back to v3.1.1 on any failure (see the module doc comment for why "any failure", not a precisely-matched version- rejection code - unverified against a live printer, no broker here to test against). - Reuses BambuTls::build_connector (now pub(crate)) for rumqttc's TLS transport via TlsConfiguration::NativeConnector, instead of a second, separate TLS trust story - hence rumqttc now pulls in use-native-tls instead of its default rustls stack, so there's one TLS implementation in the binary, not two. - request() publishes and waits (with a timeout) for a report whose sequence_id matches, correlated via a shared pending-request table that a spawned background task completes as reports arrive - the first real tokio::spawn in this crate, flagged as deferred since printer/mod.rs's very first doc comment; this is why it was deferred until now, and why it's needed now (polling the eventloop has to run continuously, independent of whatever call site is waiting). - publish()/publish_raw() are the fire-and-forget half, for commands like gcode_line that only get acknowledged implicitly. BambuPrinter::connect/send_gcode_line/publish now call the real session instead of printing what they would do. Verified against 192.0.2.1 (a reserved, never-routable test address): both AsyncClient/EventLoop pairs actually attempt a real TCP connection and fail gracefully (network timeout, not a panic) when nothing answers, and Auto genuinely attempts both while ForceV1 skips straight to v3.1.1. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MWQVtBGxHYLkVw2kBupd5g |
||
|
|
2c4ef21291
|
Add upload_project_file/send_gcode_line, real Marlin serial, Bambu protocol auto-detect
- GenericPrinter grows upload_project_file, send_gcode_line, and a generic upload_gcode<P> driver that streams a whole gcode file line by line through any vendor's send_gcode_line. - PrusaSerialPrinter's connect/send_gcode_line are real (not stubs): a held-open MarlinSerialConnection (src/printer/marlin_serial.rs) writes each line and waits for Marlin's "ok" before sending the next. Config changed from com_port: i16 to port: String + baud_rate: u32 to hold a real device path. - Bambu: BambuGenericPrinter + BambuV1Printer/BambuV2Printer collapsed into one BambuPrinter, since which MQTT/BambuMQTT version (v3.1.1/v1 vs v5/v2) a printer speaks is discovered at connect() time (or forced), not something config should pick a type for - the same physical printer can change after a firmware update. printers.toml gets a new "bambu" vendor (auto-detect, the default) alongside bambu_v1/bambu_v2 (force a specific one). - Bambu's gcode_line request is now built for real via continuum-schema-instance against continuum-schemas' JSON Schema, picking the v1 or v2 schema directory based on the detected protocol version, instead of a hand-written serde struct. - Added rumqttc as a dependency (not wired up yet - the real MQTT v5-then-v3.1.1 fallback connect and the publish/receive plumbing are still stubs). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MWQVtBGxHYLkVw2kBupd5g |
||
|
|
a2d4e578ec
|
Replace em dashes with hyphens across docs and comments | ||
|
|
1583f531fa
|
Add print.rs (AmsState/AmsUnit/AmsTray/XcamSettings), fix print vs pushing
Mirrors continuum-schemas' schemas/bambulab/ restructuring: PushallResponse removed (it never existed on the wire — pushall's request goes to "pushing", but the state-push stream it triggers arrives under "print", a different root key, not a direct reply). BambuReport::Pushing(...) -> BambuReport::Print(PrintReport). AmsTray is an untagged enum (Empty/Loaded) rather than one struct full of Option<T> fields, matching the JSON Schema's oneOf for the same slot - #[serde(deny_unknown_fields)] on EmptyTray is load-bearing: untagged enums try variants in order, and a loaded tray (superset of Empty's one field) would otherwise incorrectly match Empty first. Verified against the full real ams/xcam example from the conversation via examples/bambu_commands_demo.rs - both the empty and loaded tray in that payload deserialize to the correct enum variant. |
||
|
|
4eef52714b
|
Move bambulab JSON Schemas to continuum-schemas (moved+restructured there)
Schema definitions belong in the schemas repo, not the service that happens to be their only current consumer — see continuum-schemas' commit for the restructuring (envelope.schema.json unversioned at bambulab/ root, v1/ split into request/ and report/ subdirectories). |
||
|
|
42eb0b62d5
|
Add JSON Schema documentation for bambulab/v1, with real inheritance
Mirrors src/printer/bambu_commands/ as language-agnostic schema docs (not used for codegen — the Rust structs stay hand-written). envelope.schema.json is the base; get_version/pushall request+response schemas extend it via allOf + $ref, which is genuinely closer to real inheritance than anything else in this project's schema stack: proto and Rust only ever gave us composition (embed a field holding the base struct), never a mechanism that reuses a *referenced* schema's constraints automatically. Verified with the jsonschema Python library, not just written by hand: all three real examples from the conversation validate correctly, and — importantly — negative tests confirm the base schema's required fields and the command consts are actually enforced through the allOf chain, not just documented. README covers the one real gotcha (additionalProperties: false on a base schema silently breaks allOf composition, since allOf validates each sub-schema against the whole instance independently rather than merging them). |
||
|
|
166761479b
|
Replace proto-based bambu_protocol with plain-Rust bambu_commands/
One file per command (get_version.rs, pushall.rs), each holding its request AND response together. CommandEnvelope (sequence_id + command) is the shared-fields piece every command embeds via #[serde(flatten)] — the 'inheritance' replacement for these structs, same composition idea as BambuGenericPrinter in bambu.rs, just expressed as a struct field instead of impl-block delegation. examples/bambu_commands_demo.rs round-trips both commands against Bambu's exact real JSON (from the original conversation) and confirms byte-for- byte matches, same as the removed proto version did — this replacement is behaviorally identical, just without protoc/build.rs in the loop. |
||
|
|
8e5d85202f
|
Wire InfoCommand through as one real, runnable example
BambuRequest::Info(InfoCommand) is now live (was commented out) — examples/bambu_protocol_demo.rs constructs one, serializes it, and prints it next to Bambu's real wire JSON for comparison. Run it and copy this exact shape (proto message -> use it here -> add an enum variant) for every other command. |
||
|
|
6e18215af5
|
Add GenericPrinter::set_fan_speed() across all five printer kinds
Same override pattern as connect(): required, no default body, five completely different bodies. - BambuGenericPrinter::set_fan_speed_impl() is shared by BambuV1Printer and BambuV2Printer (composition reuse, same as connect_impl) - both speak the same MQTT gcode-injection mechanism. - PrusaLinkPrinter and PrusaSerialPrinter both send the same M106 gcode but over different transports (HTTP command injection vs. raw serial bytes) - they share the percent->PWM conversion despite having unrelated connect() implementations. - KlipperPrinter uses a hypothetical Moonraker-native endpoint that takes a percentage directly - no PWM conversion at all, since that math only applies to the gcode-speaking vendors. percent_to_pwm() lives in mod.rs as a private free function rather than a trait default: it's genuine shared logic, but only for the subset of vendors that need it, which is exactly the case a trait default can't express cleanly. Visible to bambu.rs/prusa.rs via super:: because Rust's module privacy reaches into child modules. Verified with cargo check --all-targets and a full run of printer_polymorphism: 50% converges on the same PWM value (127) across all three gcode-based printers, Klipper's native path takes 0.50 directly. |
||
|
|
aa471ff906
|
Fix real bug: pinning a device leaf cert via add_root_certificate is unreliable
This broke against a real P1P: after auto-pinning, the immediate retry failed with 'self-signed certificate in certificate chain' / 'unable to get local issuer certificate' - the exact error OpenSSL gives when a trust-anchor certificate isn't a well-formed CA, or when a device presents a chain (leaf + its own separate self-signed root) that doesn't terminate at whatever got pinned. My earlier test only covered a leaf that happened to have CA:TRUE (openssl req -x509's default), which masked this. Fixed by switching Custom(pem) from chain validation to true fingerprint pinning: connect with verification off, then byte-compare the certificate actually presented (via to_der()) against the pinned bytes, rather than asking OpenSSL's PKI path-builder to accept an arbitrary leaf as a root. BundledCa/Insecure are untouched - this only affects the Custom path. Reproduced the exact failure locally (a leaf signed by a separate self-signed root, served as a 2-cert chain - a properly non-CA leaf, not my earlier accidentally-CA:TRUE test cert) before fixing it, then verified against that same repro: first-run auto-pin now succeeds, second run is silent, and - regression check - presenting a genuinely different certificate after pinning still correctly fails, with a clear message instead of an opaque OpenSSL error. |
||
|
|
a77e93940f
|
Fix ensure_trusted(): pin by serial number, not the arbitrary id label
id in printers.toml is just a label you chose for config purposes - nothing ties it to a specific physical printer, and renaming it (or reusing it for a different unit down the line) would silently break the pin lookup and re-trigger trust-on-first-connect for hardware that was already trusted. The serial number is the one thing about a printer that can't change, so that's what a pin should be keyed by: certs/pinned/<sn>.pem instead of certs/pinned/<id>.pem. Verified against a real TLS server: pinned a printer under one id, renamed it in printers.toml with the sn left unchanged, and confirmed the second run found the existing pin silently (no re-TOFU) rather than re-pinning. |
||
|
|
af2f20a45b
|
Add BambuGenericPrinter::ensure_trusted() — smooth auto-connect, SSH-style
The manual fetch_bambu_cert workflow works but needs two commands and a printers.toml edit. ensure_trusted() collapses that into one call, using the same trust model SSH uses for host keys: - a pin already on disk (certs/pinned/<id>.pem) is used directly, no new trust decision is made on every run - no pin yet + bundled CA verifies fine (current-gen printers): nothing to do - no pin yet + bundled CA fails (P1P, etc.): auto-pins via trust-on-first-connect, same as fetch_bambu_cert, but automatic - an *explicit* pin (you set ca_cert_path yourself) never gets silently auto-pinned over — a failure there is a real error examples/auto_connect_bambu.rs demonstrates the one-command flow. Verified all three states against local test servers, not just compiled: first run with no pin auto-pins and connects; second run against the same server is silent (no re-TOFU message) and just verifies against the pin; third run after swapping the server's certificate correctly FAILS instead of silently re-pinning — confirms the security property survives the smoother UX. |
||
|
|
88603b58c3
|
Fix Bambu TLS: relax hostname/IP verification, keep chain trust enforced
Real hardware surfaced this: H2C failed with 'IP address mismatch', not a chain-of-trust error like P1P's 'self-signed certificate'. Bambu printer certs don't carry their DHCP-assigned LAN IP as a SAN, so native-tls's default hostname check will never pass against a real printer. Added danger_accept_invalid_hostnames(true) unconditionally in build_connector() — this only skips the SAN/IP match, it's independent from danger_accept_invalid_certs (chain trust), which stays fully enforced for BundledCa/Custom. Verified both directions against local test servers, not just compiled: - a cert signed by a trusted CA, with a SAN that does NOT match the connecting IP, now verifies OK (previously failed exactly like H2C did) - a cert signed by an UNTRUSTED CA still correctly fails verification, confirming this change didn't weaken chain-of-trust checking |
||
|
|
8572d564b1
|
Split BambuPrinter into BambuGenericPrinter -> BambuV1Printer/BambuV2Printer
Completes the GenericPrinter -> BambuGenericPrinter -> BambuV1/V2 chain this project wanted from the start. BambuGenericPrinter holds the fields and behavior every Bambu printer shares (access code, serial number, CA trust, the TLS test/fetch methods); BambuV1Printer and BambuV2Printer each *have* one (composition) and are now genuinely separate types, ready to carry flavour-specific report-schema fields later. Also threads through a new 'sn' (serial number) field the real MQTT topics will need. Adds real 'fetch the CA automatically' capability, verified against a live TLS server (not just compiled): - BambuGenericPrinter::fetch_certificate() does trust-on-first-connect — connects once with verification disabled, captures the certificate the printer actually presents via native_tls's peer_certificate()/to_der(), and returns it as PEM. Deliberately a method you call once by hand (examples/fetch_bambu_cert.rs), not something connect() falls back to silently, since TOFU trusts whoever's on the network the moment you run it. Verified end-to-end against a local openssl s_server: the fetched PEM's SHA-256 fingerprint exactly matched the server's real certificate. - Verified the other direction too: test_bambu_certs (bundled-CA mode) correctly REJECTS that same test server's cert, since it wasn't signed by the real Bambu CA. Splitting BambuV1Printer/BambuV2Printer into distinct types broke the PrinterHandle::BambuV1(x) | PrinterHandle::BambuV2(x) or-pattern in both examples (or-patterns require every alternative to bind the same type) — fixed by giving each variant its own match arm. |
||
|
|
d8b7430296
|
Add printers.toml (gitignored) + real TLS cert test against actual hardware
New: - printers.example.toml (committed template) / printers.toml (gitignored, real hosts + access codes don't belong in git) — a list of real printers with vendor, host, access_code/api_key/com_port, and an optional per-printer CA override. - src/fleet.rs loads that file into ready-to-use PrinterHandles, matching vendor strings to the right constructor. - BambuPrinter::test_tls_handshake() does a real (blocking, one-shot) TLS handshake to port 8883 using that printer's configured BambuTls trust mode — no MQTT protocol, just 'does the certificate verify'. Added native-tls and toml as direct dependencies for this. - examples/test_bambu_certs.rs loads printers.toml and runs the handshake test against every Bambu entry. Also fixes a real bug found while testing against unreachable IPs: plain TcpStream::connect has no timeout and hung indefinitely on an offline printer — switched to connect_timeout (5s). Verified with cargo check --all-targets (0 errors) and by actually running test_bambu_certs against a local printers.toml (correctly errored on a missing cert file, then correctly timed out against unreachable test IPs instead of hanging). |
||
|
|
72ba1e1cdc
|
Bambu CA trust is per-printer, not per-fleet — P1P needs its own cert
Checked BambuStudio's resources/cert/ directly: it ships exactly one LAN-mode CA (the bundled bambu_ca2.pem) plus an unrelated cloud-API leaf cert — no second file to bundle for P1P. So a P1P's certificate is a genuine per-device case, same as older units' 'download it from the printer' flow, and a single Config-wide override path can't express 'most printers use the bundled CA, but this one doesn't'. Config.bambu_ca_cert_path (Option<PathBuf>) -> bambu_ca_cert_overrides (HashMap<printer_id, PathBuf>), parsed from a comma-separated CONTINUUM_BAMBU_CA_CERT_OVERRIDES env var. BambuTls::from_config renamed to ::resolve to make clear it's called once per printer with that printer's own override, not once for the whole config. Verified with cargo check --all-targets and a full run of cargo run --example printer_polymorphism. |
||
|
|
9eab5e5a04
|
Bambu TLS trust config + finish PrusaLink/PrusaSerial split
Certs: - Vendor Bambu's shared LAN-mode root CA (certs/bambu_ca2.pem, verified self-signed CA:TRUE, see certs/README.md for provenance/fingerprint). - Config gains bambu_ca_cert_path (per-printer override) and bambu_require_valid_cert (the allow/reject flag); printer::bambu::BambuTls turns those into BundledCa/Custom/Insecure. connect() doesn't perform a real handshake yet (no TLS-capable MQTT client wired in), just reports which trust mode it would use. Also reconciles a rename in flight (Printer -> GenericPrinter trait) and finishes the PrusaPrinter -> PrusaLinkPrinter/PrusaSerialPrinter split: added the missing GenericPrinter impl for PrusaSerialPrinter, fixed PrinterHandle's variant payload types, updated mod.rs's pub use list and doc comments, and updated the example to the new 5-variant shape. Verified with cargo check --all-targets (0 errors) and a full run of cargo run --example printer_polymorphism. |
||
|
|
d0a17ee50e
|
Simplify boilerplate for learning: drop real protocol code, minimal main.rs
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. |
||
|
|
5705f36c01
|
Simplify printer/ module for someone learning Rust
Drop reqwest, RPITIT+Send signature, and multi-variant error enum from the teaching example so the only new idea per file is the trait override itself. Real networking stays in adapters/, unaffected. |
||
|
|
9b3815fd25
|
Fix build errors: FTPS AsyncRead bridge, TLS connector type, unsafe env::set_var, dead code
Verified with cargo check + cargo run --example printer_polymorphism. |
||
|
|
03bd07fbd7
|
Add Printer trait + PrinterHandle enum domain model with polymorphism example | ||
|
|
2807b2b067
|
Initial boilerplate scaffold for continuum-proxy |