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