testsAndMisc/python_pkg/brother_printer/constants.py
Krzysztof kuhy Rudnicki c4d5631054
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (mypy, mypy) (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (pylint, pylint) (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (rest, ) (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (shellcheck, shellcheck) (push) Waiting to run
Python tests / test (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / lib-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
fix(brother_printer): truthful probe note, pyusb auto-install, stuck-job detection
The CUPS-fallback note blamed a missing pyusb when the probe was really
skipped because CUPS owned the printer. It now names the real reason.
run.sh installs python-pyusb for the system python3 that sudo runs.

A paper misfeed blocks the usb:// backend while CUPS keeps saying
"processing", and cancel leaves that backend hung so the queue never
recovers. The report now flags a job whose page counter is frozen for
15 s, and a backend of a cancelled job, with a y/N offer to SIGKILL it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AaBRWtXXWCs3K3yPHSruUC
2026-10-12 16:23:48 +02:00

247 lines
8.6 KiB
Python

"""Constants, status codes, and lookup tables for Brother printer checking."""
from __future__ import annotations
import sys
# ── Colors ───────────────────────────────────────────────────────────
RED = "\033[0;31m"
YELLOW = "\033[1;33m"
GREEN = "\033[0;32m"
CYAN = "\033[0;36m"
BOLD = "\033[1m"
DIM = "\033[2m"
RESET = "\033[0m"
# ── SNMP supply level sentinel values ────────────────────────────────────────
SNMP_LEVEL_OK = -3
SNMP_LEVEL_LOW = -2
SUPPLY_LOW_PCT = 10
SUPPLY_WARN_PCT = 25
PROGRESS_BAR_WIDTH = 25
# Brother HL-1110 consumable page ratings
TONER_RATED_PAGES = 1000
DRUM_RATED_PAGES = 10000
CUPS_PAGE_LOG_PATH = "/var/log/cups/page_log"
CONSUMABLE_STATE_DIR = ".config/brother_printer"
MIN_LPSTAT_JOB_PARTS = 4
# How many pages CUPS may claim over the printer's own counter before we call
# it dropping pages. A small gap is just timing (a job finishing as we look);
# a large one means the printer is discarding work and saying nothing.
PAGE_DROP_WARN_THRESHOLD = 5
BROTHER_USB_VENDOR_ID = 0x04F9
def _out(text: str = "") -> None:
"""Write a line to stdout."""
sys.stdout.write(text + "\n")
def _prompt(text: str) -> str:
"""Read user input with a prompt."""
sys.stdout.write(text)
sys.stdout.flush()
return sys.stdin.readline().strip()
# ── PJL status codes ─────────────────────────────────────────────────
# Transcribed from the HP PJL Technical Reference Manual, Appendix D
# ("PJL Status Codes"). Brother publishes no PJL status table of its own,
# but an HL-1110 was probed on real hardware (2026-07-17) and matched the HP
# codes for every state that could be induced:
#
# 10001 READY 10003 WARMING UP 10023 PRINTING
# 40000 SLEEP 40021 TOP COVER OPEN 41213 NO PAPER
#
# Only codes documented in that reference appear below. Anything else is
# reported as unknown by get_status_info() rather than guessed at: a previous
# revision of this table invented meanings (40000 "Paper Jam", 30010 "Toner
# Low", 41000 "Cover Open" ...) and consequently reported a sleeping printer
# as a critical paper jam, while a genuine toner-low warning (10006) was
# rendered as a benign "Processing".
#
# Format: code -> (severity, short_text, action)
# Severities: ok, info, warn, critical
_ORDER_TONER = "Order replacement toner cartridge (TN-1050/TN-1030 compatible)."
BROTHER_STATUS_CODES: dict[int, tuple[str, str, str]] = {
# Informational (10xxx)
10001: ("ok", "Ready", ""),
10002: ("info", "Ready (offline)", "Press the power button to bring it online."),
10003: ("ok", "Warming up", ""),
10004: ("info", "Self test", ""),
10005: ("info", "Resetting / clearing memory", ""),
10006: ("warn", "Toner Low", _ORDER_TONER),
10007: ("info", "Canceling job", ""),
10014: ("info", "Printing", ""),
10023: ("info", "Printing / processing job", ""),
# Auto-continuable conditions (30xxx)
30010: ("info", "Status buffer overflow", ""),
# Potential operator intervention (35xxx)
35078: ("ok", "Power save", ""),
# Operator intervention required (40xxx). 40000 is flagged in the
# reference as "not an error - the printer is waiting for data".
40000: ("ok", "Sleep (standby)", ""),
40010: (
"critical",
"No Toner Cartridge",
"Install a toner cartridge (TN-1050/TN-1030 compatible).",
),
40019: (
"warn",
"Remove Paper From Output Bin",
"Take the printed pages out of the output bin.",
),
40021: (
"critical",
"Cover Open",
"Close the printer cover and check the toner cartridge is seated.",
),
40022: ("critical", "Paper Jam", "Clear the paper jam and close all covers."),
40026: ("critical", "Install Paper Tray", "Insert the paper tray."),
40038: ("warn", "Toner Low", _ORDER_TONER),
40050: (
"critical",
"Fuser Error (50 SERVICE)",
"Power-cycle the printer. If the error persists, contact service.",
),
40079: (
"warn",
"Offline",
"The printer is offline. Press the power button to bring it online.",
),
}
# Code families that encode a sub-status in their trailing digits. The
# HL-1110 reports 41213 for an empty tray, which places it in the reference's
# "Foreground Paper Loading (41xyy)" family - but its x/yy digits do not
# decode per the HP tray/media tables (x=2 "upper cassette", yy=13 "Japan B5"
# on a single-tray A4 printer). So match the family and ignore the digits.
# Each entry is an inclusive low/high bound plus the usual status triple.
_PAPER_ACTION = "Load paper; if the tray is full, fan and reseat the stack."
_STATUS_CODE_RANGES: tuple[tuple[int, int, tuple[str, str, str]], ...] = (
(
41000,
41999,
# A failed pick-up raises the same paper-empty bit as an empty tray.
("critical", "Out of Paper / not feeding", _PAPER_ACTION),
),
(
50000,
50999,
(
"critical",
"Hardware Error",
"Power-cycle the printer. If the error persists, contact service.",
),
),
)
# ── CUPS status code mappings ────────────────────────────────────────
# These translate what CUPS reports into the PJL codes above, for the
# fallback path used when the usblp device is unavailable and the printer
# cannot be asked directly. Every value must name a real code from
# BROTHER_STATUS_CODES (or a range family), otherwise the fallback silently
# degrades to "unknown status".
_CUPS_REASONS_TO_STATUS: dict[str, int] = {
"paused": 10002,
"moving-to-paused": 10002,
"toner-low": 40038,
"toner-empty": 40010,
"marker-supply-low": 40038,
"marker-supply-empty": 40010,
"media-empty": 41000,
"media-needed": 41000,
"media-jam": 40022,
"cover-open": 40021,
"door-open": 40021,
"input-tray-missing": 40026,
}
_CUPS_STATE_TO_STATUS: dict[str, int] = {
"idle": 10001,
"processing": 10023,
"stopped": 40079,
}
_ERROR_REASON_MAP: tuple[tuple[tuple[str, ...], str, str], ...] = (
(("media-jam",), "40022", "Paper Jam"),
(("cover-open", "door-open"), "40021", "Cover Open"),
(("toner-empty",), "40010", "No Toner Cartridge"),
(("toner-low",), "40038", "Toner Low"),
)
# States this tool infers itself, rather than reads off the printer: from the
# CUPS page-count estimate, or from an error bit whose cause we cannot name.
# They deliberately are not PJL numbers - borrowing a PJL code for a state the
# printer never reported is how the old table came to claim a sleeping printer
# had jammed.
DERIVED_TONER_END = "estimate:toner-end"
DERIVED_TONER_LOW = "estimate:toner-low"
DERIVED_CUPS_ERROR = "cups:error"
DERIVED_JOB_STALLED = "cups:job-stalled"
DERIVED_BACKEND_ORPHANED = "cups:backend-orphaned"
_DERIVED_STATUS_CODES: dict[str, tuple[str, str, str]] = {
DERIVED_TONER_END: (
"critical",
"Toner End (estimated from page count)",
_ORDER_TONER,
),
DERIVED_TONER_LOW: ("warn", "Toner Low (estimated from page count)", _ORDER_TONER),
DERIVED_CUPS_ERROR: (
"warn",
"Printer reports an error",
"Check the printer display for details.",
),
DERIVED_JOB_STALLED: (
"critical",
"Print job stalled (no page progress in 15 s)",
"Paper likely failed to feed: reseat it or power-cycle the printer.",
),
DERIVED_BACKEND_ORPHANED: (
"critical",
"Queue blocked by a hung backend of a cancelled job",
"Kill the hung backend (offered below); CUPS recovers on its own.",
),
}
def get_status_info(code: str) -> tuple[str, str, str]:
"""Look up a PJL status code. Returns (severity, text, action)."""
derived = _DERIVED_STATUS_CODES.get(code)
if derived is not None:
return derived
try:
numeric = int(code)
except TypeError, ValueError:
return _unknown_status(code)
exact = BROTHER_STATUS_CODES.get(numeric)
if exact is not None:
return exact
for low, high, info in _STATUS_CODE_RANGES:
if low <= numeric <= high:
return info
return _unknown_status(code)
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.",
)