Reporting the truth

How brother_printer stopped inventing what the printer was doing — and learned to say only what the hardware actually reports.

Commit a12e696 · 2026-07-17 · python_pkg/brother_printer · +1263 / −407

What's inside
  1. Background — the status pipeline
  2. Intuition — a table full of fiction
  3. Playground — decode a status code
  4. Code — the five moving parts
  5. Quiz — check your understanding

1 · Background

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.

Printer
USB / PJL
→
CODE=40000
raw reply
→
lookup table
code → meaning
→
Report
what you read

The whole report is only as truthful as that table. And that turned out to be the problem.

2 · Intuition

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:

CODEPrinter displayHP spec saysOld table said
40000SLEEPSleep — not an errorPaper Jam · critical
40021TOP COVER OPENPrinter open / close coverabsent (claimed 41000)
10003WARMING UPWarming upSelf-test / Calibrating
10006—Toner LowProcessing · benign
41213NO PAPEROut of paper (41xyy family)absent
10001READYReadyReady ✓

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 principle that came out of it

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).

3 · Playground — decode a status code

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.

PJL status decoder old vs new

Old table

—
—

New code

—
—

4 · Code — the five moving parts

The change touches five files, but it's cleaner to read it as five concerns. Only the hunks that carry the idea are shown.

a. Make ./run.sh actually start

Every 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.

python_pkg/brother_printer/run.sh
-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 "$@"

b. Rebuild the status table from the spec

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.

python_pkg/brother_printer/constants.py
-    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.")

c. Read the printer's own page counter

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.

python_pkg/brother_printer/usb_query.py
 _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)

d. Reading status must never hang or destroy a job

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.

python_pkg/brother_printer/usb_query.py · cups_service.py
-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

e. Notice pages the printer silently drops

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.

python_pkg/brother_printer/cups_service.py
+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.

5 · Quiz

Five questions on the reasoning behind the change. Click an option to check it.

Score: 0 / 0