ai_giga_tcg/printer_receiver.py
2025-04-13 21:11:55 -04:00

312 lines
14 KiB
Python

#!/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...")
result = send(
instructions=print_data,
printer_identifier=self.printer_identifier,
backend_identifier='pyusb',
blocking=True
)
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)