#!/usr/bin/env python3 from flask import Flask, request, jsonify from brother_ql.backends import backend_factory from brother_ql.backends.helpers import discover, status, send from brother_ql.raster import BrotherQLRaster import logging import sys import time from typing import Optional # Configure logging logging.basicConfig( level=logging.DEBUG, # Changed to DEBUG for more detailed logging format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' ) logger = logging.getLogger(__name__) class LabelPrinterReceiver: def __init__(self, printer_identifier: str = "usb://0x04f9:0x20a7"): """Initialize the label printer receiver. Args: printer_identifier: USB identifier for the printer """ logger.info(f"Initializing LabelPrinterReceiver with printer identifier: {printer_identifier}") self.printer_identifier = printer_identifier self.backend = None self._print_lock = False self._last_print_time = None self._print_in_progress = False self._last_known_state = None self._last_state_time = None self._state_cache_timeout = 1.0 # seconds def connect_printer(self) -> bool: """Connect to the printer via USB. Returns: bool: True if connection was successful, False otherwise """ logger.info("Attempting to connect to printer...") try: # Get the backend class from the factory backend_info = backend_factory('pyusb') logger.debug(f"Backend info: {backend_info}") self.backend = backend_info['backend_class'](device_specifier=self.printer_identifier) # Connect to the printer self.backend.write = self.backend.write # This triggers the connection logger.info("Successfully connected to printer") return True except Exception as e: logger.error(f"Error connecting to printer: {e}") self.backend = None return False def is_printer_busy(self) -> bool: """Check if the printer is currently busy. Returns: bool: True if printer is busy, False otherwise """ logger.debug("Checking printer status...") # Check if we have a recent cached state if (self._last_known_state is not None and self._last_state_time is not None and time.time() - self._last_state_time < self._state_cache_timeout): logger.debug("Using cached printer state") return self._last_known_state.get('phase_type') != 'Waiting to receive' try: if not self.backend: logger.warning("No backend connection, attempting to connect...") if not self.connect_printer(): logger.error("Failed to connect to printer") return False # Get actual printer status logger.debug("Requesting printer status...") status_info, raw_data = status(printer_identifier=self.printer_identifier) logger.debug(f"Raw status data: {raw_data}") logger.debug(f"Parsed status info: {status_info}") # Cache the state self._last_known_state = status_info self._last_state_time = time.time() # Check for any errors if status_info.get('errors'): logger.error(f"Printer errors detected: {status_info['errors']}") return False # Check printer phase phase_type = status_info.get('phase_type') logger.debug(f"Printer phase type: {phase_type}") if phase_type == 'Waiting to receive': logger.info("Printer is ready for next job") return False elif phase_type == 'Printing': logger.info("Printer is currently printing") return True else: logger.info(f"Printer is in unknown phase: {phase_type}") return True # Assume busy for unknown phases except Exception as e: logger.error(f"Error getting printer status: {e}") # If we get an error, clear the cached state self._last_known_state = None self._last_state_time = None return False def handle_print_request(self) -> tuple: """Handle incoming print requests. Returns: tuple: (response message, HTTP status code) """ logger.info("Received print request") if self._print_lock: logger.warning("Print lock is active, rejecting request") return {"message": "Another print job is being processed"}, 429 try: logger.info("Acquiring print lock") self._print_lock = True self._print_in_progress = True self._last_print_time = time.time() # Get the print data from the request print_data = request.get_data() if not print_data: logger.error("No print data provided in request") return {"message": "No print data provided"}, 400 logger.info(f"Received print data of size: {len(print_data)} bytes") # Use the send helper which handles the complete print lifecycle logger.info("Sending print data to printer...") try: result = send( instructions=print_data, printer_identifier=self.printer_identifier, backend_identifier='pyusb', blocking=True ) except Exception as e: if "Device not found" in str(e): logger.error("Printer device not found") return {"message": "Printer device not found"}, 500 raise logger.debug(f"Print result: {result}") # Update cached state with the result if 'printer_state' in result: self._last_known_state = result['printer_state'] self._last_state_time = time.time() if result['outcome'] == 'error': logger.error(f"Print error: {result.get('printer_state', {}).get('errors', 'Unknown error')}") return {"message": f"Print error: {result.get('printer_state', {}).get('errors', 'Unknown error')}"}, 500 if not result['did_print']: logger.warning("Print may not have completed successfully") return {"message": "Print sent but completion status unclear"}, 200 logger.info("Print completed successfully") return {"message": "Print request processed successfully"}, 200 except Exception as e: logger.error(f"Error processing print request: {e}") # Clear cached state on error self._last_known_state = None self._last_state_time = None return {"message": f"Error: {str(e)}"}, 500 finally: logger.info("Releasing print lock") self._print_lock = False self._print_in_progress = False def start_server(self, host: str = "0.0.0.0", port: int = 8000): """Start the web server to receive print requests. Args: host: Host to bind the server to port: Port to listen on """ logger.info(f"Starting print server on {host}:{port}") app = Flask(__name__) @app.route('/print', methods=['POST']) def print_endpoint(): logger.info("Received print request at /print endpoint") response, status_code = self.handle_print_request() logger.info(f"Print request completed with status {status_code}: {response}") return jsonify(response), status_code @app.route('/status', methods=['GET']) def status_endpoint(): logger.info("Received status request at /status endpoint") try: if not self.backend: if not self.connect_printer(): logger.error("Failed to connect to printer for status check") return jsonify({"status": "error", "message": "Could not connect to printer"}), 500 # Get actual printer status with retry logic max_retries = 3 retry_delay = 1 # seconds last_error = None for attempt in range(max_retries): try: logger.debug(f"Requesting printer status (attempt {attempt + 1}/{max_retries})...") status_info, raw_data = status(printer_identifier=self.printer_identifier) logger.debug(f"Raw status data: {raw_data}") logger.debug(f"Parsed status info: {status_info}") # Check for any errors if status_info.get('errors'): logger.error(f"Printer errors detected: {status_info['errors']}") return jsonify({ "status": "error", "message": f"Printer errors: {status_info['errors']}" }), 500 # Check printer phase phase_type = status_info.get('phase_type') logger.debug(f"Printer phase type: {phase_type}") # Convert status info to JSON-serializable format serializable_status = { 'status_type': status_info.get('status_type'), 'phase_type': phase_type, 'model_name': status_info.get('model_name'), 'media_type': status_info.get('media_type'), 'media_width': status_info.get('media_width'), 'media_length': status_info.get('media_length'), 'errors': status_info.get('errors', []) } # Add media info if available if 'identified_media' in status_info: media = status_info['identified_media'] serializable_status['media'] = { 'identifier': media.identifier, 'tape_size': media.tape_size, 'form_factor': str(media.form_factor), 'color': str(media.color) } if phase_type == 'Waiting to receive': logger.info("Printer status: ready") return jsonify({ "status": "ready", "phase": phase_type, "details": serializable_status }), 200 elif phase_type == 'Printing': logger.info("Printer status: busy") return jsonify({ "status": "busy", "phase": phase_type, "details": serializable_status }), 200 else: logger.info(f"Printer is in unknown phase: {phase_type}") return jsonify({ "status": "busy", "phase": phase_type, "details": serializable_status }), 200 except Exception as e: last_error = e if "Resource busy" in str(e): logger.warning(f"Printer is busy, retrying in {retry_delay} seconds...") time.sleep(retry_delay) retry_delay *= 2 # Exponential backoff else: logger.error(f"Error checking printer status: {e}") return jsonify({ "status": "error", "message": str(e) }), 500 # If we've exhausted all retries logger.error(f"Failed to get printer status after {max_retries} attempts. Last error: {last_error}") return jsonify({ "status": "error", "message": f"Printer is busy and not responding. Last error: {str(last_error)}" }), 503 # Service Unavailable except Exception as e: logger.error(f"Error checking printer status: {e}") return jsonify({ "status": "error", "message": str(e) }), 500 logger.info(f"Print server started successfully on {host}:{port}") app.run(host=host, port=port) def main(): # Create and start the printer receiver logger.info("Starting printer receiver...") receiver = LabelPrinterReceiver() receiver.start_server() if __name__ == "__main__": try: main() except KeyboardInterrupt: logger.info("Shutting down print server") sys.exit(0)