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" name = "continuum-proxy"
version = "0.1.0" version = "0.1.0"
edition = "2024" 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" license = "UNLICENSED"
[lib] [lib]
@@ -50,7 +50,7 @@ dotenvy = "0.15"
# Real printer protocol clients (MQTT for Bambu, HTTP for PrusaLink/ # Real printer protocol clients (MQTT for Bambu, HTTP for PrusaLink/
# Moonraker, FTPS for gcode transfer) go here once you're ready to build # 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" } continuum-rust-types = { path = "../continuum-schemas/packages/rust-types" }
# FOR NON-LOCAL DEV # FOR NON-LOCAL DEV
# continuum-rust-types = { git = "https://git.octoturge.com/Continuum/continuum-schemas.git" } # 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 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** (Bambu MQTT+FTPS, PrusaLink REST, Klipper/Moonraker WebSocket) are **not**
implemented yet. Each vendor is a stub that just prints what it would do 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 (`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 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 module's doc comment for the trait+enum pattern this project uses instead
of class inheritance, and run `printer_polymorphism` to see it work. of class inheritance, and run `printer_polymorphism` to see it work.
`printers.toml` (gitignored copy from `printers.example.toml`) holds real `printers.toml` (gitignored - copy from `printers.example.toml`) holds real
per-printer connection details: host, access code, and since not every per-printer connection details: host, access code, and - since not every
Bambu printer trusts the same certificate (see `certs/README.md`) an Bambu printer trusts the same certificate (see `certs/README.md`) - an
optional per-printer CA override. `src/fleet.rs` loads it; nothing in 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 just the TLS handshake, no MQTT) way to check a printer's certificate
without needing the full MQTT client built yet. without needing the full MQTT client built yet.
## What's not here yet (on purpose) ## 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. `println!` where each of these will go.
- Local SQLite buffering for telemetry across connectivity gaps. - Local SQLite buffering for telemetry across connectivity gaps.
- LAN printer discovery (SSDP/mDNS). - LAN printer discovery (SSDP/mDNS).
- The mechanical plate-changer interface (serial/GPIO). - The mechanical plate-changer interface (serial/GPIO).
Add these back in one at a time as you get comfortable with the Rust 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 API, error handling for a real protocol) rather than something to absorb
all at once. 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 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 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. instead of fetching/pinning a certificate per printer.
- Source: https://github.com/bambulab/BambuStudio/blob/master/resources/cert/printer.cer - Source: https://github.com/bambulab/BambuStudio/blob/master/resources/cert/printer.cer
- Fetched: 2026-08-28 - 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` - 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`), - 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. 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 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 `resources/cert/` only has this one file (plus an unrelated
`*.bambulab.com` leaf cert for their cloud API), so there's no second `*.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 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 certificate, the same "download it from the printer" case as the older
units. `CONTINUUM_BAMBU_CA_CERT_OVERRIDES` (see `.env.example`) is a 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 override, because different printers can legitimately need different
certificates at the same time. certificates at the same time.
+1 -1
View File
@@ -4,7 +4,7 @@
//! fetch_bambu_cert combined: for each Bambu printer in printers.toml, //! fetch_bambu_cert combined: for each Bambu printer in printers.toml,
//! makes sure it has a working certificate, auto-pinning one via //! 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 //! 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 //! number>.pem) just gets re-verified against its pin, no network trust
//! decision is made again. //! decision is made again.
+2 -2
View File
@@ -1,7 +1,7 @@
//! Run with: cargo run --example bambu_commands_demo //! Run with: cargo run --example bambu_commands_demo
//! //!
//! Round-trips both commands against the exact JSON from Bambu's real //! 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 //! matches byte for byte, same as the proto-based version did, with none
//! of the build.rs ceremony. //! of the build.rs ceremony.
@@ -34,7 +34,7 @@ fn main() {
println!("matches Bambu's shape: {}\n", actual == expected); println!("matches Bambu's shape: {}\n", actual == expected);
// --- print report: arrives under "print", NOT "pushing" (pushall // --- 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 // full ams/xcam example, with one empty tray and one loaded tray, to
// exercise the untagged Empty/Loaded split. // exercise the untagged Empty/Loaded split.
let real_print_report = r#"{ let real_print_report = r#"{
+2 -2
View File
@@ -3,11 +3,11 @@
//! "Trust on first connect": connects to one printer from printers.toml //! "Trust on first connect": connects to one printer from printers.toml
//! with certificate verification disabled, captures whatever certificate //! with certificate verification disabled, captures whatever certificate
//! it presents, and saves it as PEM. Meant for a printer like P1P that //! 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 //! printer's `ca_cert_path` in printers.toml at the saved file and
//! `test_bambu_certs` should report it as verified from then on. //! `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. //! a network you trust, ideally right after unboxing the printer.
use continuum_proxy::fleet; use continuum_proxy::fleet;
+5 -5
View File
@@ -4,7 +4,7 @@
//! - `connect()` is called the same way regardless of vendor (polymorphism), //! - `connect()` is called the same way regardless of vendor (polymorphism),
//! but each variant's `impl GenericPrinter` runs completely different //! but each variant's `impl GenericPrinter` runs completely different
//! code (override). //! 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 //! 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. //! 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::PrusaLink(PrusaLinkPrinter::new("p3", "MK4", "192.168.1.51", "prusa-api-key")),
PrinterHandle::PrusaSerial(PrusaSerialPrinter::new("p4", "MK3S+", 0)), PrinterHandle::PrusaSerial(PrusaSerialPrinter::new("p4", "MK3S+", 0)),
PrinterHandle::Klipper(KlipperPrinter::new("p5", "Voron 2.4", "192.168.1.52")), 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", "")), PrinterHandle::Klipper(KlipperPrinter::new("p6", "Broken Voron", "")),
]; ];
@@ -52,7 +52,7 @@ async fn main() {
// Vendor-only extension: reachable only after matching the concrete // Vendor-only extension: reachable only after matching the concrete
// variant back out of the enum. BambuV1Printer and BambuV2Printer are // variant back out of the enum. BambuV1Printer and BambuV2Printer are
// separate types now (each *has* a BambuGenericPrinter rather than // 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 // 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. // every alternative to bind the same type, and these no longer do.
for printer in &fleet { for printer in &fleet {
@@ -64,9 +64,9 @@ async fn main() {
} }
// A second trait method, same uniform call site as connect(): every // 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 // 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!(); println!();
for printer in &mut fleet { for printer in &mut fleet {
let _ = printer.set_fan_speed(50).await; 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 //! Loads printers.toml (copy printers.example.toml to get started, fill in
//! your real printers) and attempts a raw TLS handshake to each Bambu //! 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 //! certificate verify". Prusa/Klipper entries are skipped; they don't have
//! this trust question. //! this trust question.
@@ -28,7 +28,7 @@ fn main() -> anyhow::Result<()> {
fn report(name: &str, result: anyhow::Result<()>) { fn report(name: &str, result: anyhow::Result<()>) {
match result { match result {
Ok(()) => println!("{name}: OK certificate verified"), Ok(()) => println!("{name}: OK - certificate verified"),
Err(err) => println!("{name}: FAILED {err}"), 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" host = "192.168.1.50"
access_code = "REPLACE_WITH_ACCESS_CODE" access_code = "REPLACE_WITH_ACCESS_CODE"
sn = "REPLACE_WITH_SERIAL_NUMBER" # printer screen > Settings > Device (used for MQTT topics later) 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. # 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 # `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" ca_cert_path = "./certs/p1p.pem"
[[printer]] [[printer]]
@@ -22,7 +22,7 @@ vendor = "bambu_v2"
host = "192.168.1.51" host = "192.168.1.51"
access_code = "REPLACE_WITH_ACCESS_CODE" access_code = "REPLACE_WITH_ACCESS_CODE"
sn = "REPLACE_WITH_SERIAL_NUMBER" 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. # CA (certs/bambu_ca2.pem) fine.
# [[printer]] # [[printer]]
+2 -2
View File
@@ -13,7 +13,7 @@ pub struct Config {
/// Per-printer CA certificate overrides, keyed by printer ID. Most /// Per-printer CA certificate overrides, keyed by printer ID. Most
/// Bambu printers chain to the bundled shared CA (`certs/bambu_ca2.pem`) /// 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 /// 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 /// 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. /// (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 /// 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> { fn parse_cert_overrides(raw: &str) -> HashMap<String, PathBuf> {
raw.split(',') raw.split(',')
.filter_map(|entry| entry.split_once('=')) .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` //! `printers.example.toml` for the shape. The actual file (`printers.toml`
//! by default) is gitignored: it holds real hostnames and access codes for //! by default) is gitignored: it holds real hostnames and access codes for
//! your printers, which don't belong in version control. //! your printers, which don't belong in version control.
@@ -23,13 +23,13 @@ struct PrinterEntry {
vendor: String, vendor: String,
host: Option<String>, host: Option<String>,
access_code: 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`. /// by serial (`device/{sn}/report`), not by our own `id`.
sn: Option<String>, sn: Option<String>,
api_key: Option<String>, api_key: Option<String>,
com_port: Option<i16>, com_port: Option<i16>,
/// Path to a printer-specific CA certificate. Leave unset to use the /// 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). /// it (P1P, at least; see certs/README.md).
ca_cert_path: Option<String>, ca_cert_path: Option<String>,
#[serde(default = "default_true")] #[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"); 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 // Two things run at once for now: the cloud uplink, and the go2rtc
// camera-restreaming process. More will join them later printer // camera-restreaming process. More will join them later - printer
// discovery, the plate changer as you build past src/printer/'s // discovery, the plate changer - as you build past src/printer/'s
// stubs. `tokio::select!` runs both and stops as soon as either one // stubs. `tokio::select!` runs both and stops as soon as either one
// returns; both loop forever below, so in practice this runs until the // returns; both loop forever below, so in practice this runs until the
// process is killed. // process is killed.
+25 -25
View File
@@ -4,19 +4,19 @@ use std::time::Duration;
use super::{GenericPrinter, PrinterBase, PrinterError}; 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 /// 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 compiled binary at build time, so there's no file to ship alongside
/// the executable. /// the executable.
const BUNDLED_CA: &[u8] = include_bytes!("../../certs/bambu_ca2.pem"); const BUNDLED_CA: &[u8] = include_bytes!("../../certs/bambu_ca2.pem");
/// Which certificate to trust when connecting to a Bambu printer's MQTTS /// 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). /// fallback).
pub enum BambuTls { pub enum BambuTls {
/// Bambu's shared root CA. Correct for current-generation printers. /// Bambu's shared root CA. Correct for current-generation printers.
BundledCa, 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). /// doesn't chain to the shared CA (P1P, at least).
Custom(Vec<u8>), Custom(Vec<u8>),
/// Skip certificate verification entirely. The least safe option, only /// Skip certificate verification entirely. The least safe option, only
@@ -26,7 +26,7 @@ pub enum BambuTls {
impl BambuTls { impl BambuTls {
/// Resolves the trust mode for *one* printer. Call this per printer, /// 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 /// no single override for the whole fleet, because not every Bambu
/// model chains to the same CA. `require_valid_cert = false` always /// model chains to the same CA. `require_valid_cert = false` always
/// means `Insecure`, regardless of whether that printer also has an /// 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. /// `Insecure`, since there's nothing to verify against.
pub fn ca_bytes(&self) -> &[u8] { pub fn ca_bytes(&self) -> &[u8] {
match self { match self {
@@ -57,12 +57,12 @@ impl BambuTls {
let mut builder = native_tls::TlsConnector::builder(); let mut builder = native_tls::TlsConnector::builder();
// Bambu printer certificates don't carry their LAN IP as a Subject // 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 // manufacture time wouldn't make sense. Hostname verification has
// to be off regardless of trust mode: what matters is whether the // to be off regardless of trust mode: what matters is whether the
// certificate chains to a CA we trust, not whether its SAN matches // certificate chains to a CA we trust, not whether its SAN matches
// the specific IP we happened to dial today. Chain verification // 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. // narrows *what* gets checked, it doesn't turn checking off.
builder.danger_accept_invalid_hostnames(true); 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 /// 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. /// format as the bundled one.
fn der_to_pem(der: &[u8]) -> Vec<u8> { fn der_to_pem(der: &[u8]) -> Vec<u8> {
use base64::Engine; 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 /// 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 /// `BambuV1Printer`/`BambuV2Printer` leaves below. Those two *have* one of
/// these (composition) rather than a deeper inheritance chain; because /// these (composition) rather than a deeper inheritance chain; because
/// Rust's field/method privacy is scoped to the *module* (this whole file), /// Rust's field/method privacy is scoped to the *module* (this whole file),
@@ -131,22 +131,22 @@ impl BambuGenericPrinter {
&self.base &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. /// equivalent, since FTPS gcode transfer is a Bambu-specific thing.
pub fn dispatch_gcode(&self, file_name: &str) { pub fn dispatch_gcode(&self, file_name: &str) {
println!("[{}] uploading {file_name} over FTPS", self.base.name); 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 /// protocol, just "does the certificate verify" against this printer's
/// configured trust mode. /// configured trust mode.
/// ///
/// `BundledCa` verifies normally (Bambu's shared CA is a proper, /// `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*, /// fine). `Custom` does something different: *fingerprint pinning*,
/// not chain validation. A device's own leaf certificate often isn't a /// 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 /// 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 /// OpenSSL's normal path-building to accept an arbitrary leaf as a
/// trust anchor is unreliable for exactly that reason (this is a real /// trust anchor is unreliable for exactly that reason (this is a real
/// bug that shipped and failed against a real P1P: `add_root_certificate` /// 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 /// certificate chain` / `unable to get local issuer certificate` on
/// the very next connection). So instead: connect with verification /// the very next connection). So instead: connect with verification
/// off, then compare the certificate actually presented against the /// 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." /// no PKI judgment call, just "is this the same certificate as before."
pub fn test_tls_handshake(&self) -> anyhow::Result<()> { pub fn test_tls_handshake(&self) -> anyhow::Result<()> {
let addr = resolve_addr(&self.base.host)?; let addr = resolve_addr(&self.base.host)?;
@@ -169,7 +169,7 @@ impl BambuGenericPrinter {
if presented != pem_to_der(pinned_pem)? { if presented != pem_to_der(pinned_pem)? {
anyhow::bail!( 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 \ certificate rotation, could be something worse; re-run fetch_bambu_cert deliberately \
if you're sure it's the former", if you're sure it's the former",
self.base.name self.base.name
@@ -187,7 +187,7 @@ impl BambuGenericPrinter {
/// presents, and returns it as PEM bytes. /// presents, and returns it as PEM bytes.
/// ///
/// This is meaningfully weaker than verifying against a CA you already /// 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 /// trusted forever after. That's why it's a method you call once, by
/// hand (see `examples/fetch_bambu_cert.rs`), rather than something /// hand (see `examples/fetch_bambu_cert.rs`), rather than something
/// `connect()` falls back to silently. Save the result to a file and /// `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, /// 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 /// the "smooth, no manual steps" version of the fetch_bambu_cert
/// workflow, built the way SSH handles host keys: /// workflow, built the way SSH handles host keys:
/// ///
/// - A pinned certificate already on disk (`cert_dir/<sn>.pem`) is used /// - 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 /// already happened once, this just re-verifies against it. Keyed by
/// *serial number*, not `id`: `id` is just an arbitrary label you /// *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 /// should still find it, because a pin certifies "this physical
/// printer", and the serial number is the one thing about it that /// printer", and the serial number is the one thing about it that
/// can't change. /// can't change.
@@ -224,10 +224,10 @@ impl BambuGenericPrinter {
/// produces a working connection, and saves it to `cert_dir/<sn>.pem` /// 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 /// for every run after this one. This is the one moment a MITM
/// active on your network *right now* could plant a certificate that /// 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. /// first connection.
/// - A printer with an *explicit* pin (you set `ca_cert_path` yourself /// - 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, /// failure there is a real error, not something to paper over,
/// since that's exactly the signal pinning exists to give you (the /// since that's exactly the signal pinning exists to give you (the
/// cert rotated, or something worse). /// 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 /// gcode-injection mechanism regardless of protocol generation. Real
/// version would publish /// version would publish
/// `{"print":{"command":"gcode_line","param":"M106 P1 S<pwm>"}}` to /// `{"print":{"command":"gcode_line","param":"M106 P1 S<pwm>"}}` to
@@ -263,7 +263,7 @@ impl BambuGenericPrinter {
Ok(()) 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 /// version would open the MQTTS session here. Still a stub, but no
/// longer duplicated across two structs. /// longer duplicated across two structs.
async fn connect_impl(&mut self) -> Result<(), PrinterError> { async fn connect_impl(&mut self) -> Result<(), PrinterError> {
@@ -278,7 +278,7 @@ impl BambuGenericPrinter {
} }
/// Older Bambu MQTT report schema (P1P, P1S, original X1 firmware). /// 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 { pub struct BambuV1Printer {
generic: BambuGenericPrinter, generic: BambuGenericPrinter,
} }
@@ -320,7 +320,7 @@ impl GenericPrinter for BambuV1Printer {
} }
/// Current-generation Bambu MQTT report schema (X1 Carbon/X1E, H2 series). /// 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. /// out real report parsing.
pub struct BambuV2Printer { pub struct BambuV2Printer {
generic: BambuGenericPrinter, generic: BambuGenericPrinter,
+1 -1
View File
@@ -10,7 +10,7 @@ pub struct GetVersionRequest {
pub struct GetVersionResponse { pub struct GetVersionResponse {
#[serde(flatten)] #[serde(flatten)]
pub envelope: CommandEnvelope, 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 /// `#[serde(default)]` needed (unlike the proto-generated version this
/// replaced, which needed one explicit `field_attribute` call per /// replaced, which needed one explicit `field_attribute` call per
/// field like this). /// 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 //! serde, no protobuf. This data only ever needs to be understood by this
//! crate (raw vendor wire format, never crosses into TypeScript), and the //! 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, //! codegen tooling kept fighting us for a payoff (cross-language codegen,
//! binary wire efficiency) that doesn't apply here at all. Proto stays in //! binary wire efficiency) that doesn't apply here at all. Proto stays in
//! continuum-schemas for continuum.v1, which is a schema we designed //! continuum-schemas for continuum.v1, which is a schema we designed
//! ourselves and that both TS and Rust genuinely need. //! ourselves and that both TS and Rust genuinely need.
//! //!
//! One file per Bambu *category* (`get_version.rs` for "info", `pushall.rs` //! 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. //! 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 //! category split, same "print vs pushing" correction (see that
//! directory's README for the full explanation): a `pushall` *request* //! directory's README for the full explanation): a `pushall` *request*
//! goes to the "pushing" key, but the ongoing state-push stream it //! 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 //! lives in `pushall.rs` and the thing it triggers, `PrintReport`, gets
//! its own file, `print.rs`, rather than living together as if one were //! its own file, `print.rs`, rather than living together as if one were
//! simply "the response to" the other. //! simply "the response to" the other.
@@ -29,10 +29,10 @@ pub use print::{AmsState, AmsTray, AmsUnit, EmptyTray, LoadedTray, PrintReport,
pub use pushall::PushallRequest; pub use pushall::PushallRequest;
/// Fields every Bambu command/response shares. This is the "inheritance" /// 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)]` /// command below *has* one of these, embedded via `#[serde(flatten)]`
/// (which unwraps it onto the parent object at serialize/deserialize /// (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` /// *extending* a base class. Exactly the same idea as `BambuGenericPrinter`
/// in `../bambu.rs`, just expressed with a struct field instead of a /// in `../bambu.rs`, just expressed with a struct field instead of a
/// struct-holding-a-struct in `impl` blocks. /// 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 /// 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 /// 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 /// 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 /// happens here, that variant's payload becomes its own small
/// `#[serde(tag = "command")]` enum instead of a single struct. /// `#[serde(tag = "command")]` enum instead of a single struct.
#[derive(serde::Serialize, Debug)] #[derive(serde::Serialize, Debug)]
@@ -56,7 +56,7 @@ pub enum BambuRequest {
Pushing(PushallRequest), 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). /// this module's doc comment).
#[derive(serde::Deserialize, Debug)] #[derive(serde::Deserialize, Debug)]
#[serde(rename_all = "snake_case")] #[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 //! 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 //! 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. //! otherwise.
#[derive(serde::Deserialize, Debug)] #[derive(serde::Deserialize, Debug)]
@@ -15,12 +15,12 @@ pub struct PrintReport {
pub xcam: Option<XcamSettings>, pub xcam: Option<XcamSettings>,
pub xcam_status: Option<String>, pub xcam_status: Option<String>,
// The real payload has a lot more fields than shown here (this // 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. // `Option<T>` for anything that isn't always present.
} }
/// Bambu's own naming has the outer container and the inner array both /// 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. /// actually sends.
#[derive(serde::Deserialize, Debug)] #[derive(serde::Deserialize, Debug)]
pub struct AmsState { pub struct AmsState {
@@ -37,13 +37,13 @@ pub struct AmsUnit {
} }
/// A tray slot is either empty (just `id`) or loaded (the full field set) /// 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>`. /// so this is an untagged enum rather than a struct full of `Option<T>`.
/// `#[serde(deny_unknown_fields)]` on `Empty` is load-bearing: untagged /// `#[serde(deny_unknown_fields)]` on `Empty` is load-bearing: untagged
/// enums try variants in order, and without it a *loaded* tray (which has /// enums try variants in order, and without it a *loaded* tray (which has
/// every field `Empty` allows, plus more) would incorrectly match `Empty` /// every field `Empty` allows, plus more) would incorrectly match `Empty`
/// first, since a plain struct doesn't reject extra fields by default. /// 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)] #[derive(serde::Deserialize, Debug)]
#[serde(untagged)] #[serde(untagged)]
pub enum AmsTray { pub enum AmsTray {
+2 -2
View File
@@ -8,7 +8,7 @@ pub struct PushallRequest {
pub push_target: i32, 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, // 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. // print.rs for that.
+2 -2
View File
@@ -17,7 +17,7 @@ impl GenericPrinter for KlipperPrinter {
&self.base &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` /// WebSocket (see `adapters::moonraker`). This also shows the `Err`
/// path: an empty `host` fails immediately, same as any other vendor /// path: an empty `host` fails immediately, same as any other vendor
/// would if it couldn't reach its printer. /// would if it couldn't reach its printer.
@@ -29,7 +29,7 @@ impl GenericPrinter for KlipperPrinter {
Ok(()) 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 /// percent-to-PWM conversion needed since it already takes a
/// percentage directly. Real version: /// percentage directly. Real version:
/// `POST /printer/gcode/script?script=SET_FAN_SPEED SPEED=0.NN`. /// `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 //! `GenericPrinter -> BambuV1/BambuV2/PrusaLink/PrusaSerial/Klipper`-style
//! inheritance, since Rust structs can't `extend` each other. //! inheritance, since Rust structs can't `extend` each other.
//! //!
//! Bambu goes one level deeper than the others: `bambu.rs` has //! 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 //! access code, CA trust, the TLS handshake test/fetch) which
//! `BambuV1Printer`/`BambuV2Printer` each *have* one of, mirroring the //! `BambuV1Printer`/`BambuV2Printer` each *have* one of, mirroring the
//! `GenericPrinter -> BambuGenericPrinter -> BambuV1/V2` chain this project //! `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: //! 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 //! printer struct below *has* one of these (composition) instead of
//! *extending* one. //! *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 //! 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. //! 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". //! Stands in for "any subclass of GenericPrinter".
//! //!
//! Run `cargo run --example printer_polymorphism` to see it in action. //! 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 klipper::KlipperPrinter;
pub use prusa::{PrusaLinkPrinter, PrusaSerialPrinter}; 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. /// mainly so `?` has something concrete to convert into everywhere.
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
#[error("{0}")] #[error("{0}")]
@@ -55,7 +55,7 @@ pub struct PrinterBase {
pub host: String, 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. /// ABC (Python). Every vendor's struct implements this.
pub trait GenericPrinter { pub trait GenericPrinter {
/// Read-only access to the shared fields. /// Read-only access to the shared fields.
@@ -64,23 +64,23 @@ pub trait GenericPrinter {
/// REQUIRED, no default body: every vendor speaks a different protocol, /// REQUIRED, no default body: every vendor speaks a different protocol,
/// so there's nothing sensible to share here. Each `impl` in /// so there's nothing sensible to share here. Each `impl` in
/// `bambu.rs`/`prusa.rs`/`klipper.rs` provides a totally different /// `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. /// `cargo check` will warn about `async fn` in a public trait here.
/// That's about a more advanced concern (whether the resulting work can /// That's about a more advanced concern (whether the resulting work can
/// safely move across OS threads) that only matters once this is wired /// 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>; async fn connect(&mut self) -> Result<(), PrinterError>;
/// REQUIRED, no default body: `percent` is 0-100, but how it actually /// REQUIRED, no default body: `percent` is 0-100, but how it actually
/// gets sent an MQTT gcode-injection command, a REST call, raw bytes /// gets sent - an MQTT gcode-injection command, a REST call, raw bytes
/// on a serial port is different for every vendor, same as /// on a serial port - is different for every vendor, same as
/// `connect()`. /// `connect()`.
async fn set_fan_speed(&mut self, percent: u8) -> Result<(), PrinterError>; async fn set_fan_speed(&mut self, percent: u8) -> Result<(), PrinterError>;
/// OPTIONAL: a default body every printer gets for free unless it /// OPTIONAL: a default body every printer gets for free unless it
/// writes its own. None of ours do below, so all three vendors share /// 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. /// inheriting a base-class method unchanged.
fn display_name(&self) -> String { fn display_name(&self) -> String {
format!("{} ({})", self.base().name, self.base().id) 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 /// M106's `S` parameter is a raw 0-255 PWM value, not a percentage. Shared
/// by anything that ultimately sends gcode (Bambu, PrusaLink, PrusaSerial) /// 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, /// 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 /// 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. /// 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 // Hand-written delegation: each trait method just matches on the variant
// and forwards to that variant's own implementation. This one block is the // 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. // like normal code.
impl GenericPrinter for PrinterHandle { impl GenericPrinter for PrinterHandle {
fn base(&self) -> &PrinterBase { fn base(&self) -> &PrinterBase {
+4 -4
View File
@@ -21,7 +21,7 @@ impl GenericPrinter for PrusaLinkPrinter {
&self.base &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 /// plain REST API (real version: `GET /api/v1/status` with the API
/// key, see `adapters::prusalink`), no persistent session to hold. /// key, see `adapters::prusalink`), no persistent session to hold.
async fn connect(&mut self) -> Result<(), PrinterError> { async fn connect(&mut self) -> Result<(), PrinterError> {
@@ -29,7 +29,7 @@ impl GenericPrinter for PrusaLinkPrinter {
Ok(()) 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 /// underlying M106 as Bambu, reusing the shared percent->PWM
/// conversion, but sent over HTTP instead of MQTT. /// conversion, but sent over HTTP instead of MQTT.
async fn set_fan_speed(&mut self, percent: u8) -> Result<(), PrinterError> { async fn set_fan_speed(&mut self, percent: u8) -> Result<(), PrinterError> {
@@ -61,7 +61,7 @@ impl GenericPrinter for PrusaSerialPrinter {
&self.base &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 /// 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- /// `serialport`/`tokio-serial` crate, and speak Prusa's G-code-over-
/// serial protocol). /// serial protocol).
@@ -72,7 +72,7 @@ impl GenericPrinter for PrusaSerialPrinter {
/// OVERRIDE: same gcode as PrusaLink (still M106, still the shared /// OVERRIDE: same gcode as PrusaLink (still M106, still the shared
/// conversion), but written straight to the serial port instead of /// 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> { async fn set_fan_speed(&mut self, percent: u8) -> Result<(), PrinterError> {
let pwm = super::percent_to_pwm(percent); let pwm = super::percent_to_pwm(percent);
println!("[{}] writing to COM{}: M106 S{pwm}", self.base.name, self.com_port); 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 //! This is intentionally the simplest version of "a client that reconnects
//! when it drops": no backoff curve, no channels routing messages in from //! 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 //! heartbeat on a timer, and print whatever comes back. Once you're
//! comfortable with this, growing it (real backoff, forwarding printer //! comfortable with this, growing it (real backoff, forwarding printer
//! telemetry, dispatching cloud-issued commands) is additive, not a rewrite. //! telemetry, dispatching cloud-issued commands) is additive, not a rewrite.