a220bf56cce3fa1c28a731c5e8c4531ee017ba43
2
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
4f37384706
|
Report the OctoPrint API as what it is, and let the operator name it
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>
|
||
|
|
e6830d3846
|
Discover Klipper, PrusaLink and serial printers too
Discovery found only Bambu printers, so a mixed farm still meant typing most of printers.toml by hand. It now covers the four vendors fleet.rs accepts, over both transports. Detection splits in two, and keeping the halves apart is what makes this honest. "Something at this address speaks X" needs no credential from any vendor, and is most of the value - it yields the address and the vendor, which is two of the three or four fields an entry needs. "What is it called, what firmware does it run" only has an answer where the vendor publishes one unauthenticated: Bambu always, Moonraker usually, PrusaLink never. So an HTTP 401 counts as a find rather than a failure. A password prompt from /api/version is proof that something is serving /api/version, which is the question being asked. It also makes the Moonraker probe correct without having to know how a given host configured [authorization], which is not knowable from outside. Klipper is `GET :7125/printer/info`, Moonraker's own identity endpoint, which returns hostname and software_version. PrusaLink is `GET :80/api/version`; its OpenAPI specification puts digestAuth on every operation including /api/v1/info, so there is no unauthenticated identity endpoint at all and a 401 is the best available answer - which is enough, since the only field still missing is the API key no probe could supply. This cannot distinguish PrusaLink from OctoPrint, and that is not fixable from outside: PrusaLink deliberately implements OctoPrint's API. A wrong guess an operator can see and correct beats skipping a real printer. Two guards against claiming things that are not printers, both now tested against what a real LAN actually answers. Three devices on the test segment answer /api/version with HTTP 200 and a body that is not JSON - a router, a device with a logon page, and one serving gzip - so a 200 must carry OctoPrint's guaranteed `api` field, and Moonraker's must carry the `result` wrapper every Moonraker reply has. And a bare 401 is not enough either: routers, NASes and cameras all password protect port 80, so a 401 must carry a *Digest* challenge, which PrusaLink's spec guarantees and the Basic-auth crowd does not send. A sweep of the real /24 still finds exactly the two Bambu printers and nothing else. Serial detection is passive by default, and that is a safety property rather than a limitation. Opening a serial port toggles DTR, which reboots an Arduino-based board - marlin_serial.rs documents the same thing from the other direction - so a firmware handshake against a machine that happens to be printing ruins the print, and discovery is exactly what someone runs while a farm is busy. So list_ports opens nothing: it reads USB descriptors out of sysfs, which is where udev reads them from, and gets the manufacturer, product and USB serial without touching the device. That is enough to render an entry, since a serial printer's block needs only a path and a baud rate. probe_m115 is the opt-in extra that learns the firmware and the exact machine type, and it refuses to run without --yes-reset-the-printer, the same shape as bambu_print_probe's --yes-start-a-real-print. sysfs rather than tokio-serial's libudev feature: a gateway appliance should not need libudev-dev at build time to list its own ports, and reading the four files udev would read is thirty lines and no dependency. The walk climbs to the USB device holding idVendor rather than counting parents, because a ttyACM link points at an interface and a ttyUSB link at a usb-serial port, each a different depth down. Only Prusa's own USB vendor id counts as evidence of a printer. Everything else in the table is a generic bridge chip - FTDI, CH340, CP210x - that a printer is likely to be behind but that a GPS or an Arduino project could equally be, so those are reported with "could be a printer or any other serial device" rather than asserted. Entries now carry exactly the fields each vendor's loader demands and no others, with a test that fails if to_toml_entry and fleet.rs drift apart, and a rendered entry for a vendor whose client is still a stub says so - otherwise an operator pastes in a Klipper printer and gets a gateway that connects and does nothing. Found while testing the M115 parser: a value ran to end-of-input instead of end-of-line, so the last field on a line swallowed what followed and UUID read as "cede2a2f-...\nok". Unverified, and untestable here: the Klipper and PrusaLink probes (no such hardware) and all serial detection (no serial device attached, and /sys/class/tty holds only virtual consoles). Every parser those depend on is unit tested against recorded protocol text, including the real non-printer responses above. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |