Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 44 additions & 1 deletion backend/app/printer_backends/brother_ql_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,17 @@
from PIL import Image

from app.models.tape import TapeSpec
from app.printer_backends.exceptions import PrintFailedError
from app.printer_backends.exceptions import (
PrinterCoverOpenError,
PrinterOfflineError,
PrintFailedError,
TapeEmptyError,
)
from app.printer_backends.snmp_helper import (
PreflightStatus,
SnmpQueryError,
query_preflight,
)
from app.services.status_block import StatusBlock

_logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -111,6 +121,39 @@ async def print_image(
blocking=True,
)

async def preflight_check(
self,
*,
community: str = "public",
timeout_s: float = 3.0,
) -> PreflightStatus:
"""SNMP-based preflight: hrPrinterStatus + error bitmap + loaded tape.

QL-820NWB nutzt dieselbe Printer MIB wie PT-Series — query_preflight()
funktioniert identisch auf beiden Gerätereihen.

Raises:
PrinterOfflineError: SNMP query failed (host unreachable or timeout)
TapeEmptyError: hrPrinterDetectedErrorState has noPaper bit
PrinterCoverOpenError: hrPrinterDetectedErrorState has doorOpen bit

Does NOT raise TapeMismatchError — caller compares loaded_tape_mm.
"""
try:
preflight = await query_preflight(
self.host,
community=community,
timeout_s=timeout_s,
)
except SnmpQueryError as exc:
raise PrinterOfflineError(f"preflight SNMP failed: {exc}") from exc
Comment on lines +148 to +149

if "noPaper" in preflight.error_flags:
raise TapeEmptyError()
if "doorOpen" in preflight.error_flags:
raise PrinterCoverOpenError()
return preflight

async def query_status(self) -> StatusBlock:
"""QL-Series uses SNMP-Probe via StatusProbeProducer, no synchronous path.

Expand Down
66 changes: 66 additions & 0 deletions backend/tests/unit/printer_backends/test_brother_ql_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,69 @@ async def test_half_cut_ignored_on_ql_with_warning(

def test_half_cut_supported_is_false() -> None:
assert BrotherQLBackend.half_cut_supported is False


# ---------------------------------------------------------------------------
# preflight_check — Phase 1i hotfix (Task 8b)
# ---------------------------------------------------------------------------


@pytest.mark.anyio
async def test_preflight_check_success(monkeypatch: pytest.MonkeyPatch) -> None:
"""preflight_check returns PreflightStatus on healthy printer."""
from app.printer_backends.snmp_helper import PreflightStatus

async def fake_preflight(
host: str, *, community: str = "public", timeout_s: float = 3.0
) -> PreflightStatus:
return PreflightStatus(
hr_printer_status="idle",
loaded_tape_mm=62,
error_flags=[],
)

monkeypatch.setattr("app.printer_backends.brother_ql_backend.query_preflight", fake_preflight)
backend = BrotherQLBackend(host="172.16.51.213", model_id="QL-820NWB")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

Avoid using real LAN IP addresses (such as 172.16.51.213) in test code to prevent internal network details from being deducible. Instead, use RFC 5737 documentation IP addresses (e.g., 192.0.2.11).

Suggested change
backend = BrotherQLBackend(host="172.16.51.213", model_id="QL-820NWB")
backend = BrotherQLBackend(host="192.0.2.11", model_id="QL-820NWB")
References
  1. Privacy violations. Flag any hardcoded LAN IPs, real hostnames, real domains, real tokens, or PII. The maintainer's network must not be deducible from this repository. (link)
  2. Use RFC 5737 documentation IPs and 'example.com' placeholders instead of real LAN IPs, hostnames, or domains in documentation and code to maintain privacy.

result = await backend.preflight_check()
assert result.hr_printer_status == "idle"
assert result.loaded_tape_mm == 62


@pytest.mark.anyio
async def test_preflight_check_raises_tape_empty(monkeypatch: pytest.MonkeyPatch) -> None:
"""preflight_check raises TapeEmptyError when noPaper bit is set."""
from app.printer_backends.exceptions import TapeEmptyError
from app.printer_backends.snmp_helper import PreflightStatus

async def fake_preflight(
host: str, *, community: str = "public", timeout_s: float = 3.0
) -> PreflightStatus:
return PreflightStatus(
hr_printer_status="other",
loaded_tape_mm=None,
error_flags=["noPaper"],
)

monkeypatch.setattr("app.printer_backends.brother_ql_backend.query_preflight", fake_preflight)
backend = BrotherQLBackend(host="x", model_id="QL-820NWB")
with pytest.raises(TapeEmptyError):
await backend.preflight_check()


@pytest.mark.anyio
async def test_preflight_check_raises_offline_on_snmp_error(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""preflight_check raises PrinterOfflineError when SNMP query fails."""
from app.printer_backends.exceptions import PrinterOfflineError
from app.printer_backends.snmp_helper import PreflightStatus, SnmpQueryError

async def fake_preflight(
host: str, *, community: str = "public", timeout_s: float = 3.0
) -> PreflightStatus:
raise SnmpQueryError("timeout")

monkeypatch.setattr("app.printer_backends.brother_ql_backend.query_preflight", fake_preflight)
backend = BrotherQLBackend(host="unreachable", model_id="QL-820NWB")
with pytest.raises(PrinterOfflineError, match="preflight SNMP failed"):
await backend.preflight_check()
Loading