Replace em dashes with hyphens across docs and comments

This commit is contained in:
2026-08-29 08:13:54 +00:00
parent 1583f531fa
commit a2d4e578ec
21 changed files with 104 additions and 104 deletions
+2 -2
View File
@@ -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" }
+8 -8
View File
@@ -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
View File
@@ -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.
+1 -1
View File
@@ -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.
+2 -2
View File
@@ -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#"{
+2 -2
View File
@@ -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;
+5 -5
View File
@@ -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;
+3 -3
View File
@@ -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}"),
}
}
+3 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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,
+1 -1
View File
@@ -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).
+10 -10
View File
@@ -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")]
+6 -6
View File
@@ -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 {
+2 -2
View File
@@ -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.
+2 -2
View File
@@ -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
View File
@@ -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 {
+4 -4
View File
@@ -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
View File
@@ -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.