0

I originally had a Raspberry Pi running Raspberry Pi OS (Full Desktop) with Apache serving a Flask app on port 80:

from flask import Flask, request
from escpos.printer import Usb

app = Flask(__name__)

# https://python-escpos.readthedocs.io/en/latest/user/methods.html#escpos-class


@app.route("/", methods=["GET"])
def print():
    codigo = request.args.get("codigo", "")
    ref = request.args.get("ref", "")
    pedido = request.args.get("pedido", "")
    operario = request.args.get("oper", "")
    mez = request.args.get("mez", "")
    if not codigo:
        return error("missing codigo", 400)

    mensaje = (
        "Codigo: "
        + codigo
        + "\nReferencia: "
        + ref
        + "\nMezcla: "
        + mez
        + "\nOperador: "
        + operario
        + "\nPedido: "
        + pedido
    )

    p = None
    try:
        p = Usb(0x04B8, 0x0E15)
        p.set(double_height=True, double_width=True)
        p.text(mensaje)
        p.qr(codigo, size=13)
        p.cut()
        p.close()

    except Exception as exc:
        if p:
            p.cut()
            p.close()
        return error(str(exc), 500)

    return (
        {},
        200,
        {"Content-Type": "application/json"},
    )


def error(msg, code):
    return ({"err": msg}, code, {"Content-Type": "application/json"})

To make the USB printer accessible, I added the following udev rule:

SUBSYSTEMS=="usb", ATTRS{idVendor}=="04b8", ATTRS{idProduct}=="0e15", MODE="0666"` in file `/lib/udev/rules.d/99-myusb.rules

...in order for it to not be a forbidden resource.

The setup used Python 3.11 and python-escpos 3.0a8, and everything ran smoothly.

New setup

Years later, I decided to streamline things — running a full desktop OS and Apache felt like overkill. So, I switched to Raspberry Pi OS Lite, serving the Flask app directly with Gunicorn on port 5000.

Same udev rules, but now I manage the app as a systemd service:

[Unit]
Description=EPOS Print Service
After=network.target

[Service]
User=cc
WorkingDirectory=/home/cc/rasp
ExecStart=/home/cc/rasp/venv/bin/gunicorn -w 1 -b 0.0.0.0:5000 app:app
Restart=always

[Install]
WantedBy=multi-user.target

I’m now using Python 3.13, python-escpos 3.1, and pyusb 1.3.1.

The Flask code evolved into a slightly more structured version with basic logging and a small printer wrapper class:

from flask import Flask, request
from escpos.printer import Usb
import os
from datetime import datetime
import random
import string


class Log:
    def __init__(self):
        self.script_dir = os.path.dirname(os.path.abspath(__file__))
        self.log_path = os.path.join(self.script_dir, "printer.log")

    def log(self, message: str):
        """Simple homemade logger that appends messages to printer.log."""
        timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        line = f"{timestamp} | {message}\n"
        try:
            with open(self.log_path, "a", encoding="utf-8") as f:
                f.write(line)
        except Exception as e:
            print(f"Logging failed: {e}")


class Printer:
    id_vendor = 0x04B8
    id_product = 0x0E15
    usb_printer = None

    def __init__(self):
        self.usb_printer = None

    def loadUsbPrinter(self):
        try:
            logging.log("Attempting to connect to printer...")
            self.usb_printer = Usb(self.id_vendor, self.id_product)
            self.usb_printer.set(double_height=True, double_width=True)
            logging.log("Connected to printer successfully.")
        except Exception as e:
            logging.log(f"Printer connection failed: {e}")

    def getUsb(self):
        if not self.usb_printer:
            self.loadUsbPrinter()
        return self.usb_printer


def http_response():
    return ({}, 200, {"Content-Type": "application/json"})


def generateUID():
    return "".join(
        random.choices(
            string.ascii_uppercase + string.ascii_lowercase + string.digits, k=10
        )
    )


