Commit Graph

18 Commits

Author SHA1 Message Date
octoturge 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).
2026-08-28 22:58:34 +00:00
octoturge 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).
2026-08-28 22:53:59 +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 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.
2026-08-28 21:33:05 +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 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
2026-08-28 21:13:28 +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 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.
2026-08-28 20:42:41 +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 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.
2026-08-28 18:22:38 +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 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.
2026-08-28 17:09:11 +00:00
octoturge 03bd07fbd7 Add Printer trait + PrinterHandle enum domain model with polymorphism example 2026-08-28 17:03:21 +00:00
octoturge 2807b2b067 Initial boilerplate scaffold for continuum-proxy 2026-08-28 16:25:26 +00:00