Replace em dashes with hyphens across docs and comments
This commit is contained in:
+2
-2
@@ -2,7 +2,7 @@
|
||||
name = "continuum-proxy"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
description = "Continuum edge gateway daemon — runs on-site, bridges printers to the cloud control plane"
|
||||
description = "Continuum edge gateway daemon - runs on-site, bridges printers to the cloud control plane"
|
||||
license = "UNLICENSED"
|
||||
|
||||
[lib]
|
||||
@@ -50,7 +50,7 @@ dotenvy = "0.15"
|
||||
|
||||
# Real printer protocol clients (MQTT for Bambu, HTTP for PrusaLink/
|
||||
# Moonraker, FTPS for gcode transfer) go here once you're ready to build
|
||||
# past the stubs in src/printer/ — see that module's doc comment.
|
||||
# past the stubs in src/printer/ - see that module's doc comment.
|
||||
continuum-rust-types = { path = "../continuum-schemas/packages/rust-types" }
|
||||
# FOR NON-LOCAL DEV
|
||||
# continuum-rust-types = { git = "https://git.octoturge.com/Continuum/continuum-schemas.git" }
|
||||
|
||||
@@ -6,7 +6,7 @@ cloud control plane (`continuum-backend`).
|
||||
|
||||
## This is a learning-stage boilerplate
|
||||
|
||||
This repo is deliberately minimal right now — real printer protocol clients
|
||||
This repo is deliberately minimal right now - real printer protocol clients
|
||||
(Bambu MQTT+FTPS, PrusaLink REST, Klipper/Moonraker WebSocket) are **not**
|
||||
implemented yet. Each vendor is a stub that just prints what it would do
|
||||
(`src/printer/bambu.rs`, `prusa.rs`, `klipper.rs`). The idea is to learn
|
||||
@@ -39,27 +39,27 @@ examples/
|
||||
test_bambu_certs.rs Loads printers.toml, does a real TLS handshake to each Bambu printer
|
||||
```
|
||||
|
||||
`src/printer/` is where the "inheritance" question lives — see that
|
||||
`src/printer/` is where the "inheritance" question lives - see that
|
||||
module's doc comment for the trait+enum pattern this project uses instead
|
||||
of class inheritance, and run `printer_polymorphism` to see it work.
|
||||
|
||||
`printers.toml` (gitignored — copy from `printers.example.toml`) holds real
|
||||
per-printer connection details: host, access code, and — since not every
|
||||
Bambu printer trusts the same certificate (see `certs/README.md`) — an
|
||||
`printers.toml` (gitignored - copy from `printers.example.toml`) holds real
|
||||
per-printer connection details: host, access code, and - since not every
|
||||
Bambu printer trusts the same certificate (see `certs/README.md`) - an
|
||||
optional per-printer CA override. `src/fleet.rs` loads it; nothing in
|
||||
`main.rs` uses it yet, but `test_bambu_certs` does, as a real (if narrow —
|
||||
`main.rs` uses it yet, but `test_bambu_certs` does, as a real (if narrow -
|
||||
just the TLS handshake, no MQTT) way to check a printer's certificate
|
||||
without needing the full MQTT client built yet.
|
||||
|
||||
## What's not here yet (on purpose)
|
||||
|
||||
- Real MQTT/FTPS/HTTP/WebSocket printer clients — `src/printer/*.rs` has a
|
||||
- Real MQTT/FTPS/HTTP/WebSocket printer clients - `src/printer/*.rs` has a
|
||||
`println!` where each of these will go.
|
||||
- Local SQLite buffering for telemetry across connectivity gaps.
|
||||
- LAN printer discovery (SSDP/mDNS).
|
||||
- The mechanical plate-changer interface (serial/GPIO).
|
||||
|
||||
Add these back in one at a time as you get comfortable with the Rust
|
||||
underneath them — each is its own small lesson (async I/O, a new crate's
|
||||
underneath them - each is its own small lesson (async I/O, a new crate's
|
||||
API, error handling for a real protocol) rather than something to absorb
|
||||
all at once.
|
||||
|
||||
+4
-4
@@ -4,24 +4,24 @@
|
||||
|
||||
Bambu Lab's shared root CA ("BBL CA2 RSA") used to verify a printer's LAN-mode
|
||||
MQTTS (port 8883) certificate. Current-generation Bambu printers present a
|
||||
certificate signed by (a chain rooted at) this CA — trust this one file
|
||||
certificate signed by (a chain rooted at) this CA - trust this one file
|
||||
instead of fetching/pinning a certificate per printer.
|
||||
|
||||
- Source: https://github.com/bambulab/BambuStudio/blob/master/resources/cert/printer.cer
|
||||
- Fetched: 2026-08-28
|
||||
- SHA-256 fingerprint: `E9:8F:19:57:8B:3F:12:4A:CE:6B:8A:24:7F:FE:DA:52:DC:99:C8:9F:D4:E7:D2:0C:82:82:99:77:B7:F3:35:02`
|
||||
- Verified with `openssl x509 -noout -text`: self-signed (`Issuer == Subject`),
|
||||
`Basic Constraints: CA:TRUE`, `Key Usage: Certificate Sign, CRL Sign` — a
|
||||
`Basic Constraints: CA:TRUE`, `Key Usage: Certificate Sign, CRL Sign` - a
|
||||
genuine root CA, not a per-device leaf certificate. Valid until 2050.
|
||||
|
||||
The "CA2" name implies there was a CA1 generation before it, and at least
|
||||
one current model — P1P — doesn't chain to this root either: BambuStudio's
|
||||
one current model - P1P - doesn't chain to this root either: BambuStudio's
|
||||
`resources/cert/` only has this one file (plus an unrelated
|
||||
`*.bambulab.com` leaf cert for their cloud API), so there's no second
|
||||
bundled file to grab for P1P. That means it's a genuine per-device
|
||||
certificate, the same "download it from the printer" case as the older
|
||||
units. `CONTINUUM_BAMBU_CA_CERT_OVERRIDES` (see `.env.example`) is a
|
||||
per-printer-id map for exactly this — there's no single fleet-wide
|
||||
per-printer-id map for exactly this - there's no single fleet-wide
|
||||
override, because different printers can legitimately need different
|
||||
certificates at the same time.
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
//! fetch_bambu_cert combined: for each Bambu printer in printers.toml,
|
||||
//! makes sure it has a working certificate, auto-pinning one via
|
||||
//! trust-on-first-connect if the bundled CA doesn't verify. Safe to run
|
||||
//! repeatedly — a printer that's already pinned (certs/pinned/<serial
|
||||
//! repeatedly - a printer that's already pinned (certs/pinned/<serial
|
||||
//! number>.pem) just gets re-verified against its pin, no network trust
|
||||
//! decision is made again.
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Run with: cargo run --example bambu_commands_demo
|
||||
//!
|
||||
//! Round-trips both commands against the exact JSON from Bambu's real
|
||||
//! protocol — proves the plain hand-written struct + serde approach
|
||||
//! protocol - proves the plain hand-written struct + serde approach
|
||||
//! matches byte for byte, same as the proto-based version did, with none
|
||||
//! of the build.rs ceremony.
|
||||
|
||||
@@ -34,7 +34,7 @@ fn main() {
|
||||
println!("matches Bambu's shape: {}\n", actual == expected);
|
||||
|
||||
// --- print report: arrives under "print", NOT "pushing" (pushall
|
||||
// triggers this stream, it isn't a direct reply to it) — includes the
|
||||
// triggers this stream, it isn't a direct reply to it) - includes the
|
||||
// full ams/xcam example, with one empty tray and one loaded tray, to
|
||||
// exercise the untagged Empty/Loaded split.
|
||||
let real_print_report = r#"{
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
//! "Trust on first connect": connects to one printer from printers.toml
|
||||
//! with certificate verification disabled, captures whatever certificate
|
||||
//! it presents, and saves it as PEM. Meant for a printer like P1P that
|
||||
//! doesn't chain to the bundled CA — after running this, point that
|
||||
//! doesn't chain to the bundled CA - after running this, point that
|
||||
//! printer's `ca_cert_path` in printers.toml at the saved file and
|
||||
//! `test_bambu_certs` should report it as verified from then on.
|
||||
//!
|
||||
//! This trusts whoever answers on the network *right now* — only run it on
|
||||
//! This trusts whoever answers on the network *right now* - only run it on
|
||||
//! a network you trust, ideally right after unboxing the printer.
|
||||
|
||||
use continuum_proxy::fleet;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
//! - `connect()` is called the same way regardless of vendor (polymorphism),
|
||||
//! but each variant's `impl GenericPrinter` runs completely different
|
||||
//! code (override).
|
||||
//! - `dispatch_gcode()` only exists on the two Bambu structs (extension) —
|
||||
//! - `dispatch_gcode()` only exists on the two Bambu structs (extension) -
|
||||
//! you have to `match` the enum back down to the concrete type to reach
|
||||
//! it, which is the trade-off for not having implicit downcasting.
|
||||
|
||||
@@ -35,7 +35,7 @@ async fn main() {
|
||||
PrinterHandle::PrusaLink(PrusaLinkPrinter::new("p3", "MK4", "192.168.1.51", "prusa-api-key")),
|
||||
PrinterHandle::PrusaSerial(PrusaSerialPrinter::new("p4", "MK3S+", 0)),
|
||||
PrinterHandle::Klipper(KlipperPrinter::new("p5", "Voron 2.4", "192.168.1.52")),
|
||||
// No host configured — this one will hit the Err path.
|
||||
// No host configured - this one will hit the Err path.
|
||||
PrinterHandle::Klipper(KlipperPrinter::new("p6", "Broken Voron", "")),
|
||||
];
|
||||
|
||||
@@ -52,7 +52,7 @@ async fn main() {
|
||||
// Vendor-only extension: reachable only after matching the concrete
|
||||
// variant back out of the enum. BambuV1Printer and BambuV2Printer are
|
||||
// separate types now (each *has* a BambuGenericPrinter rather than
|
||||
// being the same struct), so — unlike before the split — this needs
|
||||
// being the same struct), so - unlike before the split - this needs
|
||||
// two arms instead of one `A(x) | B(x)` pattern: an or-pattern requires
|
||||
// every alternative to bind the same type, and these no longer do.
|
||||
for printer in &fleet {
|
||||
@@ -64,9 +64,9 @@ async fn main() {
|
||||
}
|
||||
|
||||
// A second trait method, same uniform call site as connect(): every
|
||||
// vendor sends "50%" a completely different way — an MQTT gcode
|
||||
// vendor sends "50%" a completely different way - an MQTT gcode
|
||||
// command, Moonraker's native API, gcode over HTTP, gcode over serial
|
||||
// — but the caller doesn't need to know or care which.
|
||||
// - but the caller doesn't need to know or care which.
|
||||
println!();
|
||||
for printer in &mut fleet {
|
||||
let _ = printer.set_fan_speed(50).await;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
//!
|
||||
//! Loads printers.toml (copy printers.example.toml to get started, fill in
|
||||
//! your real printers) and attempts a raw TLS handshake to each Bambu
|
||||
//! printer's MQTTS port (8883) — no MQTT protocol involved, just "does the
|
||||
//! printer's MQTTS port (8883) - no MQTT protocol involved, just "does the
|
||||
//! certificate verify". Prusa/Klipper entries are skipped; they don't have
|
||||
//! this trust question.
|
||||
|
||||
@@ -28,7 +28,7 @@ fn main() -> anyhow::Result<()> {
|
||||
|
||||
fn report(name: &str, result: anyhow::Result<()>) {
|
||||
match result {
|
||||
Ok(()) => println!("{name}: OK — certificate verified"),
|
||||
Err(err) => println!("{name}: FAILED — {err}"),
|
||||
Ok(()) => println!("{name}: OK - certificate verified"),
|
||||
Err(err) => println!("{name}: FAILED - {err}"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,10 +9,10 @@ vendor = "bambu_v1" # bambu_v1 | bambu_v2 | prusa_link | prus
|
||||
host = "192.168.1.50"
|
||||
access_code = "REPLACE_WITH_ACCESS_CODE"
|
||||
sn = "REPLACE_WITH_SERIAL_NUMBER" # printer screen > Settings > Device (used for MQTT topics later)
|
||||
# P1P doesn't chain to the bundled CA — point this at a cert you've
|
||||
# P1P doesn't chain to the bundled CA - point this at a cert you've
|
||||
# downloaded from the printer itself. Leave unset to use the bundled CA.
|
||||
# `cargo run --example fetch_bambu_cert -- p1p ./certs/p1p.pem` can fetch it
|
||||
# for you (trust-on-first-connect — see that example's doc comment).
|
||||
# for you (trust-on-first-connect - see that example's doc comment).
|
||||
ca_cert_path = "./certs/p1p.pem"
|
||||
|
||||
[[printer]]
|
||||
@@ -22,7 +22,7 @@ vendor = "bambu_v2"
|
||||
host = "192.168.1.51"
|
||||
access_code = "REPLACE_WITH_ACCESS_CODE"
|
||||
sn = "REPLACE_WITH_SERIAL_NUMBER"
|
||||
# No ca_cert_path — current-generation printers verify against the bundled
|
||||
# No ca_cert_path - current-generation printers verify against the bundled
|
||||
# CA (certs/bambu_ca2.pem) fine.
|
||||
|
||||
# [[printer]]
|
||||
|
||||
+2
-2
@@ -13,7 +13,7 @@ pub struct Config {
|
||||
|
||||
/// Per-printer CA certificate overrides, keyed by printer ID. Most
|
||||
/// Bambu printers chain to the bundled shared CA (`certs/bambu_ca2.pem`)
|
||||
/// and don't need an entry here — but some models (P1P, at least) ship
|
||||
/// and don't need an entry here - but some models (P1P, at least) ship
|
||||
/// their own certificate instead of one signed by that shared CA, with
|
||||
/// no single file covering all of them. Look up a printer's own entry
|
||||
/// (if any) when constructing it; there's no fleet-wide "the" override.
|
||||
@@ -42,7 +42,7 @@ impl Config {
|
||||
}
|
||||
|
||||
/// Parses `"printer_id=path,printer_id2=path2"` into a lookup map. Empty
|
||||
/// input (the common case — no overrides needed) yields an empty map.
|
||||
/// input (the common case - no overrides needed) yields an empty map.
|
||||
fn parse_cert_overrides(raw: &str) -> HashMap<String, PathBuf> {
|
||||
raw.split(',')
|
||||
.filter_map(|entry| entry.split_once('='))
|
||||
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
//! Loads real printer connection details from a TOML file — see
|
||||
//! Loads real printer connection details from a TOML file - see
|
||||
//! `printers.example.toml` for the shape. The actual file (`printers.toml`
|
||||
//! by default) is gitignored: it holds real hostnames and access codes for
|
||||
//! your printers, which don't belong in version control.
|
||||
@@ -23,13 +23,13 @@ struct PrinterEntry {
|
||||
vendor: String,
|
||||
host: Option<String>,
|
||||
access_code: Option<String>,
|
||||
/// The printer's serial number — Bambu's real MQTT topics are addressed
|
||||
/// The printer's serial number - Bambu's real MQTT topics are addressed
|
||||
/// by serial (`device/{sn}/report`), not by our own `id`.
|
||||
sn: Option<String>,
|
||||
api_key: Option<String>,
|
||||
com_port: Option<i16>,
|
||||
/// Path to a printer-specific CA certificate. Leave unset to use the
|
||||
/// bundled Bambu CA — only needed for a printer that doesn't chain to
|
||||
/// bundled Bambu CA - only needed for a printer that doesn't chain to
|
||||
/// it (P1P, at least; see certs/README.md).
|
||||
ca_cert_path: Option<String>,
|
||||
#[serde(default = "default_true")]
|
||||
|
||||
+2
-2
@@ -19,8 +19,8 @@ async fn main() -> anyhow::Result<()> {
|
||||
tracing::info!(gateway_id = %config.gateway_id, farm_id = %config.farm_id, "starting continuum-proxy");
|
||||
|
||||
// Two things run at once for now: the cloud uplink, and the go2rtc
|
||||
// camera-restreaming process. More will join them later — printer
|
||||
// discovery, the plate changer — as you build past src/printer/'s
|
||||
// camera-restreaming process. More will join them later - printer
|
||||
// discovery, the plate changer - as you build past src/printer/'s
|
||||
// stubs. `tokio::select!` runs both and stops as soon as either one
|
||||
// returns; both loop forever below, so in practice this runs until the
|
||||
// process is killed.
|
||||
|
||||
+25
-25
@@ -4,19 +4,19 @@ use std::time::Duration;
|
||||
|
||||
use super::{GenericPrinter, PrinterBase, PrinterError};
|
||||
|
||||
/// Bambu's shared LAN-mode root CA ("BBL CA2 RSA") — see certs/README.md
|
||||
/// Bambu's shared LAN-mode root CA ("BBL CA2 RSA") - see certs/README.md
|
||||
/// for how this was obtained and verified. `include_bytes!` embeds it into
|
||||
/// the compiled binary at build time, so there's no file to ship alongside
|
||||
/// the executable.
|
||||
const BUNDLED_CA: &[u8] = include_bytes!("../../certs/bambu_ca2.pem");
|
||||
|
||||
/// Which certificate to trust when connecting to a Bambu printer's MQTTS
|
||||
/// port (8883 — Bambu's LAN mode is TLS-only, there's no unencrypted
|
||||
/// port (8883 - Bambu's LAN mode is TLS-only, there's no unencrypted
|
||||
/// fallback).
|
||||
pub enum BambuTls {
|
||||
/// Bambu's shared root CA. Correct for current-generation printers.
|
||||
BundledCa,
|
||||
/// A specific certificate instead — for a printer whose firmware
|
||||
/// A specific certificate instead - for a printer whose firmware
|
||||
/// doesn't chain to the shared CA (P1P, at least).
|
||||
Custom(Vec<u8>),
|
||||
/// Skip certificate verification entirely. The least safe option, only
|
||||
@@ -26,7 +26,7 @@ pub enum BambuTls {
|
||||
|
||||
impl BambuTls {
|
||||
/// Resolves the trust mode for *one* printer. Call this per printer,
|
||||
/// passing `config.bambu_ca_cert_overrides.get(printer_id)` — there's
|
||||
/// passing `config.bambu_ca_cert_overrides.get(printer_id)` - there's
|
||||
/// no single override for the whole fleet, because not every Bambu
|
||||
/// model chains to the same CA. `require_valid_cert = false` always
|
||||
/// means `Insecure`, regardless of whether that printer also has an
|
||||
@@ -41,7 +41,7 @@ impl BambuTls {
|
||||
}
|
||||
}
|
||||
|
||||
/// The CA certificate bytes to hand to the TLS layer — empty when
|
||||
/// The CA certificate bytes to hand to the TLS layer - empty when
|
||||
/// `Insecure`, since there's nothing to verify against.
|
||||
pub fn ca_bytes(&self) -> &[u8] {
|
||||
match self {
|
||||
@@ -57,12 +57,12 @@ impl BambuTls {
|
||||
let mut builder = native_tls::TlsConnector::builder();
|
||||
|
||||
// Bambu printer certificates don't carry their LAN IP as a Subject
|
||||
// Alternative Name — it's DHCP-assigned, so baking it in at
|
||||
// Alternative Name - it's DHCP-assigned, so baking it in at
|
||||
// manufacture time wouldn't make sense. Hostname verification has
|
||||
// to be off regardless of trust mode: what matters is whether the
|
||||
// certificate chains to a CA we trust, not whether its SAN matches
|
||||
// the specific IP we happened to dial today. Chain verification
|
||||
// itself stays fully enforced below for BundledCa/Custom — this
|
||||
// itself stays fully enforced below for BundledCa/Custom - this
|
||||
// narrows *what* gets checked, it doesn't turn checking off.
|
||||
builder.danger_accept_invalid_hostnames(true);
|
||||
|
||||
@@ -81,7 +81,7 @@ fn resolve_addr(host: &str) -> anyhow::Result<SocketAddr> {
|
||||
}
|
||||
|
||||
/// Minimal DER -> PEM encoder (base64, 64-char lines, standard
|
||||
/// header/footer) — just enough to save a fetched certificate in the same
|
||||
/// header/footer) - just enough to save a fetched certificate in the same
|
||||
/// format as the bundled one.
|
||||
fn der_to_pem(der: &[u8]) -> Vec<u8> {
|
||||
use base64::Engine;
|
||||
@@ -103,7 +103,7 @@ fn pem_to_der(pem: &[u8]) -> anyhow::Result<Vec<u8>> {
|
||||
}
|
||||
|
||||
/// Fields and behavior every Bambu printer shares, regardless of protocol
|
||||
/// generation — the layer between `GenericPrinter` and the version-specific
|
||||
/// generation - the layer between `GenericPrinter` and the version-specific
|
||||
/// `BambuV1Printer`/`BambuV2Printer` leaves below. Those two *have* one of
|
||||
/// these (composition) rather than a deeper inheritance chain; because
|
||||
/// Rust's field/method privacy is scoped to the *module* (this whole file),
|
||||
@@ -131,22 +131,22 @@ impl BambuGenericPrinter {
|
||||
&self.base
|
||||
}
|
||||
|
||||
/// Not part of the `GenericPrinter` trait — Prusa/Klipper have no
|
||||
/// Not part of the `GenericPrinter` trait - Prusa/Klipper have no
|
||||
/// equivalent, since FTPS gcode transfer is a Bambu-specific thing.
|
||||
pub fn dispatch_gcode(&self, file_name: &str) {
|
||||
println!("[{}] uploading {file_name} over FTPS", self.base.name);
|
||||
}
|
||||
|
||||
/// A real (blocking, one-shot) TLS handshake to port 8883 — no MQTT
|
||||
/// A real (blocking, one-shot) TLS handshake to port 8883 - no MQTT
|
||||
/// protocol, just "does the certificate verify" against this printer's
|
||||
/// configured trust mode.
|
||||
///
|
||||
/// `BundledCa` verifies normally (Bambu's shared CA is a proper,
|
||||
/// well-formed root — OpenSSL's usual chain validation handles it
|
||||
/// well-formed root - OpenSSL's usual chain validation handles it
|
||||
/// fine). `Custom` does something different: *fingerprint pinning*,
|
||||
/// not chain validation. A device's own leaf certificate often isn't a
|
||||
/// well-formed CA (no `CA:TRUE`), and the device may present it
|
||||
/// alongside a separate self-signed root you never captured — asking
|
||||
/// alongside a separate self-signed root you never captured - asking
|
||||
/// OpenSSL's normal path-building to accept an arbitrary leaf as a
|
||||
/// trust anchor is unreliable for exactly that reason (this is a real
|
||||
/// bug that shipped and failed against a real P1P: `add_root_certificate`
|
||||
@@ -154,7 +154,7 @@ impl BambuGenericPrinter {
|
||||
/// certificate chain` / `unable to get local issuer certificate` on
|
||||
/// the very next connection). So instead: connect with verification
|
||||
/// off, then compare the certificate actually presented against the
|
||||
/// exact bytes pinned, byte for byte. A mismatch is a hard failure —
|
||||
/// exact bytes pinned, byte for byte. A mismatch is a hard failure -
|
||||
/// no PKI judgment call, just "is this the same certificate as before."
|
||||
pub fn test_tls_handshake(&self) -> anyhow::Result<()> {
|
||||
let addr = resolve_addr(&self.base.host)?;
|
||||
@@ -169,7 +169,7 @@ impl BambuGenericPrinter {
|
||||
|
||||
if presented != pem_to_der(pinned_pem)? {
|
||||
anyhow::bail!(
|
||||
"{} presented a certificate different from the one pinned — could be a legitimate \
|
||||
"{} presented a certificate different from the one pinned - could be a legitimate \
|
||||
certificate rotation, could be something worse; re-run fetch_bambu_cert deliberately \
|
||||
if you're sure it's the former",
|
||||
self.base.name
|
||||
@@ -187,7 +187,7 @@ impl BambuGenericPrinter {
|
||||
/// presents, and returns it as PEM bytes.
|
||||
///
|
||||
/// This is meaningfully weaker than verifying against a CA you already
|
||||
/// trust — anyone on the network at the exact moment you run this gets
|
||||
/// trust - anyone on the network at the exact moment you run this gets
|
||||
/// trusted forever after. That's why it's a method you call once, by
|
||||
/// hand (see `examples/fetch_bambu_cert.rs`), rather than something
|
||||
/// `connect()` falls back to silently. Save the result to a file and
|
||||
@@ -205,15 +205,15 @@ impl BambuGenericPrinter {
|
||||
}
|
||||
|
||||
/// Makes sure this printer has a *working* trust configuration,
|
||||
/// auto-pinning a certificate via trust-on-first-connect if needed —
|
||||
/// auto-pinning a certificate via trust-on-first-connect if needed -
|
||||
/// the "smooth, no manual steps" version of the fetch_bambu_cert
|
||||
/// workflow, built the way SSH handles host keys:
|
||||
///
|
||||
/// - A pinned certificate already on disk (`cert_dir/<sn>.pem`) is used
|
||||
/// directly. No new trust decision gets made on every run — that
|
||||
/// directly. No new trust decision gets made on every run - that
|
||||
/// already happened once, this just re-verifies against it. Keyed by
|
||||
/// *serial number*, not `id`: `id` is just an arbitrary label you
|
||||
/// picked in printers.toml — rename a printer's `id` and the pin
|
||||
/// picked in printers.toml - rename a printer's `id` and the pin
|
||||
/// should still find it, because a pin certifies "this physical
|
||||
/// printer", and the serial number is the one thing about it that
|
||||
/// can't change.
|
||||
@@ -224,10 +224,10 @@ impl BambuGenericPrinter {
|
||||
/// produces a working connection, and saves it to `cert_dir/<sn>.pem`
|
||||
/// for every run after this one. This is the one moment a MITM
|
||||
/// active on your network *right now* could plant a certificate that
|
||||
/// gets trusted from then on — the same tradeoff SSH accepts on a
|
||||
/// gets trusted from then on - the same tradeoff SSH accepts on a
|
||||
/// first connection.
|
||||
/// - A printer with an *explicit* pin (you set `ca_cert_path` yourself
|
||||
/// in printers.toml) never auto-pins over it — a verification
|
||||
/// in printers.toml) never auto-pins over it - a verification
|
||||
/// failure there is a real error, not something to paper over,
|
||||
/// since that's exactly the signal pinning exists to give you (the
|
||||
/// cert rotated, or something worse).
|
||||
@@ -252,7 +252,7 @@ impl BambuGenericPrinter {
|
||||
}
|
||||
}
|
||||
|
||||
/// Shared by both `BambuV1Printer` and `BambuV2Printer` — same MQTT
|
||||
/// Shared by both `BambuV1Printer` and `BambuV2Printer` - same MQTT
|
||||
/// gcode-injection mechanism regardless of protocol generation. Real
|
||||
/// version would publish
|
||||
/// `{"print":{"command":"gcode_line","param":"M106 P1 S<pwm>"}}` to
|
||||
@@ -263,7 +263,7 @@ impl BambuGenericPrinter {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Shared "connect" logic both V1 and V2 delegate to — the real
|
||||
/// Shared "connect" logic both V1 and V2 delegate to - the real
|
||||
/// version would open the MQTTS session here. Still a stub, but no
|
||||
/// longer duplicated across two structs.
|
||||
async fn connect_impl(&mut self) -> Result<(), PrinterError> {
|
||||
@@ -278,7 +278,7 @@ impl BambuGenericPrinter {
|
||||
}
|
||||
|
||||
/// Older Bambu MQTT report schema (P1P, P1S, original X1 firmware).
|
||||
/// No V1-only fields yet — add them here as you learn what differs.
|
||||
/// No V1-only fields yet - add them here as you learn what differs.
|
||||
pub struct BambuV1Printer {
|
||||
generic: BambuGenericPrinter,
|
||||
}
|
||||
@@ -320,7 +320,7 @@ impl GenericPrinter for BambuV1Printer {
|
||||
}
|
||||
|
||||
/// Current-generation Bambu MQTT report schema (X1 Carbon/X1E, H2 series).
|
||||
/// No V2-only fields yet (e.g. AMS slot state) — add them here as you build
|
||||
/// No V2-only fields yet (e.g. AMS slot state) - add them here as you build
|
||||
/// out real report parsing.
|
||||
pub struct BambuV2Printer {
|
||||
generic: BambuGenericPrinter,
|
||||
|
||||
@@ -10,7 +10,7 @@ pub struct GetVersionRequest {
|
||||
pub struct GetVersionResponse {
|
||||
#[serde(flatten)]
|
||||
pub envelope: CommandEnvelope,
|
||||
/// Not always present — `Option<T>` handles that natively, no
|
||||
/// Not always present - `Option<T>` handles that natively, no
|
||||
/// `#[serde(default)]` needed (unlike the proto-generated version this
|
||||
/// replaced, which needed one explicit `field_attribute` call per
|
||||
/// field like this).
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
//! Bambu's raw MQTT command/report structs — plain hand-written Rust +
|
||||
//! Bambu's raw MQTT command/report structs - plain hand-written Rust +
|
||||
//! serde, no protobuf. This data only ever needs to be understood by this
|
||||
//! crate (raw vendor wire format, never crosses into TypeScript), and the
|
||||
//! actual wire format is JSON over MQTT, not binary protobuf — protobuf's
|
||||
//! actual wire format is JSON over MQTT, not binary protobuf - protobuf's
|
||||
//! codegen tooling kept fighting us for a payoff (cross-language codegen,
|
||||
//! binary wire efficiency) that doesn't apply here at all. Proto stays in
|
||||
//! continuum-schemas for continuum.v1, which is a schema we designed
|
||||
//! ourselves and that both TS and Rust genuinely need.
|
||||
//!
|
||||
//! One file per Bambu *category* (`get_version.rs` for "info", `pushall.rs`
|
||||
//! + `print.rs` for "pushing"/"print" — see below for why those are two
|
||||
//! + `print.rs` for "pushing"/"print" - see below for why those are two
|
||||
//! files, not one). Copy `get_version.rs`'s shape for your next command.
|
||||
//!
|
||||
//! Mirrors continuum-schemas' `schemas/bambulab/` JSON Schemas — same
|
||||
//! Mirrors continuum-schemas' `schemas/bambulab/` JSON Schemas - same
|
||||
//! category split, same "print vs pushing" correction (see that
|
||||
//! directory's README for the full explanation): a `pushall` *request*
|
||||
//! goes to the "pushing" key, but the ongoing state-push stream it
|
||||
//! triggers arrives under a different key, "print" — so `PushallRequest`
|
||||
//! triggers arrives under a different key, "print" - so `PushallRequest`
|
||||
//! lives in `pushall.rs` and the thing it triggers, `PrintReport`, gets
|
||||
//! its own file, `print.rs`, rather than living together as if one were
|
||||
//! simply "the response to" the other.
|
||||
@@ -29,10 +29,10 @@ pub use print::{AmsState, AmsTray, AmsUnit, EmptyTray, LoadedTray, PrintReport,
|
||||
pub use pushall::PushallRequest;
|
||||
|
||||
/// Fields every Bambu command/response shares. This is the "inheritance"
|
||||
/// replacement for these structs — composition, not extension: every
|
||||
/// replacement for these structs - composition, not extension: every
|
||||
/// command below *has* one of these, embedded via `#[serde(flatten)]`
|
||||
/// (which unwraps it onto the parent object at serialize/deserialize
|
||||
/// time, so the JSON stays flat — no extra nesting), rather than
|
||||
/// time, so the JSON stays flat - no extra nesting), rather than
|
||||
/// *extending* a base class. Exactly the same idea as `BambuGenericPrinter`
|
||||
/// in `../bambu.rs`, just expressed with a struct field instead of a
|
||||
/// struct-holding-a-struct in `impl` blocks.
|
||||
@@ -44,9 +44,9 @@ pub struct CommandEnvelope {
|
||||
|
||||
/// One request you can send to a Bambu printer, keyed by Bambu's top-level
|
||||
/// JSON category ("info", "pushing", ...). NOTE: that top-level key is a
|
||||
/// *category*, not the specific command — `CommandEnvelope::command` is
|
||||
/// *category*, not the specific command - `CommandEnvelope::command` is
|
||||
/// what actually says which one ("get_version", "pushall", ...). A
|
||||
/// category can have more than one possible request shape — if that ever
|
||||
/// category can have more than one possible request shape - if that ever
|
||||
/// happens here, that variant's payload becomes its own small
|
||||
/// `#[serde(tag = "command")]` enum instead of a single struct.
|
||||
#[derive(serde::Serialize, Debug)]
|
||||
@@ -56,7 +56,7 @@ pub enum BambuRequest {
|
||||
Pushing(PushallRequest),
|
||||
}
|
||||
|
||||
/// Same idea, the response direction — note "print", not "pushing" (see
|
||||
/// Same idea, the response direction - note "print", not "pushing" (see
|
||||
/// this module's doc comment).
|
||||
#[derive(serde::Deserialize, Debug)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! The ongoing print-state push (root key "print"). Doesn't carry a
|
||||
//! `CommandEnvelope` — no `sequence_id`/`command` seen on this one in the
|
||||
//! `CommandEnvelope` - no `sequence_id`/`command` seen on this one in the
|
||||
//! real capture, and it may genuinely not have them (an async push, not a
|
||||
//! reply to a specific request) — revisit if a real capture proves
|
||||
//! reply to a specific request) - revisit if a real capture proves
|
||||
//! otherwise.
|
||||
|
||||
#[derive(serde::Deserialize, Debug)]
|
||||
@@ -15,12 +15,12 @@ pub struct PrintReport {
|
||||
pub xcam: Option<XcamSettings>,
|
||||
pub xcam_status: Option<String>,
|
||||
// The real payload has a lot more fields than shown here (this
|
||||
// example was truncated) — add them the same way, one field per line,
|
||||
// example was truncated) - add them the same way, one field per line,
|
||||
// `Option<T>` for anything that isn't always present.
|
||||
}
|
||||
|
||||
/// Bambu's own naming has the outer container and the inner array both
|
||||
/// called `ams` — `print.ams.ams`, not a typo here, just what the printer
|
||||
/// called `ams` - `print.ams.ams`, not a typo here, just what the printer
|
||||
/// actually sends.
|
||||
#[derive(serde::Deserialize, Debug)]
|
||||
pub struct AmsState {
|
||||
@@ -37,13 +37,13 @@ pub struct AmsUnit {
|
||||
}
|
||||
|
||||
/// A tray slot is either empty (just `id`) or loaded (the full field set)
|
||||
/// — two genuinely different shapes, not one shape with optional fields,
|
||||
/// - two genuinely different shapes, not one shape with optional fields,
|
||||
/// so this is an untagged enum rather than a struct full of `Option<T>`.
|
||||
/// `#[serde(deny_unknown_fields)]` on `Empty` is load-bearing: untagged
|
||||
/// enums try variants in order, and without it a *loaded* tray (which has
|
||||
/// every field `Empty` allows, plus more) would incorrectly match `Empty`
|
||||
/// first, since a plain struct doesn't reject extra fields by default.
|
||||
/// Verified this the hard way — see git history.
|
||||
/// Verified this the hard way - see git history.
|
||||
#[derive(serde::Deserialize, Debug)]
|
||||
#[serde(untagged)]
|
||||
pub enum AmsTray {
|
||||
|
||||
@@ -8,7 +8,7 @@ pub struct PushallRequest {
|
||||
pub push_target: i32,
|
||||
}
|
||||
|
||||
// No PushallResponse here — sending this request doesn't get a direct
|
||||
// No PushallResponse here - sending this request doesn't get a direct
|
||||
// reply. It makes the printer start/refresh the ongoing print-state push,
|
||||
// which arrives under a different root key ("print", not "pushing") — see
|
||||
// which arrives under a different root key ("print", not "pushing") - see
|
||||
// print.rs for that.
|
||||
|
||||
@@ -17,7 +17,7 @@ impl GenericPrinter for KlipperPrinter {
|
||||
&self.base
|
||||
}
|
||||
|
||||
/// OVERRIDE: a third, again-different body — Moonraker's real feed is a
|
||||
/// OVERRIDE: a third, again-different body - Moonraker's real feed is a
|
||||
/// WebSocket (see `adapters::moonraker`). This also shows the `Err`
|
||||
/// path: an empty `host` fails immediately, same as any other vendor
|
||||
/// would if it couldn't reach its printer.
|
||||
@@ -29,7 +29,7 @@ impl GenericPrinter for KlipperPrinter {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// OVERRIDE: Moonraker's own **hypothetical** native API, not gcode injection — no
|
||||
/// OVERRIDE: Moonraker's own **hypothetical** native API, not gcode injection - no
|
||||
/// percent-to-PWM conversion needed since it already takes a
|
||||
/// percentage directly. Real version:
|
||||
/// `POST /printer/gcode/script?script=SET_FAN_SPEED SPEED=0.NN`.
|
||||
|
||||
+16
-16
@@ -1,24 +1,24 @@
|
||||
//! Printer hierarchy — the Rust answer to
|
||||
//! Printer hierarchy - the Rust answer to
|
||||
//! `GenericPrinter -> BambuV1/BambuV2/PrusaLink/PrusaSerial/Klipper`-style
|
||||
//! inheritance, since Rust structs can't `extend` each other.
|
||||
//!
|
||||
//! Bambu goes one level deeper than the others: `bambu.rs` has
|
||||
//! `BambuGenericPrinter` (fields + behavior every Bambu printer shares —
|
||||
//! `BambuGenericPrinter` (fields + behavior every Bambu printer shares -
|
||||
//! access code, CA trust, the TLS handshake test/fetch) which
|
||||
//! `BambuV1Printer`/`BambuV2Printer` each *have* one of, mirroring the
|
||||
//! `GenericPrinter -> BambuGenericPrinter -> BambuV1/V2` chain this project
|
||||
//! actually wanted from the start — see that file's doc comment.
|
||||
//! actually wanted from the start - see that file's doc comment.
|
||||
//!
|
||||
//! Three pieces, each doing one job an OOP base class would normally do:
|
||||
//!
|
||||
//! 1. `PrinterBase` — a plain struct holding the shared *fields*. Every
|
||||
//! 1. `PrinterBase` - a plain struct holding the shared *fields*. Every
|
||||
//! printer struct below *has* one of these (composition) instead of
|
||||
//! *extending* one.
|
||||
//! 2. `GenericPrinter` — a trait holding the shared *behavior*. `connect()`
|
||||
//! 2. `GenericPrinter` - a trait holding the shared *behavior*. `connect()`
|
||||
//! has no default body, so every printer type is forced to write its
|
||||
//! own — that's what "overriding" looks like when there's no inherited
|
||||
//! own - that's what "overriding" looks like when there's no inherited
|
||||
//! body to override in the first place.
|
||||
//! 3. `PrinterHandle` — an enum listing the closed set of printer kinds.
|
||||
//! 3. `PrinterHandle` - an enum listing the closed set of printer kinds.
|
||||
//! Stands in for "any subclass of GenericPrinter".
|
||||
//!
|
||||
//! Run `cargo run --example printer_polymorphism` to see it in action.
|
||||
@@ -41,7 +41,7 @@ pub use bambu_commands::{
|
||||
pub use klipper::KlipperPrinter;
|
||||
pub use prusa::{PrusaLinkPrinter, PrusaSerialPrinter};
|
||||
|
||||
/// A tiny error type: just a message. `String` would work too — this exists
|
||||
/// A tiny error type: just a message. `String` would work too - this exists
|
||||
/// mainly so `?` has something concrete to convert into everywhere.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[error("{0}")]
|
||||
@@ -55,7 +55,7 @@ pub struct PrinterBase {
|
||||
pub host: String,
|
||||
}
|
||||
|
||||
/// The shared interface — think `interface GenericPrinter` (Java/TS) or an
|
||||
/// The shared interface - think `interface GenericPrinter` (Java/TS) or an
|
||||
/// ABC (Python). Every vendor's struct implements this.
|
||||
pub trait GenericPrinter {
|
||||
/// Read-only access to the shared fields.
|
||||
@@ -64,23 +64,23 @@ pub trait GenericPrinter {
|
||||
/// REQUIRED, no default body: every vendor speaks a different protocol,
|
||||
/// so there's nothing sensible to share here. Each `impl` in
|
||||
/// `bambu.rs`/`prusa.rs`/`klipper.rs` provides a totally different
|
||||
/// body — that's the override.
|
||||
/// body - that's the override.
|
||||
///
|
||||
/// `cargo check` will warn about `async fn` in a public trait here.
|
||||
/// That's about a more advanced concern (whether the resulting work can
|
||||
/// safely move across OS threads) that only matters once this is wired
|
||||
/// into the multi-threaded daemon — safe to ignore for now.
|
||||
/// into the multi-threaded daemon - safe to ignore for now.
|
||||
async fn connect(&mut self) -> Result<(), PrinterError>;
|
||||
|
||||
/// REQUIRED, no default body: `percent` is 0-100, but how it actually
|
||||
/// gets sent — an MQTT gcode-injection command, a REST call, raw bytes
|
||||
/// on a serial port — is different for every vendor, same as
|
||||
/// gets sent - an MQTT gcode-injection command, a REST call, raw bytes
|
||||
/// on a serial port - is different for every vendor, same as
|
||||
/// `connect()`.
|
||||
async fn set_fan_speed(&mut self, percent: u8) -> Result<(), PrinterError>;
|
||||
|
||||
/// OPTIONAL: a default body every printer gets for free unless it
|
||||
/// writes its own. None of ours do below, so all three vendors share
|
||||
/// this exact implementation — the trait-method equivalent of
|
||||
/// this exact implementation - the trait-method equivalent of
|
||||
/// inheriting a base-class method unchanged.
|
||||
fn display_name(&self) -> String {
|
||||
format!("{} ({})", self.base().name, self.base().id)
|
||||
@@ -89,7 +89,7 @@ pub trait GenericPrinter {
|
||||
|
||||
/// M106's `S` parameter is a raw 0-255 PWM value, not a percentage. Shared
|
||||
/// by anything that ultimately sends gcode (Bambu, PrusaLink, PrusaSerial)
|
||||
/// — *not* a trait default, since Klipper's hypothetical native fan API
|
||||
/// - *not* a trait default, since Klipper's hypothetical native fan API
|
||||
/// below takes a percent directly and has no use for this at all. Private,
|
||||
/// but still visible to bambu.rs/prusa.rs: Rust's module privacy reaches
|
||||
/// into child modules, not just the exact file it's defined in.
|
||||
@@ -109,7 +109,7 @@ pub enum PrinterHandle {
|
||||
|
||||
// Hand-written delegation: each trait method just matches on the variant
|
||||
// and forwards to that variant's own implementation. This one block is the
|
||||
// only "boilerplate tax" for not having inheritance — everything else reads
|
||||
// only "boilerplate tax" for not having inheritance - everything else reads
|
||||
// like normal code.
|
||||
impl GenericPrinter for PrinterHandle {
|
||||
fn base(&self) -> &PrinterBase {
|
||||
|
||||
@@ -21,7 +21,7 @@ impl GenericPrinter for PrusaLinkPrinter {
|
||||
&self.base
|
||||
}
|
||||
|
||||
/// OVERRIDE: a completely different body from Bambu's — PrusaLink is a
|
||||
/// OVERRIDE: a completely different body from Bambu's - PrusaLink is a
|
||||
/// plain REST API (real version: `GET /api/v1/status` with the API
|
||||
/// key, see `adapters::prusalink`), no persistent session to hold.
|
||||
async fn connect(&mut self) -> Result<(), PrinterError> {
|
||||
@@ -29,7 +29,7 @@ impl GenericPrinter for PrusaLinkPrinter {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// OVERRIDE: gcode via PrusaLink's command-injection endpoint — same
|
||||
/// OVERRIDE: gcode via PrusaLink's command-injection endpoint - same
|
||||
/// underlying M106 as Bambu, reusing the shared percent->PWM
|
||||
/// conversion, but sent over HTTP instead of MQTT.
|
||||
async fn set_fan_speed(&mut self, percent: u8) -> Result<(), PrinterError> {
|
||||
@@ -61,7 +61,7 @@ impl GenericPrinter for PrusaSerialPrinter {
|
||||
&self.base
|
||||
}
|
||||
|
||||
/// OVERRIDE: a fourth, again-different body — no network host at all,
|
||||
/// OVERRIDE: a fourth, again-different body - no network host at all,
|
||||
/// just a local serial port (real version: open the port, e.g. via the
|
||||
/// `serialport`/`tokio-serial` crate, and speak Prusa's G-code-over-
|
||||
/// serial protocol).
|
||||
@@ -72,7 +72,7 @@ impl GenericPrinter for PrusaSerialPrinter {
|
||||
|
||||
/// OVERRIDE: same gcode as PrusaLink (still M106, still the shared
|
||||
/// conversion), but written straight to the serial port instead of
|
||||
/// sent over HTTP — a third transport for the same underlying command.
|
||||
/// sent over HTTP - a third transport for the same underlying command.
|
||||
async fn set_fan_speed(&mut self, percent: u8) -> Result<(), PrinterError> {
|
||||
let pwm = super::percent_to_pwm(percent);
|
||||
println!("[{}] writing to COM{}: M106 S{pwm}", self.base.name, self.com_port);
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
//!
|
||||
//! This is intentionally the simplest version of "a client that reconnects
|
||||
//! when it drops": no backoff curve, no channels routing messages in from
|
||||
//! other parts of the program yet — just connect, say hello, send a
|
||||
//! other parts of the program yet - just connect, say hello, send a
|
||||
//! heartbeat on a timer, and print whatever comes back. Once you're
|
||||
//! comfortable with this, growing it (real backoff, forwarding printer
|
||||
//! telemetry, dispatching cloud-issued commands) is additive, not a rewrite.
|
||||
|
||||
Reference in New Issue
Block a user