import os import json import time import threading 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_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) client.publish(MQTT_TOPIC_PUB_STATUS, json.dumps({"status": "error", "job_id": job_id, "error": error_msg, "topic": msg.topic})) except Exception as e: error_msg = f"Error processing message: {str(e)}" print(error_msg) client.publish(MQTT_TOPIC_PUB_STATUS, json.dumps({"status": "error", "job_id": job_id, "error": error_msg, "original_payload": raw_payload})) def heartbeat(client): while True: try: heartbeat_msg = { "status": "alive", "timestamp": time.time(), "host": MQTT_HOST, "port": MQTT_PORT } client.publish(MQTT_TOPIC_HEARTBEAT, json.dumps(heartbeat_msg)) print(f"Published heartbeat: {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.on_connect = on_connect client.on_message = on_message print(f"Attempting to connect to MQTT broker at {MQTT_HOST}:{MQTT_PORT}") 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()