testsAndMisc/python_pkg/brother_printer/data_classes.py
Krzysztof kuhy Rudnicki a12e696494
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Python tests / test (push) Waiting to run
fix(brother-printer): report the truth, not invented status codes
./run.sh failed outright with ModuleNotFoundError, and once running it
reported a sleeping printer as a critical paper jam. Both are fixed, plus
the defects found while verifying them against the real HL-1110.

Entry point: run.sh ran the script by path, so the absolute
python_pkg.* imports could not resolve. Resolve the repo root and pass
PYTHONPATH as a sudo argument, since sudo's env_reset strips an exported
one. __main__.py had the wrong import prefix and ran main() on import.

Status codes: the table claimed provenance from a "Brother PJL Technical
Reference" that does not appear to exist, and 23 of its 25 entries were
wrong. Rebuilt from the HP PJL Technical Reference Manual, Appendix D,
and verified against six states induced on real hardware (10001 READY,
10003 WARMING UP, 10023 PRINTING, 40000 SLEEP, 40021 TOP COVER OPEN,
41213 NO PAPER) - all six matched the reference, none matched the table.
40000 is SLEEP MODE, which the reference explicitly flags "not an error";
the real jam code 40022 was absent entirely. The inverse also bit: 10006
is TONER LOW, previously shown as a benign "Processing". Codes in no
reference are dropped and now report "Unknown status - check printer
display" rather than a confident wrong answer, and states we infer
ourselves no longer borrow PJL numbers to describe themselves.

Page counts: estimates came from the CUPS page log, which only sees jobs
CUPS logged and counts pages the printer never printed (it logged
total=50 for a job that printed 3). Take them from @PJL INFO PAGECOUNT
instead, migrating stored baselines onto the printer's scale once so the
reported percentages do not jump.

Reading status must not cost anything: a blocking open() hung forever
whenever the printer was unwell - precisely when a status report is
wanted - and the port-status probe stopped CUPS and reset the USB device,
destroying the in-flight job and then misreporting the damage as a
printer error. The fd is now non-blocking with deadline-bounded writes,
and the probe reads the same byte via the free usblp ioctl, skips
entirely while a job prints, and re-attaches the kernel driver.

Dropped pages: the HL-1110 silently discards pages whose 600dpi raster
exceeds its ~1MB memory, stays READY, and CUPS still calls the job
successful - so nothing anywhere reports the failure. Compare CUPS's
claim against the printer's counter between jobs and name the workaround
(lp -o Resolution=300dpi).

402 tests, 100% branch coverage.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RG3eEn8rwZbcXaJrKJxdv8
2026-07-17 14:49:23 +02:00

133 lines
3.4 KiB
Python

"""Data classes for Brother printer status information."""
from __future__ import annotations
from dataclasses import dataclass, field
@dataclass
class CUPSJob:
"""A single CUPS print job."""
job_id: str
user: str
size: str
date: str
@dataclass
class CUPSQueueStatus:
"""Status of the CUPS print queue for a printer."""
printer_name: str = ""
enabled: bool = True
reason: str = ""
jobs: list[CUPSJob] = field(default_factory=list)
has_backend_errors: bool = False
last_backend_error: str = ""
@dataclass
class PageCountEstimate:
"""Estimated consumable life based on the printer's lifetime page count."""
total_pages: int = 0
toner_pages: int = 0
drum_pages: int = 0
toner_pct_remaining: int = 100
drum_pct_remaining: int = 100
toner_exhausted: bool = False
toner_low: bool = False
drum_near_end: bool = False
# True when total_pages came from the CUPS page log instead of the
# printer's own counter. The log only sees jobs CUPS itself printed, so
# it undercounts and the percentages read optimistically high.
approximate: bool = False
@dataclass
class PageDeliveryCheck:
"""Pages CUPS claims it sent versus pages the printer's counter recorded.
CUPS reports a job as successful once the data leaves the machine, so a page
the printer silently drops still shows up as "printed". The HL-1110 does
exactly that when a page's 600 dpi raster overflows its ~1 MB of memory: it
discards the page, stays READY, and reports no error anywhere. Comparing the
two counters is the only way to notice.
"""
cups_pages: int = 0
printer_pages: int = 0
dropped: int = 0
suspected: bool = False
@dataclass
class USBPortStatus:
"""IEEE 1284 USB printer port status bits."""
paper_empty: bool = False
online: bool = True
error: bool = False
raw_byte: int = 0
@dataclass
class USBResult:
"""Result from a USB PJL query."""
connection: str = "usb"
device: str = ""
product: str = "Brother Laser Printer"
serial: str = ""
status_code: str = ""
display: str = ""
online: str = ""
economode: str = ""
error: str = ""
# Lifetime page count from @PJL INFO PAGECOUNT: the printer's own counter,
# and the authoritative one. Empty when the printer could not be asked
# (CUPS fallback mode), which forces the estimate onto the CUPS page log.
page_count: str = ""
port_status: USBPortStatus | None = None
@dataclass
class SupplyReadings:
"""Parallel SNMP supply tables (descriptions, capacities, current levels).
The three lists are always populated and indexed together, so they travel
as one object rather than three loose fields on NetworkResult.
"""
descriptions: list[str] = field(default_factory=list)
max_values: list[str] = field(default_factory=list)
levels: list[str] = field(default_factory=list)
@dataclass
class NetworkResult:
"""Result from an SNMP network query."""
connection: str = "network"
ip: str = ""
product: str = "Unknown"
serial: str = ""
printer_status: str = ""
device_status: str = ""
display: str = ""
page_count: str = ""
supplies: SupplyReadings = field(default_factory=SupplyReadings)
error: str = ""
@dataclass
class SupplyStatus:
"""Processed supply level info for display."""
color: str
bar_text: str
status_text: str
warning: str
needs_replacement: bool