Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
37 changes: 35 additions & 2 deletions src/escpos/escpos.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@
:copyright: Copyright (c) 2012-2017 Bashlinux and python-escpos
:license: MIT
"""

from __future__ import annotations

import logging
import re
import textwrap
import time
Expand Down Expand Up @@ -94,6 +96,7 @@
ImageWidthError,
SetVariableError,
TabPosError,
ValidationError,
)
from .magicencode import MagicEncode

Expand Down Expand Up @@ -1421,26 +1424,55 @@ def panel_buttons(self, enable: bool = True) -> None:
else:
self._raw(PANEL_BUTTON_OFF)

def query_status(self, mode: bytes) -> bytes:
def query_status(self, mode: bytes, raise_not_valid=False) -> bytes:
"""Query the printer for its status.

Returns byte array containing it.

:param mode: Integer that sets the status mode queried to the printer.
- RT_STATUS_ONLINE: Printer status.
- RT_STATUS_PAPER: Paper sensor.
:param raise_not_valid: Default False.
False to log error but do not raise exception.
:raises: :py:exc:`~escpos.exceptions.ValidationError`
"""
self._raw(mode)
status = self._read()
is_valid = self._check_valid_response(status)
if not is_valid:
logging.error("Invalid status data: Couldn't get a valid printer response.")
if raise_not_valid:
raise ValidationError(
"An attemp to read a response value from the device returned an invalid response"
)
return b""
return status

def _check_valid_response(self, resp: bytes) -> bool:
"""Check a byte to be a valid ESC/POS response.

Check if a byte is in the format 0xx1xx10 which is the unique way to
distinguish a possible printer's response from other bytes.

A printer response is obtained after a Real Time Status Query command (DLE EOT).

:param resp: A byte containing the printer's response.
"""
if len(resp) == 0 or len(resp) > 1:
return False
# Check bits 7 or 0 are not 1
is_valid_7_0 = (resp[0] & 0b10000001) == 0b00000000
# Return True if additionally bits 4 and 1 are 1
return is_valid_7_0 and (resp[0] & 0b00010010) == 0b00010010

def is_online(self) -> bool:
"""Query the online status of the printer.

:returns: When online, returns ``True``; ``False`` otherwise.
"""
status = self.query_status(RT_STATUS_ONLINE)
if len(status) == 0:
logging.warning("Unknown online status data")
return False
return not (status[0] & RT_MASK_OFFLINE == RT_MASK_OFFLINE)

Expand All @@ -1454,7 +1486,8 @@ def paper_status(self) -> int: # could be IntEnum
"""
status = self.query_status(RT_STATUS_PAPER)
if len(status) == 0:
return 2
logging.warning("Unknown paper status data")
return 0
if status[0] & RT_MASK_NOPAPER == RT_MASK_NOPAPER:
return 0
if status[0] & RT_MASK_LOWPAPER == RT_MASK_LOWPAPER:
Expand Down
25 changes: 25 additions & 0 deletions src/escpos/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
- `90` = Device not found :py:exc:`~escpos.exceptions.DeviceNotFoundError`
- `91` = USB device not found :py:exc:`~escpos.exceptions.USBNotFoundError`
- `100` = Set variable out of range :py:exc:`~escpos.exceptions.SetVariableError`
- `110` = Invalid response format :py:exc:`~escpos.exceptions.ValidationError`
- `200` = Configuration not found :py:exc:`~escpos.exceptions.ConfigNotFoundError`
- `210` = Configuration syntax error :py:exc:`~escpos.exceptions.ConfigSyntaxError`
- `220` = Configuration section not found :py:exc:`~escpos.exceptions.ConfigSectionMissingError`
Expand Down Expand Up @@ -344,6 +345,30 @@ def __str__(self) -> str:
return f"Set variable out of range ({self.msg})"


class ValidationError(Error):
"""Invalid response format.

An error occurred while validating data.
The return code for this exception is `110`.

inheritance:

.. inheritance-diagram:: escpos.exceptions.ValidationError
:parts: 1

"""

def __init__(self, msg: str = "") -> None:
"""Initialize ValidationError object."""
Error.__init__(self, msg)
self.msg = msg
self.resultcode = 110

def __str__(self) -> str:
"""Return string representation of ValidationError."""
return f"Invalid data ({self.msg})"


# Configuration errors


Expand Down
108 changes: 108 additions & 0 deletions test/test_functions/test_function_status.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
"""tests for status queries

:author: Benito López and the python-escpos developers
:organization: `python-escpos <https://github.com/python-escpos>`_
:copyright: Copyright (c) 2026 `python-escpos <https://github.com/python-escpos>`_
:license: MIT
"""

import logging

import pytest

from escpos.constants import RT_STATUS_ONLINE, RT_STATUS_PAPER
from escpos.exceptions import ValidationError


@pytest.mark.parametrize("resp", [b"\x12", b"\x7e"])
# valid bytes: '0b00010010', '0b01111110'
def test_check_valid_response(driver, resp) -> None:
"""
GIVEN a dummy printer object
WHEN valid bytes are passed to the response checker
THEN check the checks are passed
"""
assert driver._check_valid_response(resp) is True


@pytest.mark.parametrize("resp", [b"", b"\x10\x04", b"\x81\x01\x80\x02\x10"])
# invalid bytes: empty, multiple, 0b10000001, 0b00000001, 0b10000000, 0b00000010, 0b00010000
def test_check_invalid_response(driver, resp) -> None:
"""
GIVEN a dummy printer object
WHEN invalid bytes are passed to the response checker
THEN check the checks are not passed
"""
assert driver._check_valid_response(resp) is False


def test_query_status_error(driver, mocker, caplog) -> None:
"""
GIVEN a dummy printer object
WHEN non valid response is read
THEN raise ValidationError, log error and check return value
"""
mocker.patch("escpos.printer.Dummy._read", return_value=b"")
with pytest.raises(ValidationError):
driver.query_status(RT_STATUS_PAPER, raise_not_valid=True)

with caplog.at_level(logging.ERROR):
status = driver.query_status(RT_STATUS_ONLINE, raise_not_valid=False)

assert "Invalid status data" in caplog.text
assert status == b""


def test_query_is_online_error(driver, mocker, caplog) -> None:
"""
GIVEN a dummy printer object
WHEN non valid response is read
THEN log error and check return value
"""
mocker.patch("escpos.printer.Dummy._read", return_value=b"")
with caplog.at_level(logging.WARNING):
status = driver.is_online()

assert "Unknown online status data" in caplog.text
assert status is False


def test_query_is_online(driver, mocker) -> None:
"""
GIVEN a dummy printer object
WHEN a valid response is read
THEN check return value
"""
mocker.patch("escpos.printer.Dummy._read", return_value=b"\x12")
status = driver.is_online()

assert status is True


def test_paper_status_error(driver, mocker, caplog) -> None:
"""
GIVEN a dummy printer object
WHEN non valid response is read
THEN log error and check return value
"""
mocker.patch("escpos.printer.Dummy._read", return_value=b"")
with caplog.at_level(logging.WARNING):
status = driver.paper_status()

assert "Unknown paper status data" in caplog.text
assert status == 0


@pytest.mark.parametrize("resp, expected", [(b"\x12", 2), (b"\x1e", 1)])
def test_query_paper_status(driver, mocker, resp, expected) -> None:
"""
GIVEN a dummy printer object
WHEN a valid response is read
THEN check return value is the expected
"""
mocker.patch("escpos.printer.Dummy._read", return_value=resp)
status = driver.paper_status()

assert status == expected
Loading