brother — the Brother multifunction device: scan, printer state, admin pages
One module for one device (novox/hq ADR 0467, issue 1453): the scanner and the printer are one machine on the
network, so one module serves both. It claims two seats, each held once for the mesh: scanner (verbs scan,
status, list) and printer (verbs status, settings; restart follows in a later round). It replaces the
scan module of issue 1440 (ADR 0464), which ran on no node and is gone from the catalogue.
The scanner seat
Built for issue 1440 as novox/hq ADR 0464 decides. It scans the glass of a network scanner (any scanner that
speaks eSCL, also called AirScan or Mopria), cuts the document out of the lid's white, files it as a one-page PDF
in a folder and says scan.added on the bus, so other modules can react. A scan happens when a tool is called:
the module runs no process of its own, and it runs as the node's operator account.
What a scan does
- Asks the scanner for its glass size and puts one job to it (
POST <scanner>/ScanJobs): the platen, the whole glass, at the configured resolution and colour mode, as a JPEG; it fetches the page once. - Reads the resolution from the image's own JFIF density, never from the request: a scanner may answer a 300 dpi request with 200 dpi, and the page size and the event's millimetres follow what it gave.
- Crops: the lid's level is measured in the corner farthest from where documents lie, a pixel is document when
it is darker than the lid, and the rows and columns holding enough of them give the box. No document, or one
filling nearly the whole glass, keeps the whole image (
croppedfalse), and so doescropfalse. - Files a one-page PDF holding the JPEG: written whole beside its place as
.scan-*.tmpand renamed toscan-YYYY-MM-DD-HHMMSS.pdf(local time); a taken name gets-2,-3, and no file is ever overwritten. - Says
scan.addedonce, after the rename, under the file's sha256 as its message id. It never carries the image.
A scanner that refuses or gives no page files nothing and says nothing on the bus; the tool's error says why.
Config
| key | meaning | default |
|---|---|---|
scanner |
the scanner's eSCL address, such as http://192.0.2.10/eSCL (required, trusted) |
none |
scan-folder |
the absolute path the PDFs are filed in (required, trusted) | none |
printer |
the printer's host, such as 192.0.2.10, for its admin page and IPP (required, trusted) |
none |
printer-certificate |
the SHA-256 fingerprint of the one certificate the admin page may serve, AA:BB:... (required, trusted) |
none |
resolution |
dpi asked: 100, 200, 300 or 600 | 300 |
color-mode |
color or gray |
color |
crop |
cut the document out of the lid's white | true |
The values reach the code in one file, settings.json in the module's state directory, which the mesh writes
from the config; the code reads it at every call. With scanner or scan-folder unset, every scanner verb says which and
writes and asks nothing; printer and printer-certificate are the printer verbs' to say.
The scan folder is not a manifest resource: a directory's path cannot take a config value (issue 1441), so the code makes it (mode 0755) when it files the first scan.
Tools
| verb (tool) | does |
|---|---|
scanner.scan |
scans, crops, files and emits; answers the event's body |
scanner.status |
whether the scanner answers, its state (Idle or Processing) and its document feeder's state; a scanner that does not answer is answers: false with why. This is how its health is read: the module has no long-running resource to declare a health check on |
scanner.list |
the newest filed scans, newest first (limit, default 20). Read-only |
The printer seat
| verb (tool) | does |
|---|---|
printer.status |
without a login: over IPP (Get-Printer-Attributes, encoded by hand, on port 631) the state (idle, processing, stopped), the state reasons (such as toner-low-warning) and the markers' names, levels (percent; below 0 is unknown) and colours; over eSCL the scanner's state and its feeder's. A part that does not answer is answers: false with why. Needs printer and scanner |
printer.settings |
asks the admin process (below) to log in, read one admin page and log out, and waits up to 60 s for its answer. Argument page: a path on the admin site, starting with / and ending in .html (default /general/status.html). Answers the page's title, links (href and text) and forms with their fields (name, type, value, label); a password field's value is never given. Read-only: it sends the login and the logout and nothing else |
Trust and the password
The admin page is HTTPS with a self-signed certificate. The module verifies no chain: it accepts the one certificate
whose SHA-256 fingerprint is printer-certificate and refuses any other, in the handshake, so no password is sent
to it; the error names both fingerprints. Get the fingerprint from the device's certificate page, and give it as 32
upper-case hex pairs joined by colons.
The admin password is the module's own secret admin-password (ADR 0467 clause 4; ADR 0283): the operator gives it
at their desk, and the mesh never generates or rotates it. It is owned by the account brother-admin, which the
module makes, because the tools run as the operator account and must never read it (ADR 0436 §1, §12; the shape of
mesh-mail's sender). The file is ${dir:state}/admin-password.secret, mode 0600, and BROTHER_ADMIN_PASSWORD names
it for the admin process alone; the tools' bundle is given no such name, and admin_boundary_test.go checks that
no tool file imports the package that reads it. While it is not given, printer.settings answers that the secret
is not given, and the printer is not contacted.
The admin process
brother admin runs as brother-admin with a bus account of its own (the own secret broker-admin), never a
seat. It consumes the work-queue stream printer-asks (event printer.asked, body id, page, asked-at) and
writes each answer, once, into the state printer-answers under the ask's id (ten minutes' life): the page's
title, links and form fields, or error. It alone reads the password, pins the certificate, logs in, reads the
page and logs out. An ask older than two minutes is dropped without contacting the printer; at most three
deliveries. It reads settings.json (mode 0644, no secret in it) and the state directory is 0711 so that it can
reach its own files; both are the choices that let its account read what it needs. Its health is its unit.
The login reads the form each time: the device generates the password field's name, so the code takes the name from
the form, posts it with the form's hidden fields, and keeps the session cookie. A login is failed when the answer is
the login page again. The logout follows a logout link or form on the page; a page with none is said in the answer
(logout), and the session is left to expire on the device.
The event
scan.added, said by this module, on the wire mesh.mod.brother.event.scan.added; a consumer declares brother.scan.added. Its body:
path, node, bytes, sha256, width-mm, height-mm, resolution (dpi, as the scanner gave it),
color-mode, cropped, scanned-at (RFC 3339, UTC).
Files it does not know
Each directory or archive this module declares says how it treats a file it did not write (ADR 0422, ADR 0416).
${dir:state}: everything in it is the program's; the mesh moves nothing out (ADR 0422 §2).- The scan folder (
scan-folder) is declared by no manifest. The module writes only thescan-*.pdffiles it files and their.scan-*.tmpwhile they are written; it never changes, moves or removes any other file there, andscanner.listonly reads.
Tests
The unit tests use the captured answers of a real scanner (cmd/brother/testdata/escl, captured 2026-10-20) served
by a test HTTP server, the captured IPP exchange (testdata/ipp) replayed, and the captured login page
(testdata/admin/login-page.html) served by a test TLS server with its own certificate. The pages behind the login
are synthetic (testdata/admin/synthetic-*.html) until round 2 captures the real ones. No test reaches the real
device.