From 7fd2e0050b8af641549a68e0ec88374dc45762e4 Mon Sep 17 00:00:00 2001 From: James Pattinson Date: Sat, 7 Feb 2026 09:38:36 -0500 Subject: [PATCH] Printer realtime status --- backend/app/main.py | 28 ++++-- backend/app/mqtt_service.py | 167 +++++++++++++++++++++++++++++++++++- docker-compose.yml | 1 + frontend/app.js | 15 +++- 4 files changed, 200 insertions(+), 11 deletions(-) diff --git a/backend/app/main.py b/backend/app/main.py index e31b4ab..8822bfe 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -6,7 +6,7 @@ from datetime import datetime, timedelta from .database import engine, get_db, Base from .models import Drug, DrugVariant, Dispensing, User from .auth import hash_password, verify_password, create_access_token, get_current_user, get_current_admin_user, ACCESS_TOKEN_EXPIRE_MINUTES -from .mqtt_service import publish_label_print +from .mqtt_service import publish_label_print_with_response from pydantic import BaseModel # Create tables @@ -543,19 +543,31 @@ def print_label(label_request: LabelPrintRequest, current_user: User = Depends(g "test": test_mode } - # Publish to MQTT - success = publish_label_print(mqtt_message) + # Publish to MQTT and wait for response + success, response = publish_label_print_with_response(mqtt_message, timeout=10.0) + + print(f"Label print result: success={success}, response={response}") if success: - return LabelPrintResponse( + result = LabelPrintResponse( success=True, - message="Label print request sent successfully" + message=response.get("message", "Label printed successfully") ) + print(f"Returning success response: {result}") + return result else: - raise HTTPException( - status_code=500, - detail="Failed to send label print request to MQTT broker" + # Return error details from printer + # Check both 'message' and 'error' fields for error details + if response: + error_msg = response.get("message") or response.get("error", "Unknown error") + else: + error_msg = "No response from printer" + result = LabelPrintResponse( + success=False, + message=f"Print failed: {error_msg}" ) + print(f"Returning error response: {result}") + return result except Exception as e: raise HTTPException( diff --git a/backend/app/mqtt_service.py b/backend/app/mqtt_service.py index 70a2352..855102d 100644 --- a/backend/app/mqtt_service.py +++ b/backend/app/mqtt_service.py @@ -1,8 +1,12 @@ import os import json import logging -from typing import Optional +import uuid +import time +from typing import Optional, Dict, Any import paho.mqtt.client as mqtt +import threading +import atexit logger = logging.getLogger(__name__) @@ -12,10 +16,169 @@ MQTT_BROKER_PORT = int(os.getenv("MQTT_BROKER_PORT", "1883")) MQTT_USERNAME = os.getenv("MQTT_USERNAME", "") MQTT_PASSWORD = os.getenv("MQTT_PASSWORD", "") MQTT_LABEL_TOPIC = os.getenv("MQTT_LABEL_TOPIC", "vet/labels/print") +MQTT_STATUS_TOPIC = os.getenv("MQTT_STATUS_TOPIC", "vet/labels/status") + +# Store responses by job_id +_response_store: Dict[str, Any] = {} +_response_lock = threading.Lock() + +# Persistent MQTT client +_mqtt_client: Optional[mqtt.Client] = None +_client_connected = threading.Event() +_client_lock = threading.Lock() +_initialization_attempted = False + +def on_connect(client, userdata, flags, rc): + """Callback when connected to broker""" + if rc == 0: + print(f"MQTT: Connected to broker at {MQTT_BROKER_HOST}:{MQTT_BROKER_PORT}") + # Subscribe to status topic + result = client.subscribe(MQTT_STATUS_TOPIC, qos=1) + print(f"MQTT: Subscribed to {MQTT_STATUS_TOPIC}, result={result}") + _client_connected.set() + else: + print(f"MQTT: Failed to connect, return code: {rc}") + _client_connected.clear() + +def on_disconnect(client, userdata, rc): + """Callback when disconnected from broker""" + print(f"MQTT: Disconnected, return code: {rc}") + _client_connected.clear() + +def on_status_message(client, userdata, message): + """Callback for status messages""" + print(f"MQTT: Received message on topic '{message.topic}': {message.payload.decode()[:200]}") + try: + payload = json.loads(message.payload.decode()) + job_id = payload.get("job_id") + if job_id: + with _response_lock: + _response_store[job_id] = payload + print(f"MQTT: Stored response for job {job_id}: {payload.get('status')}") + else: + print(f"MQTT: Message has no job_id: {payload}") + except Exception as e: + print(f"MQTT: Error processing status message: {str(e)}") + +def get_mqtt_client(): + """Get or initialize the persistent MQTT client""" + global _mqtt_client, _initialization_attempted + + with _client_lock: + # If client exists and is connected, return it + if _mqtt_client is not None and _client_connected.is_set(): + return _mqtt_client + + # If we already tried and failed recently, don't retry immediately + if _initialization_attempted and _mqtt_client is None: + return None + + _initialization_attempted = True + + try: + print(f"MQTT: Initializing client connection to {MQTT_BROKER_HOST}:{MQTT_BROKER_PORT}") + + # Clean up old client if exists + if _mqtt_client is not None: + try: + _mqtt_client.loop_stop() + _mqtt_client.disconnect() + except: + pass + + # Create new MQTT client + _mqtt_client = mqtt.Client(client_id=f"drug-inventory-main") + + # Set username and password if provided + if MQTT_USERNAME and MQTT_PASSWORD: + _mqtt_client.username_pw_set(MQTT_USERNAME, MQTT_PASSWORD) + + # Set up callbacks + _mqtt_client.on_connect = on_connect + _mqtt_client.on_disconnect = on_disconnect + _mqtt_client.on_message = on_status_message + + # Connect to broker + _mqtt_client.connect(MQTT_BROKER_HOST, MQTT_BROKER_PORT, 60) + + # Start network loop in background + _mqtt_client.loop_start() + + # Wait for connection (max 3 seconds) + if _client_connected.wait(timeout=3.0): + print("MQTT: Client initialized successfully") + return _mqtt_client + else: + print("MQTT: Connection timeout") + _mqtt_client.loop_stop() + _mqtt_client = None + return None + + except Exception as e: + print(f"MQTT: Error initializing client: {str(e)}") + _mqtt_client = None + return None + +def publish_label_print_with_response(label_data: dict, timeout: float = 5.0) -> tuple[bool, Optional[dict]]: + """ + Publish a label print request to MQTT broker and wait for response + + Args: + label_data: Dictionary containing label print information + timeout: Maximum time to wait for response in seconds + + Returns: + Tuple of (success, response_data) + """ + # Get or initialize MQTT client + client = get_mqtt_client() + if client is None: + print("MQTT: Client not available") + return False, {"status": "error", "message": "MQTT client not connected"} + + job_id = str(uuid.uuid4()) + label_data["job_id"] = job_id + + try: + # Publish message + message = json.dumps(label_data) + result = client.publish(MQTT_LABEL_TOPIC, message, qos=1) + + if result.rc != mqtt.MQTT_ERR_SUCCESS: + print(f"MQTT: Failed to publish, rc={result.rc}") + return False, {"status": "error", "message": "Failed to publish message"} + + print(f"MQTT: Published job {job_id}") + + # Wait for response + start_time = time.time() + while time.time() - start_time < timeout: + with _response_lock: + if job_id in _response_store: + response = _response_store.pop(job_id) + + # Check if print was successful + status = response.get("status", "").lower() + success = status in ["success", "completed", "ok"] + print(f"MQTT: Job {job_id} completed with status: {status}") + return success, response + + time.sleep(0.05) # Check every 50ms + + # Timeout - no response received + print(f"MQTT: Timeout waiting for job {job_id}") + # Clean up in case response arrives late + with _response_lock: + _response_store.pop(job_id, None) + return False, {"status": "timeout", "message": "No response from printer"} + + except Exception as e: + print(f"MQTT: Error in publish_label_print_with_response: {str(e)}") + return False, {"status": "error", "message": str(e)} def publish_label_print(label_data: dict) -> bool: """ - Publish a label print request to MQTT broker + Publish a label print request to MQTT broker (fire and forget) Args: label_data: Dictionary containing label print information diff --git a/docker-compose.yml b/docker-compose.yml index 741dee0..7cab79a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,6 +19,7 @@ services: - MQTT_USERNAME=${MQTT_USERNAME:-} - MQTT_PASSWORD=${MQTT_PASSWORD:-} - MQTT_LABEL_TOPIC=${MQTT_LABEL_TOPIC:-vet/labels/print} + - MQTT_STATUS_TOPIC=${MQTT_STATUS_TOPIC:-vet/labels/status} - LABEL_TEMPLATE_ID=${LABEL_TEMPLATE_ID:-vet_label} - LABEL_SIZE=${LABEL_SIZE:-29x90} - LABEL_TEST=${LABEL_TEST:-false} diff --git a/frontend/app.js b/frontend/app.js index 1cf5f2d..cb3d3ff 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -742,7 +742,20 @@ async function handlePrescribeDrug(e) { console.error('Label printing failed, but drug was dispensed'); showToast('Drug prescribed successfully, but label printing failed', 'warning', 5000); } else { - showToast('Drug prescribed and label sent to printer!', 'success'); + const labelResult = await labelResponse.json(); + console.log('Label print result:', labelResult); + if (labelResult.success) { + showToast('Drug prescribed and label printed successfully!', 'success'); + } else { + // Show as error toast if it contains specific error keywords + const isError = labelResult.message && ( + labelResult.message.includes('not found') || + labelResult.message.includes('error') || + labelResult.message.includes('failed') + ); + const toastType = isError ? 'error' : 'warning'; + showToast('Drug prescribed but ' + labelResult.message, toastType, 5000); + } } document.getElementById('prescribeForm').reset();