app = Flask(__name__)
logging = Log()
printer = Printer()


logging.log(f"--- App started ---")


@app.after_request
def after_request(response):
    response.headers.add("Access-Control-Allow-Origin", "*")
    response.headers.add("Access-Control-Allow-Headers", "Content-Type,Authorization")
    response.headers.add("Access-Control-Allow-Methods", "GET,POST,OPTIONS")
    return response


@app.route("/", methods=["POST"])
def print_codigo():
    data = request.get_json(force=True, silent=True) or {}
    mensaje = data.get("mensaje", "Test")
    codigo = data.get("codigo", "Test")

    uid = generateUID()

    logging.log(f"Print request received: {codigo} | UID: {uid}")

    p = printer.getUsb()

    if not p:
        logging.log("Error: Printer not connected.")
        return http_response()

    try:
        p.text(f"{uid}\n")
        p.qr(codigo, size=11)
        p._raw(b"\n")
        p.text(mensaje)
        p._raw(b"\n")
        p.cut()
    except Exception as exc:
        logging.log(f"Error during printing: {str(exc)}")

    return http_response()

The issue

Everything works most of the time, but occasionally the printer stops partway through a job — it prints the QR code and then hangs. The codigo field is usually something like "a123456789".

Sometimes, instead of a proper QR code, it prints a long stream of random characters before printing the message text. My guess is that it’s failing to encode or send the QR code data correctly to the printer.

Here’s what I’ve tried so far:

  • Changing the number of Gunicorn workers
  • Reinitializing or resetting the USB printer on every request
  • Creating a new Usb() instance per request and closing it immediately after
  • Using with Usb(...): to ensure close() is always called
  • Coming back to apache

None of these fully solved the issue — the problem still appears intermittently.

The fun thing: when the issue happens (printer stuck in qr) it doesn't throw any error to the log, I've also tried adding some time.sleep right before returning but same.

After that, if the printer is called again, it prints properly, with the QR code of the previous ticket at the top.

Has anyone experienced similar behavior with python-escpos and Gunicorn (or Flask) on Raspberry Pi OS Lite?

Any insight, workaround, or debugging tips?

The CURL of the usual requests:

curl 'http://localhost:5000/' \
  -H 'Accept: application/json, text/plain, */*' \
  -H 'Accept-Language: es-ES,es;q=0.9,en;q=0.8' \
  -H 'Connection: keep-alive' \
  -H 'Content-Type: application/json' \
  -H 'Origin: http://10.0.0.9:8080' \
  -H 'Referer: http://10.0.0.9:8080' \
  -H 'Sec-Fetch-Dest: empty' \
  -H 'Sec-Fetch-Mode: cors' \
  -H 'Sec-Fetch-Site: same-site' \
  -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36' \
  -H 'sec-ch-ua: "Google Chrome";v="141", "Not?A_Brand";v="8", "Chromium";v="141"' \
  -H 'sec-ch-ua-mobile: ?0' \
  -H 'sec-ch-ua-platform: "macOS"' \
  --data-raw '{"mensaje":"a13023\nVN150E-G\nEPDM-008/1\nOper: \nPedido: 961","codigo":"a13023"}'
2
  • 1
    What I believe is when sharing a physical resource like a USB printer across multiple processes or threads, especially in a Gunicorn setup. Since Gunicorn uses multiple worker processes, and you're attempting to manage a single, persistent $p$ = printer.getUsb() connection, it leads to race conditions. I would suggest you to Serialize USB access with a global Lock. use something like from threading import Lock and usb_lock = Lock() and use it with usb_lock: p = printer.getUsb() // remaining code inside the lock Commented Oct 30 at 17:53
  • That was a nice shot, the funny thing is that I've gone from the beginning and even a simple python print.py with the example code now its getting crazy. I opened an issue in github as it looks like either a hardware issue, either some extremly odd bug in the library github.com/python-escpos/python-escpos/issues/… Commented Oct 31 at 10:36

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.