Commit Graph
16 Commits
Author SHA1 Message Date
octoturgeandClaude Opus 5 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>
2026-10-03 10:12:05 +00:00
octoturgeandClaude Sonnet 5 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>
2026-09-07 16:17:15 +00:00
octoturgeandClaude Sonnet 5 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
2026-09-07 14:39:55 +00:00
octoturgeandClaude Sonnet 5 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
2026-09-05 20:40:21 +00:00
octoturge a2d4e578ec Replace em dashes with hyphens across docs and comments 2026-08-29 08:13:54 +00:00
octoturge 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.
2026-08-28 23:19:20 +00:00
octoturge 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.
2026-08-28 22:49:06 +00:00
octoturge 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.
2026-08-28 22:41:55 +00:00
octoturge 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.
2026-08-28 21:40:11 +00:00
octoturge 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.
2026-08-28 21:22:16 +00:00
octoturge 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.
2026-08-28 21:18:46 +00:00
octoturge 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.
2026-08-28 21:04:56 +00:00
octoturge 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).
2026-08-28 20:48:05 +00:00
octoturge 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.
2026-08-28 20:38:10 +00:00
octoturge 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.
2026-08-28 18:04:13 +00:00
octoturge 03bd07fbd7 Add Printer trait + PrinterHandle enum domain model with polymorphism example 2026-08-28 17:03:21 +00:00