diff --git a/docs/explanations/2026-07-17-explanation-brother-printer-status-truth.html b/docs/explanations/2026-07-17-explanation-brother-printer-status-truth.html new file mode 100644 index 00000000..83afa3d3 --- /dev/null +++ b/docs/explanations/2026-07-17-explanation-brother-printer-status-truth.html @@ -0,0 +1,489 @@ + + +
How brother_printer stopped inventing what the printer was doing — and learned to say only what the hardware actually reports.
brother_printer is a small command-line tool that answers one question: is my Brother HL-1110 laser printer okay? You run ./run.sh and it prints a status report — online or not, toner and drum estimates, whether anything needs attention.
To answer that, it talks to the printer three ways. The important one here is PJL (Printer Job Language): a plain-text control protocol you speak over the USB character device /dev/usb/lp0. Send @PJL INFO STATUS, and the printer answers with a numeric code and a display string:
@PJL INFO STATUS
+CODE=40000
+DISPLAY="SLEEP "
+ONLINE=TRUE
+
+The tool's job is to turn that CODE=40000 into a human sentence. It does that with a lookup table, BROTHER_STATUS_CODES, mapping each code to a (severity, short text, suggested action) triple. The report's headline — healthy, warning, or "action required" — comes straight from that severity.
The whole report is only as truthful as that table. And that turned out to be the problem.
+ +Before this change, two things were broken at once. First, ./run.sh didn't run at all — it crashed with ModuleNotFoundError: No module named 'python_pkg' before printing a single line. Once that was fixed, the report it produced was worse than no report: a printer sitting quietly asleep was announced as a critical paper jam.
The core bug in one line. The lookup table said 40000 → Paper Jam (critical). The printer's own manual says 40000 → SLEEP MODE (STANDBY), with a footnote in the spec that reads, verbatim: "This is not an error. The printer is waiting for data."
Where did the wrong table come from? Its comment cited a "Brother PJL Technical Reference" that nobody could locate. When each code was checked against the real authority — the HP PJL Technical Reference Manual, Appendix D, which Brother printers actually follow — only 2 of the 25 entries were correct. The table wasn't transcribed; it looked generated, plausible-sounding numbers attached to plausible-sounding meanings.
+ +To be sure the HP manual really applied to this printer, six states were induced on the actual HL-1110 and the wire codes recorded:
+ +| CODE | Printer display | HP spec says | Old table said |
|---|---|---|---|
| 40000 | SLEEP | Sleep — not an error | Paper Jam · critical |
| 40021 | TOP COVER OPEN | Printer open / close cover | absent (claimed 41000) |
| 10003 | WARMING UP | Warming up | Self-test / Calibrating |
| 10006 | — | Toner Low | Processing · benign |
| 41213 | NO PAPER | Out of paper (41xyy family) | absent |
| 10001 | READY | Ready | Ready ✓ |
All six matched the spec. None but 10001 matched the old table. Notice the failure cuts both ways: 40000 cried wolf (calm printer → critical alarm), while 10006 did the opposite — a genuine toner-low warning was dressed up as a harmless "Processing" and would never reach you.
The fix isn't just "correct the numbers." It's a rule about honesty: never assert a meaning the hardware didn't give you. Three consequences follow from that:
+ +rebuilt Codes are transcribed from the HP manual, verbatim. 40000 is Sleep (severity ok); the real paper-jam code, 40022, was added — it wasn't even in the old table.
dropped Nine codes that appear in no reference (like the invented 40310, 42000) were deleted. Ask for one now and you get an honest "Unknown status (code X) — check printer display" instead of a confident lie.
ranged The HL-1110 reports 41213 for an empty tray, but its last three digits don't decode per the spec's sub-tables. So the 41xyy family matches as a family — right about "out of paper," silent about details it can't trust.
The same lie had a second home. CUPS (the print system) reports its own reasons, which the tool maps to PJL codes. That map sent media-jam → 40000. After the rebuild, 40000 means Sleep — so a real jam detected via CUPS would have been reported as "Sleep." Fixing the table without fixing this map would have just moved the bug. Both were corrected together (media-jam → 40022).
Type a PJL code (or tap one of the states induced on the real printer) and watch the two tables disagree. The left column is what the old code would have told you; the right is what the new code says, next to what the HP manual actually documents.
+ +The change touches five files, but it's cleaner to read it as five concerns. Only the hunks that carry the idea are shown.
+ +./run.sh actually startEvery module imports absolutely (python_pkg.brother_printer.*), so the repo root must be importable. The script ran the file by path, which only puts the package dir on the path. And sudo wipes the environment, so exporting PYTHONPATH first wouldn't survive. The fix passes it as a sudo argument and runs the package as a module.
-if [[ $EUID -ne 0 ]]; then
+- exec sudo python3 "$SCRIPT_DIR/check_brother_printer.py" "$@"
+-fi
+-exec python3 "$SCRIPT_DIR/check_brother_printer.py" "$@"
++REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
++if [[ $EUID -ne 0 ]]; then
++ exec sudo PYTHONPATH="$REPO_ROOT" python3 -m python_pkg.brother_printer "$@"
++fi
++exec env PYTHONPATH="$REPO_ROOT" python3 -m python_pkg.brother_printer "$@"
+The heart of the change. 40000 flips from a critical jam to a benign sleep; 40022 becomes the real jam; unknown codes fall through to an honest message rather than a fabricated one.
- 40000: ("critical", "Paper Jam", "Clear the paper jam and close all covers."),
++ # 40000 is flagged in the reference as "not an error - waiting for data".
++ 40000: ("ok", "Sleep (standby)", ""),
++ 40022: ("critical", "Paper Jam", "Clear the paper jam and close all covers."),
+
++def _unknown_status(code: str) -> tuple[str, str, str]:
++ """Describe a code that is not in the PJL reference. Never guesses."""
++ return ("info", f"Unknown status (code {code})",
++ "Check printer display for details.")
+Toner and drum estimates came from CUPS's page log, which only counts jobs CUPS itself printed — it undercounts. The printer knows its true lifetime count via @PJL INFO PAGECOUNT. Reading it meant sending one more PJL query and migrating the stored "replaced at" baseline onto the printer's scale once, so the reported percentages don't visibly jump.
_retry_pjl_query(fd, "@PJL INFO STATUS", _parse_status, result, max_retries)
++_retry_pjl_query(fd, "@PJL INFO PAGECOUNT", _parse_pagecount, result, max_retries)
+ _retry_pjl_query(fd, "@PJL INFO VARIABLES", _parse_variables, result, max_retries)
+Two latent defects surfaced during verification. The device was opened blocking, so an unwell printer froze the tool forever — exactly when you want a status report. And the port-status probe stopped CUPS and reset the USB device just to read one byte, killing any in-flight print and then misreporting the damage as a printer error. The open became non-blocking; the probe now reads the same byte via a free ioctl and skips entirely while a job is printing.
-fd = os.open(dev_path, os.O_RDWR)
++# O_NONBLOCK or this open() hangs forever on an unwell printer.
++fd = os.open(dev_path, os.O_RDWR | os.O_NONBLOCK)
+
+- dev.reset() # destroyed the in-flight job
+- ... claim interface, stop CUPS ...
++status = _port_status_via_usblp() # free ioctl, no side effects
++if status is not None: return status
++if _cups_is_busy(cups_state): return None # never fight a running job
+The HL-1110 has ~1 MB of memory. A page whose 600 dpi raster overflows it is discarded — the printer stays READY, CUPS reports success, and nothing anywhere flags the loss. The only witness is the gap between what CUPS claims it sent and how far the printer's own counter advanced.
+def check_page_delivery(printer_total, *, queue_idle):
++ # Only meaningful between jobs; mid-job the counters differ legitimately.
++ cups_delta = cups_total - state["last_cups_total"]
++ printer_delta = printer_total - state["last_printer_count"]
++ check.dropped = cups_delta - printer_delta
++ check.suspected = check.dropped >= PAGE_DROP_WARN_THRESHOLD
+When it fires, the report reads: "60 pages did not print. CUPS sent 63 pages since the last check; the printer's own counter advanced by 3. Reprint at lp -o Resolution=300dpi." — the answer that took a long debugging session to find, now surfaced in one run.
Five questions on the reasoning behind the change. Click an option to check it.
+ +6 explanations · interactive walkthroughs of notable changes, newest first.
+7 explanations · interactive walkthroughs of notable changes, newest first.