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"}'