Files
vet-print-client/client.py

235 lines
9.2 KiB
Python

import os
import json
import time
import threading
import ssl
import paho.mqtt.client as mqtt
from PIL import Image, ImageDraw, ImageFont
from datetime import date
from brother_ql.raster import BrotherQLRaster
from brother_ql.conversion import convert
from brother_ql.backends.helpers import send
from templates import TEMPLATES
# Load configuration from environment
MQTT_HOST = os.getenv('MQTT_HOST', 'localhost')
MQTT_PORT = int(os.getenv('MQTT_PORT', 1883))
MQTT_USE_WEBSOCKET = os.getenv('MQTT_USE_WEBSOCKET', 'False').lower() == 'true'
MQTT_WEBSOCKET_PATH = os.getenv('MQTT_WEBSOCKET_PATH', '/mqtt')
MQTT_TLS_INSECURE = os.getenv('MQTT_TLS_INSECURE', 'False').lower() == 'true'
MQTT_USERNAME = os.getenv('MQTT_USERNAME', '')
MQTT_PASSWORD = os.getenv('MQTT_PASSWORD', '')
MQTT_TOPIC_SUB = os.getenv('MQTT_TOPIC_SUB', 'vet/labels/print')
MQTT_TOPIC_PUB_STATUS = os.getenv('MQTT_TOPIC_PUB_STATUS', 'vet/labels/status')
MQTT_TOPIC_HEARTBEAT = os.getenv('MQTT_TOPIC_HEARTBEAT', 'vet/labels/heartbeat')
PRINTER_DEVICE = os.getenv('PRINTER_DEVICE', '/dev/usb/lp0')
PRINTER_MODEL = os.getenv('PRINTER_MODEL', 'QL-800')
LABEL_SIZE_DEFAULT = os.getenv('LABEL_SIZE_DEFAULT', '29x90')
def print_label(image, printer=PRINTER_DEVICE, model=PRINTER_MODEL, label=LABEL_SIZE_DEFAULT):
"""Print the label directly using brother_ql module"""
import os
try:
# Check if printer device exists
if not os.path.exists(printer):
raise Exception(f"Printer device {printer} not found. Make sure the printer is powered on and connected via USB.")
qlr = BrotherQLRaster(model)
qlr.exception_on_warning = True
# Convert the PIL image to instructions
instructions = convert(qlr=qlr, images=[image], label=label, cut=True)
# Send to printer using linux_kernel backend
print(f"Sending to printer: {printer}")
status = send(instructions=instructions, printer_identifier=f"file://{printer}", backend_identifier='linux_kernel', blocking=True)
return status
except Exception as e:
raise e
def on_connect(client, userdata, flags, rc):
print(f"Connected to MQTT broker at {MQTT_HOST}:{MQTT_PORT} with result code {rc}")
if rc == 0:
print(f"Subscribing to topic: {MQTT_TOPIC_SUB}")
client.subscribe(MQTT_TOPIC_SUB)
print("Subscription successful")
else:
print(f"Failed to connect, result code: {rc}")
def on_message(client, userdata, msg):
try:
raw_payload = msg.payload.decode('utf-8')
except UnicodeDecodeError:
print(f"Received non-UTF-8 message on topic '{msg.topic}': {msg.payload}")
return
print(f"Raw message received on topic '{msg.topic}': {raw_payload}")
job_id = None
try:
payload = json.loads(raw_payload)
print(f"Parsed payload: {payload}")
job_id = payload.get('job_id')
template_id = payload.get('template_id', 'vet_label')
label_size = payload.get('label_size', LABEL_SIZE_DEFAULT)
variables = payload.get('variables', {})
test = payload.get('test', False)
print(f"Processing: template_id={template_id}, label_size={label_size}, test={test}")
# For now, only support one template
if template_id not in TEMPLATES:
raise ValueError(f"Unknown template_id: {template_id}")
template_func = TEMPLATES[template_id]
# TODO: Adjust dimensions based on label_size if needed
# For simplicity, using fixed dimensions
filename = f"/app/output/label_{template_id}.png" if test else None
image = template_func(variables, filename=filename)
if test:
print(f"Test mode: PNG saved as {filename}")
# Publish success status for test mode
success_details = {
"status": "success",
"job_id": job_id,
"template_id": template_id,
"label_size": label_size,
"test_mode": True,
"filename": filename,
"timestamp": time.time()
}
client.publish(MQTT_TOPIC_PUB_STATUS, json.dumps(success_details))
else:
print("Printing label...")
status = print_label(image, label=label_size)
print(f"Print status: {status}")
# Check for print errors
if not status.get('did_print', False):
printer_state = status.get('printer_state', {})
status_type = printer_state.get('status_type', 'Unknown')
media_type = printer_state.get('media_type', 'Unknown')
media_width = printer_state.get('media_width', 'Unknown')
errors = printer_state.get('errors', [])
error_details = {
"status": "error",
"job_id": job_id,
"error": f"Print failed: {status_type}",
"status_type": status_type,
"media_type": media_type,
"media_width": media_width,
"errors": errors,
"outcome": status.get('outcome', 'unknown'),
"original_payload": raw_payload
}
error_msg = f"Print failed: {status_type}. Media: {media_type} ({media_width}mm)"
print(error_msg)
client.publish(MQTT_TOPIC_PUB_STATUS, json.dumps(error_details))
raise Exception(error_msg)
else:
# Print successful - publish success status
success_details = {
"status": "success",
"job_id": job_id,
"template_id": template_id,
"label_size": label_size,
"timestamp": time.time()
}
print(f"Print successful: {template_id}")
client.publish(MQTT_TOPIC_PUB_STATUS, json.dumps(success_details))
except json.JSONDecodeError as e:
error_msg = f"Invalid JSON in message: {e}. Raw payload: {raw_payload}"
print(error_msg)
error_details = {"status": "error", "job_id": job_id, "error": error_msg, "topic": msg.topic}
client.publish(MQTT_TOPIC_PUB_STATUS, json.dumps(error_details))
send_webhook("parse_error", error_details)
error_msg = f"Error processing message: {str(e)}"
print(error_msg)
error_details = {"status": "error", "job_id": job_id, "error": error_msg, "original_payload": raw_payload}
client.publish(MQTT_TOPIC_PUB_STATUS, json.dumps(error_details))
send_webhook("processing_error", error_details)
def heartbeat(client):
while True:
try:
heartbeat_msg = {
"status": "alive",
"timestamp": time.time(),
"host": MQTT_HOST,
"port": MQTT_PORT,
"websocket": MQTT_USE_WEBSOCKET
}
client.publish(MQTT_TOPIC_HEARTBEAT, json.dumps(heartbeat_msg))
time.sleep(30) # Publish every 30 seconds
except Exception as e:
print(f"Error publishing heartbeat: {e}")
time.sleep(5) # Retry sooner on error
def main():
client = mqtt.Client(client_id="printer_client", transport="websockets" if MQTT_USE_WEBSOCKET else "tcp")
client.on_connect = on_connect
client.on_message = on_message
# Configure TLS if using WebSocket
if MQTT_USE_WEBSOCKET:
if MQTT_TLS_INSECURE:
# Disable TLS certificate verification (use with caution)
print("WARNING: TLS certificate verification is disabled!")
client.tls_set(
ca_certs=None,
certfile=None,
keyfile=None,
cert_reqs=ssl.CERT_NONE,
tls_version=ssl.PROTOCOL_TLS,
ciphers=None
)
client.tls_insecure_set(True)
else:
# Use default CA certificates
client.tls_set(
ca_certs=None,
certfile=None,
keyfile=None,
cert_reqs=ssl.CERT_REQUIRED,
tls_version=ssl.PROTOCOL_TLS,
ciphers=None
)
# Set username and password if provided
if MQTT_USERNAME or MQTT_PASSWORD:
client.username_pw_set(MQTT_USERNAME, MQTT_PASSWORD)
# Construct connection string with WebSocket path if needed
print(f"Attempting to connect to MQTT broker at {MQTT_HOST}:{MQTT_PORT}")
if MQTT_USE_WEBSOCKET:
print(f"Using WebSocket transport with path: {MQTT_WEBSOCKET_PATH}")
print(f"TLS Certificate Validation: {'Disabled' if MQTT_TLS_INSECURE else 'Enabled'}")
client.connect(MQTT_HOST, MQTT_PORT, 60)
# Start the network loop in a background thread
client.loop_start()
# Start heartbeat thread
heartbeat_thread = threading.Thread(target=heartbeat, args=(client,))
heartbeat_thread.daemon = True
heartbeat_thread.start()
print("Client is running. Press Ctrl+C to stop.")
try:
while True:
time.sleep(1) # Keep main thread alive
except KeyboardInterrupt:
print("Shutting down...")
client.loop_stop()
client.disconnect()
if __name__ == "__main__":
main()