Add a discovered_printers gateway message

A gateway can find printers on its own network and USB ports, but nothing
carried that back to the cloud, so adding one in the web app still meant an
operator reading an IP address and a serial number off a machine by hand.

Two schemas, both gateway -> cloud:

- `DiscoveredPrinter`, a candidate the gateway found but cannot drive,
  because it is not in that gateway's fleet file.
- A `discovered_printers` variant on the client message, carrying a whole
  batch that replaces what that gateway last reported. A printer that has
  gone is reported by its absence, and a gateway seeing nothing sends an
  empty list.

A candidate deliberately carries no credential, and cannot: a Bambu access
code is on the printer's screen and a PrusaLink API key is in its web UI, so
no probe learns either. What this buys is the address and the identity, which
are the parts an operator would otherwise have to go and look up.

Two details worth keeping in mind downstream:

`ambiguous_with` exists because PrusaLink serves OctoPrint's API, so a probe
that saw only that API cannot honestly claim PrusaLink. The candidate says
`octoprint` and names the alternative, and the operator settles it.

Every string here was chosen by a device on the customer's network - anything
answering an SSDP query or an HTTP probe names itself - so they are untrusted
display strings. Hence the length caps, and hence `DiscoveredPrinter` being
closed where its sibling variants are open: an unbounded LAN-supplied `name`
should not reach a database column or a React tree.
This commit is contained in:
2026-10-19 08:04:27 +00:00
parent 1386b1b4db
commit 6ad400d44c
3 changed files with 171 additions and 0 deletions
@@ -182,6 +182,32 @@
"session_id",
"message"
]
},
{
"type": "object",
"title": "DiscoveredPrinters",
"description": "Printers the gateway found on its own network or USB ports that are not in its fleet file, so an operator can add one in the web app without first reading an IP address or a serial off a machine. Sent as a whole batch and treated as a replacement for what this gateway previously reported, not as additions: a printer that has gone is reported by its absence, and a gateway that can see nothing sends an empty list. Only sent when discovery is enabled in the gateway's own config, which it is NOT by default - probing a customer's network is something they have to ask for.",
"properties": {
"type": {
"const": "discovered_printers"
},
"printers": {
"type": "array",
"maxItems": 256,
"items": {
"$ref": "./discovered_printer.schema.json"
}
},
"scanned_at": {
"description": "When the gateway finished the scan these results came from, so the cloud can show the age of the list rather than implying it is live.",
"type": "string",
"format": "date-time"
}
},
"required": [
"type",
"printers"
]
}
]
}
@@ -0,0 +1,55 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://schemas.continuum.dev/continuum/v1/edge/discovered_printer.schema.json",
"title": "DiscoveredPrinter",
"description": "A printer a gateway found on its own network or USB ports that is NOT in its fleet file - so it is a candidate for an operator to add, not a printer the gateway can drive. It deliberately carries no credential: a Bambu access code is on the printer's screen and a PrusaLink API key is in its web UI, and no probe can learn either. The point of sending these to the cloud is that an operator adding a printer in the web app should not have to go and read an IP address or a 15-character serial off a machine. Every string here is reported by a device on the customer's network and must be treated as untrusted display text - hence the length caps - because anything that answers an SSDP query or an HTTP probe chooses its own name.",
"type": "object",
"properties": {
"vendor": {
"description": "The `vendor` value this candidate would need in the gateway's fleet file. `octoprint` rather than `prusa_link` is the honest answer when a probe saw only the OctoPrint API, which PrusaLink also serves - see `ambiguous_with`.",
"type": "string",
"enum": ["bambu", "klipper", "prusa_link", "octoprint", "prusa_serial"]
},
"address": {
"description": "Where the gateway found it: an IP or hostname for a network printer, or a device path like `/dev/ttyACM0` for one on a serial port. Not a URL, and not reachable from the cloud - it is an address on the gateway's network, shown so an operator can recognise which machine this is.",
"type": "string",
"minLength": 1,
"maxLength": 255
},
"serial": {
"description": "The printer's serial number where the probe could learn it without a credential - which today means a Bambu printer's TLS certificate CN, or a Marlin board's `UUID`. Absent is normal and not an error.",
"type": "string",
"maxLength": 64
},
"model": {
"description": "The vendor's own model code or machine type, e.g. Bambu's `O1C2` or Marlin's `MACHINE_TYPE`.",
"type": "string",
"maxLength": 128
},
"name": {
"description": "The name the printer reports for itself, which is usually whatever the operator typed into their slicer or set as the host's hostname. Display only.",
"type": "string",
"maxLength": 128
},
"detail": {
"description": "Free-form human detail to recognise the machine by - a firmware version, a USB vendor string, or why a probe could not say more. Never parsed, and never written into a config file.",
"type": "string",
"maxLength": 256
},
"ambiguous_with": {
"description": "A vendor this could equally be, when the probe genuinely cannot tell. Today only `prusa_link` versus `octoprint`: PrusaLink implements OctoPrint's API, so without a credential nothing on the network distinguishes them. Present means the operator has to choose; the web app should offer both rather than picking one.",
"type": "string",
"enum": ["bambu", "klipper", "prusa_link", "octoprint", "prusa_serial"]
},
"source": {
"description": "Which probe found it, which bounds how much it can know: `certificate` and `usb_descriptor` are passive reads, `ssdp` is an unsolicited announcement, `http_api` an identity endpoint, and `serial_handshake` an `M115` that reset the board to ask.",
"type": "string",
"enum": ["ssdp", "certificate", "http_api", "usb_descriptor", "serial_handshake"]
},
"driver_ready": {
"description": "Whether this gateway build can actually drive this vendor. False means the printer was found and reported but its client is still a stub, so adding it would produce a printer that connects and does nothing - the web app should say so rather than offering it as ready to use.",
"type": "boolean"
}
},
"required": ["vendor", "address", "source", "driver_ready"]
}