Reporting a detected printer as `prusa_link` asserted a product nothing on
the wire mentions. What a probe observes is that something speaks the
OctoPrint REST API; PrusaLink is one implementation of it, OctoPrint itself
is another, and without a credential the two are indistinguishable - every
PrusaLink identity endpoint requires digest auth.
So discovery now reports `vendor = "octoprint"`, the protocol it actually
saw, and carries `ambiguous_with` so the rendered entry prints a comment
offering `prusa_link` directly above the `vendor` line. Changing it is a
one-word edit in the file the operator is already pasting into, which is the
right place for a question only they can answer. The exception is a body
that names itself: `/api/version`'s `text` field reads "PrusaLink 0.7.0" or
"OctoPrint 1.9.3", so on the rare unauthenticated 200 the answer is known
and no choice is offered.
For that to mean anything, `octoprint` had to become a vendor `fleet.rs`
accepts - otherwise discovery printed a block that would not load. It maps
to the same client as `prusa_link`, because they speak the same API, with an
`OctoPrintFlavour` recording which. The flavour is stored now, while the
client is still a stub, precisely because no probe can settle it: it is the
operator's answer, and without somewhere to keep it the real client would
have to ask them a second time. `PrusaLinkPrinter` is accordingly now
`OctoPrintPrinter` - one client for the API family - since a struct called
PrusaLink holding a flavour that says "Generic" is a contradiction someone
would later have to unpick.
**Found by the new test, and the reason it exists:** `fleet::load` did *not*
reject an entry whose credential was blank. `require_field` took an
`Option`, so `access_code = ""` arrived as `Some("")` and loaded happily -
which falsified the claim this feature was built on, that printing a block
with an empty credential is safe because pasting it in unfinished fails
loudly at startup. It does now. An empty credential is not a usable one; it
only moves the failure somewhere worse, where a Bambu printer is refused at
MQTT and a PrusaLink one 401s on every poll with nothing saying the field is
blank. This is the same trap as `??`-instead-of-`||` on an environment
variable that ships empty, which the backend's `auth/edge_secret.ts` and
`s3/index.ts` both document; it bites identically on a TOML key.
tests/discovery_to_fleet.rs is new and closes the loop: discovery and fleet
are coupled by nothing but a string, unit tests on either side cannot catch
a drift between them, and the symptom of a drift is an operator pasting in
exactly what they were told to and the gateway refusing to start. It renders
a candidate for every vendor, fills in the credential the way a human would,
loads it for real, and checks the handle matches the vendor asked for - not
merely that something loaded, since `octoprint` and `prusa_link` share a
client and a mapping that collapsed them would still load and still be
wrong.
Verification: the Moonraker probe now runs end to end over real HTTP against
a stand-in serving Moonraker's documented `/printer/info`, and correctly
reports a klipper entry named from the returned hostname. Port 80 cannot be
bound without root here, so the PrusaLink path is covered by a test that
takes a real socket answering a real digest challenge - the one link unit
tests on the classifier cannot reach, and the one PrusaLink is identified by.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
84 lines
3.4 KiB
Rust
84 lines
3.4 KiB
Rust
//! Run with: cargo run --example printer_polymorphism
|
|
//!
|
|
//! Demonstrates the trait+enum pattern that stands in for inheritance:
|
|
//! - `connect()` is called the same way regardless of vendor (polymorphism),
|
|
//! but each variant's `impl GenericPrinter` runs completely different
|
|
//! code (override).
|
|
//! - `dispatch_gcode()` only exists on the Bambu struct (extension) - you
|
|
//! have to `match` the enum back down to the concrete type to reach it,
|
|
//! which is the trade-off for not having implicit downcasting.
|
|
|
|
use continuum_proxy::printer::{
|
|
BambuPrinter, BambuProtocolPreference, BambuTls, GenericPrinter, KlipperPrinter, PrinterHandle,
|
|
OctoPrintFlavour, OctoPrintPrinter, PrusaSerialPrinter,
|
|
};
|
|
|
|
#[tokio::main]
|
|
async fn main() {
|
|
let mut fleet: Vec<PrinterHandle> = vec![
|
|
// Forced: a known-old printer.
|
|
PrinterHandle::Bambu(BambuPrinter::new(
|
|
"p1",
|
|
"P1S",
|
|
"192.168.1.49",
|
|
"87654321",
|
|
"01P00A000000000",
|
|
BambuTls::BundledCa,
|
|
BambuProtocolPreference::ForceV1,
|
|
)),
|
|
// Auto: the recommended default - see bambu.rs's doc comment.
|
|
PrinterHandle::Bambu(BambuPrinter::new(
|
|
"p2",
|
|
"X1 Carbon",
|
|
"192.168.1.50",
|
|
"12345678",
|
|
"00M00A000000000",
|
|
BambuTls::BundledCa,
|
|
BambuProtocolPreference::Auto,
|
|
)),
|
|
PrinterHandle::OctoPrint(OctoPrintPrinter::new(
|
|
"p3",
|
|
"MK4",
|
|
"192.168.1.51",
|
|
"prusa-api-key",
|
|
OctoPrintFlavour::PrusaLink,
|
|
)),
|
|
PrinterHandle::PrusaSerial(PrusaSerialPrinter::new("p4", "MK3S+", "/dev/ttyACM0", 115_200)),
|
|
PrinterHandle::Klipper(KlipperPrinter::new("p5", "Voron 2.4", "192.168.1.52")),
|
|
// No host configured - this one will hit the Err path.
|
|
PrinterHandle::Klipper(KlipperPrinter::new("p6", "Broken Voron", "")),
|
|
];
|
|
|
|
// Uniform call site: every printer connects the same way from the
|
|
// caller's point of view, even though each variant's `connect()` body
|
|
// is an unrelated implementation.
|
|
for printer in &mut fleet {
|
|
match printer.connect().await {
|
|
Ok(()) => println!(" -> connected: {}\n", printer.display_name()),
|
|
Err(err) => println!(" -> failed: {err}\n"),
|
|
}
|
|
}
|
|
|
|
// Vendor-only extension: reachable only after matching the concrete
|
|
// variant back out of the enum - one arm now that Bambu is a single
|
|
// type regardless of which protocol version `connect()` settled on.
|
|
// `print_status()` is real Bambu-only behavior with no equivalent on
|
|
// `GenericPrinter` (no other vendor here has an in-memory report
|
|
// cache) - none of these connected above (fake IPs), so it's always
|
|
// `None`, but the point is the downcast, not a live reading.
|
|
for printer in &fleet {
|
|
if let PrinterHandle::Bambu(bambu) = printer {
|
|
println!(" {}: cached print_status = {:?}", bambu.sn, bambu.print_status().await)
|
|
}
|
|
}
|
|
|
|
// A second trait method, same uniform call site as connect(): every
|
|
// vendor sends "50%" a completely different way - an MQTT gcode
|
|
// command, Moonraker's native API, gcode over HTTP, gcode over serial
|
|
// - but the caller doesn't need to know or care which.
|
|
println!();
|
|
for printer in &mut fleet {
|
|
let _ = printer.set_fan_speed(50).await;
|
|
}
|
|
}
|