Report discovered printers to the cloud, off by default
The gateway could already find printers; only a human reading a terminal could see the result. So adding a printer in the web console still began with walking to a machine to read its IP address off the screen. Now a background task scans on a timer and sends a `discovered_printers` frame. It cannot finish the job - the credential is on that screen and no probe can read it - but it removes the address, the serial and the guesswork about which vendor the thing is. OFF by default, which is the load-bearing part of this commit. A /24 sweep is 254 TLS handshakes and up to three HTTP requests per host, and nothing watching can tell that from a port scan; on a corporate network, being indistinguishable from a port scan is itself the problem. A gateway that did this by default would scan networks its operator never meant to point it at, and the first they would hear of it is from their own security team. So: - `discovery.enabled` is false unless set, per gateway, in that gateway's own config. The cloud cannot switch it on. A setting whose whole purpose is to bound what this process does to the local network has to be decided where that network is understood, which is not in a data centre. - `subnets` is explicit, never derived from this host's interfaces, so a second NIC, a VPN or a container bridge cannot widen the sweep. Anything wider than a /24 is still refused, now at config-import time rather than when the timer first fires. - A flag that is neither `true` nor `false` is an error, not a shrug. The two readings of a typo are not equally safe. - Naming a subnet without setting `enabled` scans nothing - the safe reading of an incomplete instruction - and `enabled` with nowhere to look is refused as the mistake it is. - The env var can switch it *off* over a file that enables it, which is the direction that has to work: a Proxy Box's config is signed in the cloud, so an operator who finds their gateway probing cannot just edit it. What a scheduled scan will not do, unlike the example CLI: send `M115`. Opening a serial port toggles DTR, which reboots an Arduino-based board and ruins a print in progress. The example can do it behind `--yes-reset-the-printer` because a human is standing there; a timer has nobody to ask, so it does not get the option and reads sysfs instead. Two things that took more care than expected: `Scan::looked()`. An empty report means "this gateway looked and its network has no unconfigured printers", so sending one after every probe failed would state something untrue about the farm. A partial failure is still evidence, though - withholding a good sweep because a container can never bind SSDP would suppress it forever - so the test is "did anything succeed", not "did everything". `fleet::configured()` reads the fleet file at parse level rather than through `load`, because the entry it most needs to recognise is the one `load` refuses: a printer discovery suggested, pasted in, with the access code not yet typed in. Re-offering that printer as a fresh find is noise at the one moment the operator is mid-way through adding it. Verified against the real LAN: a sweep found both printers by TLS certificate with correct serials and no credentials, and the frame it produced validates against the backend's EdgeClientMessageSchema. The `wire` subcommand on the example prints exactly that frame, and the capture is pinned as a backend fixture. SSDP remains untestable here - this container is a router hop from the printers and multicast does not cross one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -56,6 +56,58 @@ pub enum ClientMessage {
|
||||
/// waiting on this and a viewer otherwise watches a video element that
|
||||
/// never loads with nothing anywhere saying why.
|
||||
WebrtcError { session_id: String, message: String },
|
||||
/// Printers found on this gateway's own network and USB ports that are
|
||||
/// *not* in its fleet file, so the console can offer them instead of
|
||||
/// making an operator walk to a machine and read its IP address off the
|
||||
/// screen.
|
||||
///
|
||||
/// A whole scan result that replaces the last one, not a set of
|
||||
/// additions: a printer that has gone is reported by its absence. An
|
||||
/// empty list is therefore meaningful - it says the gateway looked and
|
||||
/// found nothing, which is different from never having sent one of these
|
||||
/// at all, and a gateway with discovery off (the default) never does.
|
||||
DiscoveredPrinters {
|
||||
printers: Vec<DiscoveredPrinter>,
|
||||
scanned_at: String,
|
||||
},
|
||||
}
|
||||
|
||||
/// One candidate, carrying no credential - and it is worth being precise
|
||||
/// about why not, because this is the field a reader looks for.
|
||||
///
|
||||
/// It is not omitted for tidiness. No probe can learn one: a Bambu access
|
||||
/// code is displayed on the printer's own screen and a PrusaLink API key is
|
||||
/// in its web UI, and every identity endpoint that would reveal either needs
|
||||
/// the credential first. So discovery shortens the operator's job to typing
|
||||
/// one secret, rather than a secret plus an address plus a serial number -
|
||||
/// and the backend's schema refuses a candidate that carries a credential,
|
||||
/// so this staying empty is enforced at the far end too.
|
||||
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
|
||||
pub struct DiscoveredPrinter {
|
||||
/// The `vendor` string this would need in a fleet file.
|
||||
pub vendor: &'static str,
|
||||
/// An IP or hostname, or a device path like `/dev/ttyACM0` for a serial
|
||||
/// candidate. Named `address` rather than `host` because half of them
|
||||
/// are not hosts.
|
||||
pub address: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub serial: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub model: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub name: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub detail: Option<String>,
|
||||
/// A vendor this could equally be; the operator settles it. Today only
|
||||
/// `prusa_link` against `octoprint`, which nothing observable separates.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub ambiguous_with: Option<&'static str>,
|
||||
pub source: &'static str,
|
||||
/// Whether this build can actually drive the vendor. False is still
|
||||
/// worth reporting - it answers "what is on this network?" - but the
|
||||
/// console has to say so, or an entry looks finished when adding it
|
||||
/// would produce a printer that connects and does nothing.
|
||||
pub driver_ready: bool,
|
||||
}
|
||||
|
||||
/// A flattened snapshot of one printer, in the terms an operator console
|
||||
|
||||
Reference in New Issue
Block a user