312 lines
14 KiB
Python
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) |