From 3d1c00760959256d282b5e29591606d8100fb30c Mon Sep 17 00:00:00 2001 From: James Pattinson Date: Sat, 7 Feb 2026 06:41:39 -0500 Subject: [PATCH] Label print API --- backend/app/main.py | 62 ++++++++++++++++++++++++++++++++++++- backend/app/mqtt_service.py | 57 ++++++++++++++++++++++++++++++++++ backend/requirements.txt | 1 + docker-compose.yml | 8 +++++ frontend/app.js | 5 +++ 5 files changed, 132 insertions(+), 1 deletion(-) create mode 100644 backend/app/mqtt_service.py diff --git a/backend/app/main.py b/backend/app/main.py index 03aee8a..591174a 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -1,11 +1,12 @@ from fastapi import FastAPI, Depends, HTTPException, APIRouter, status from fastapi.middleware.cors import CORSMiddleware from sqlalchemy.orm import Session -from typing import List, Optional +from typing import List, Optional, Dict, Any 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 pydantic import BaseModel # Create tables @@ -117,6 +118,21 @@ class DispensingResponse(BaseModel): class Config: from_attributes = True +class LabelVariables(BaseModel): + practice_name: str + animal_name: str + drug_name: str + dosage: str + quantity: str + expiry_date: str + +class LabelPrintRequest(BaseModel): + variables: LabelVariables + +class LabelPrintResponse(BaseModel): + success: bool + message: str + # Authentication Routes @router.post("/auth/register", response_model=TokenResponse) def register(user_data: UserCreate, db: Session = Depends(get_db)): @@ -475,5 +491,49 @@ def get_variant_dispensings(variant_id: int, db: Session = Depends(get_db), curr return db.query(Dispensing).filter(Dispensing.drug_variant_id == variant_id).order_by(Dispensing.dispensed_at.desc()).all() +# Label printing endpoint +@router.post("/labels/print", response_model=LabelPrintResponse) +def print_label(label_request: LabelPrintRequest, current_user: User = Depends(get_current_user)): + """ + Print a drug label by publishing an MQTT message + + This endpoint publishes a label print request to the MQTT broker, + which will be picked up by the label printing service. + """ + try: + # Get label configuration from environment + import os + template_id = os.getenv("LABEL_TEMPLATE_ID", "vet_label") + label_size = os.getenv("LABEL_SIZE", "29x90") + test_mode = os.getenv("LABEL_TEST", "false").lower() == "true" + + # Convert the request to the MQTT message format + mqtt_message = { + "template_id": template_id, + "label_size": label_size, + "variables": label_request.variables.dict(), + "test": test_mode + } + + # Publish to MQTT + success = publish_label_print(mqtt_message) + + if success: + return LabelPrintResponse( + success=True, + message="Label print request sent successfully" + ) + else: + raise HTTPException( + status_code=500, + detail="Failed to send label print request to MQTT broker" + ) + + except Exception as e: + raise HTTPException( + status_code=500, + detail=f"Error sending label print request: {str(e)}" + ) + # Include router with /api prefix app.include_router(router) diff --git a/backend/app/mqtt_service.py b/backend/app/mqtt_service.py new file mode 100644 index 0000000..70a2352 --- /dev/null +++ b/backend/app/mqtt_service.py @@ -0,0 +1,57 @@ +import os +import json +import logging +from typing import Optional +import paho.mqtt.client as mqtt + +logger = logging.getLogger(__name__) + +# MQTT Configuration from environment +MQTT_BROKER_HOST = os.getenv("MQTT_BROKER_HOST", "localhost") +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") + +def publish_label_print(label_data: dict) -> bool: + """ + Publish a label print request to MQTT broker + + Args: + label_data: Dictionary containing label print information + + Returns: + True if successful, False otherwise + """ + try: + # Create MQTT client + client = mqtt.Client() + + # Set username and password if provided + if MQTT_USERNAME and MQTT_PASSWORD: + client.username_pw_set(MQTT_USERNAME, MQTT_PASSWORD) + + # Connect to broker + client.connect(MQTT_BROKER_HOST, MQTT_BROKER_PORT, 60) + + # Start network loop to process connection + client.loop_start() + + # Publish message with QoS 0 (fire and forget) + message = json.dumps(label_data) + result = client.publish(MQTT_LABEL_TOPIC, message, qos=0) + + # Stop loop and disconnect + client.loop_stop() + client.disconnect() + + if result.rc == mqtt.MQTT_ERR_SUCCESS: + logger.info(f"Successfully published label print request to {MQTT_LABEL_TOPIC}") + return True + else: + logger.error(f"Failed to publish label print request: {result.rc}") + return False + + except Exception as e: + logger.error(f"Error publishing MQTT message: {str(e)}") + return False diff --git a/backend/requirements.txt b/backend/requirements.txt index 8c18216..eac39a6 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -5,3 +5,4 @@ pydantic==2.5.0 python-multipart==0.0.6 python-jose[cryptography]==3.3.0 passlib[argon2]==1.7.4 +paho-mqtt==1.6.1 diff --git a/docker-compose.yml b/docker-compose.yml index 0139c99..741dee0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,6 +14,14 @@ services: - DATABASE_URL=sqlite:///./data/drugs.db - PUID=1001 - PGID=1001 + - MQTT_BROKER_HOST=${MQTT_BROKER_HOST:-localhost} + - MQTT_BROKER_PORT=${MQTT_BROKER_PORT:-1883} + - MQTT_USERNAME=${MQTT_USERNAME:-} + - MQTT_PASSWORD=${MQTT_PASSWORD:-} + - MQTT_LABEL_TOPIC=${MQTT_LABEL_TOPIC:-vet/labels/print} + - LABEL_TEMPLATE_ID=${LABEL_TEMPLATE_ID:-vet_label} + - LABEL_SIZE=${LABEL_SIZE:-29x90} + - LABEL_TEST=${LABEL_TEST:-false} frontend: image: nginx:alpine diff --git a/frontend/app.js b/frontend/app.js index c68b3a6..cbcfab4 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -324,6 +324,11 @@ function renderDrugs() { ); } + // Sort alphabetically by drug name + drugsToShow = drugsToShow.sort((a, b) => + a.name.toLowerCase().localeCompare(b.name.toLowerCase()) + ); + if (drugsToShow.length === 0) { drugsList.innerHTML = '

No drugs found matching your criteria

'; return;