Replace em dashes with hyphens across docs and comments
This commit is contained in:
+2
-2
@@ -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" }
|
||||||
|
|||||||
@@ -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
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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#"{
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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,
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|||||||
@@ -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")]
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
@@ -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 {
|
||||||
|
|||||||
@@ -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
@@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user