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
- 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
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.
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.
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.
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.
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.
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.
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.
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).
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.
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.