Compare commits

5 Commits

Author SHA1 Message Date
n07070
f52d7493c8 Restructure main class to activate worker and use tasks, print queue,
update Printer
2026-05-21 02:34:12 +02:00
n07070
b48e7072bf Restructure web class to use print queue and tasks 2026-05-21 02:33:51 +02:00
n07070
cbd5d59445 Add worker class 2026-05-21 02:33:40 +02:00
n07070
e7a7c84664 Add printing queue objects 2026-05-21 02:33:25 +02:00
n07070
60f4eff26c Add task objects 2026-05-21 02:33:12 +02:00
6 changed files with 470 additions and 75 deletions

View File

@@ -23,6 +23,7 @@ import sys
import os # For VARS from the shell. import os # For VARS from the shell.
import pprint # To pretty print JSON import pprint # To pretty print JSON
import toml # Used for the config file parsing import toml # Used for the config file parsing
import threading
from flask import ( from flask import (
Flask, Flask,
request, request,
@@ -39,11 +40,15 @@ from flask_limiter.util import get_remote_address
from printer import Printer # The wrapper for the printer class from printer import Printer # The wrapper for the printer class
from raspberry import Raspberry # The Raspberry pi control Class from raspberry import Raspberry # The Raspberry pi control Class
from web import Web # Wrapper for the web routes and API from web import Web # Wrapper for the web routes and API
from print_queue import PrintQueue
from worker import PrintWorker
# Variables
# We create the main Flask object
app = Flask(__name__) app = Flask(__name__)
socketio = SocketIO(app) socketio = SocketIO(app, cors_allowed_origins="*")
# Global variables
ALLOWED_EXTENSIONS = {"png", "jpg", "jpeg", "gif", "webp"} ALLOWED_EXTENSIONS = {"png", "jpg", "jpeg", "gif", "webp"}
# Load the configuration file # Load the configuration file
@@ -116,18 +121,21 @@ rpi = Raspberry(
RASPBERRY_PI_CONNECTED = rpi.is_raspberry_pi() RASPBERRY_PI_CONNECTED = rpi.is_raspberry_pi()
# Queue creation
print_queue = PrintQueue(app)
# Web & API routes # Web & API routes
web = Web(app, print_queue)
web = Web(app, printer) # Start worker thread
worker = PrintWorker(app, print_queue, printer, socketio)
if __name__ == "__main__": worker.start()
app.run(debug=True, use_reloader=False, host="0.0.0.0", ssl_context="adhoc")
limiter = Limiter( limiter = Limiter(
get_remote_address, app=app, default_limits=["1500 per day", "500 per hour"] get_remote_address, app=app, default_limits=["1500 per day", "500 per hour"]
) )
# General routes
@app.route("/") @app.route("/")
@limiter.limit("1/second", override_defaults=False) @limiter.limit("1/second", override_defaults=False)
@@ -144,6 +152,8 @@ def webcam():
app.logger.debug("Loading webcam interface") app.logger.debug("Loading webcam interface")
return render_template("webcam.html") return render_template("webcam.html")
# Form treatement
@app.route("/web/print/sms", methods=["POST"]) @app.route("/web/print/sms", methods=["POST"])
@limiter.limit("6/minute", override_defaults=False) @limiter.limit("6/minute", override_defaults=False)
def web_print_sms(): def web_print_sms():
@@ -299,7 +309,7 @@ def api_print_image():
@app.route("/api/camera/picture", methods=["GET"]) @app.route("/api/camera/picture", methods=["GET"])
def camera_picture(): def camera_picture():
"""Returns a picture taken by the camera""" """Returns a picture taken by the camera on a raspberry pi"""
if RASPBERRY_PI_CONNECTED: if RASPBERRY_PI_CONNECTED:
try: try:
return rpi.camera_picture() return rpi.camera_picture()
@@ -308,6 +318,26 @@ def camera_picture():
else: else:
return jsonify({"message": "No camera present"}), 500 return jsonify({"message": "No camera present"}), 500
@app.route('/api/queue', methods=["GET"])
def api_queue_status():
"""API endpoint for entire queue"""
return jsonify(web.get_queue_state())
@app.route('/api/worker', methods=["GET"])
def api_worker_state():
"""API endpoint to get the worker state"""
return jsonify(worker.current_state())
@app.route('/api/worker/start')
def api_worker_start():
worker.start_worker()
return jsonify(worker.current_state())
@app.route('/api/worker/stop')
def api_worker_stop():
worker.stop_worker()
return jsonify(worker.current_state())
## Authentification ## Authentification
@@ -362,3 +392,6 @@ def camera_status():
socketio.emit("camera_status", True) socketio.emit("camera_status", True)
else: else:
socketio.emit("camera_status", False) socketio.emit("camera_status", False)
if __name__ == "__main__":
app.run(debug=True, use_reloader=False, host="0.0.0.0", ssl_context="adhoc")

127
src/print_queue.py Normal file
View File

@@ -0,0 +1,127 @@
# This class has the method by which we manage the Tasks
# It's a printing queue, so we need to add, remove and get information on where
# the queue is
from collections import deque
# Because actually printing and adding new print job requests happen at
# diffrent times, the print queue is managed by it's own thread.
import threading
from datetime import datetime
from task import TaskType, CutTask
class PrintQueue:
"""
A Double-ended Queue to manage the printing Tasks
"""
def __init__(self, app):
self.app = app
self._queue = deque()
self._lock = threading.Lock()
self._completed_tasks = {} # Store completed task info
self._task_counter = 0
self.app.logger.debug("Created a new PrintQueue")
def __len__(self) -> int:
return len(self._queue)
def enqueue(self, task):
"""Add task to right of the queue and return position"""
with self._lock:
try:
self.app.logger.info("Add task %s to queue ", task.task_id)
self._queue.append(task)
position = self._queue.index(task)
# We return the current position of the task if it was added
self.app.logger.debug("Added a new task %s to the queue at position %s", task.task_id, position)
return position
except Exception as e:
self.app.logger.error("Could not add a task to the queue : %s ", e)
raise e
def dequeue(self):
"""Remove and return next task ( from the left of the queue ) (thread-safe)"""
with self._lock:
return self._queue.popleft() if len(self._queue) > 0 else None
def get_position(self, task):
"""Get current position of task in queue (1-indexed)"""
with self._lock:
if task.task_id in self._completed_tasks:
return None # Task already completed
try:
# Try to get the position of a Task
return self._queue.index(task)
except ValueError as e:
raise e
# end try
def is_empty(self):
"""Check if queue is empty"""
with self._lock:
self.app.logger.debug("Checking if queue is empty")
return len(self._queue) == 0
def get_queue_state(self):
"""Return current queue state"""
with self._lock:
self.app.logger.debug("Return current queue state")
return [{"task_id": t.task_id, "status": t.status} for t in self._queue]
def get_status(self, task_id):
"""Get full status info for a task"""
with self._lock:
if task_id in self._completed_tasks:
return self._completed_tasks[task_id]
# Check in queue if it exists
for index, task in enumerate(self._queue):
if task.task_id == task_id:
# Depending on it's type, we return more info
if task.task_type == TaskType.IMAGE:
return {
"task_id": task_id,
"status": task.status,
"type" : task.task_type,
"position": index,
"in_queue": True,
"content" : task.content,
"signature": task.signature
}
if task.task_type == TaskType.TEXT:
return {
"task_id": task_id,
"status": task.status,
"type" : task.task_type,
"position": index,
"in_queue": True,
"image_path" : str(task.image_path),
"signature" : task.signature,
"process" : str(task.process)
}
if task.task_type == TaskType.CUT:
return {
"task_id": task_id,
"status": task.status,
"type" : task.task_type,
"position": index,
"in_queue": True
}
return None
def mark_completed(self, task_id, task_status):
"""Mark task as completed and remove from queue"""
with self._lock:
self._completed_tasks[task_id] = {
"task_id": task_id,
"status": task_status,
"position": None,
"in_queue": False,
"completed_at": datetime.now().isoformat()
}

View File

@@ -1,12 +1,11 @@
# Importing the module to manage the connection to the printer. # Importing the module to manage the connection to the printer.
import escpos.printer import escpos.printer
import brother_ql # import brother_ql
from time import sleep, gmtime, strftime from time import sleep, gmtime, strftime
import os.path import os.path
from PIL import Image, ImageEnhance, ImageOps from PIL import Image, ImageEnhance, ImageOps
import numpy as np import numpy as np
class Printer(object): class Printer(object):
""" """
# The connection is based on the ESC/POS library # The connection is based on the ESC/POS library
@@ -39,7 +38,9 @@ class Printer(object):
self.usb_args["idProduct"] = self.vendor_id self.usb_args["idProduct"] = self.vendor_id
def check_paper(self) -> bool: def check_paper(self) -> bool:
# Let's check paper status """
On printers that support it, we check that the printer has paper
"""
self.app.logger.debug("Checking paper status...") self.app.logger.debug("Checking paper status...")
self.printer.open(self.usb_args) self.printer.open(self.usb_args)
status = self.printer.paper_status() status = self.printer.paper_status()
@@ -58,10 +59,13 @@ class Printer(object):
self.printer.close() self.printer.close()
def init_printer(self): def init_printer(self):
"""
Check if the printer online ? Is the communication with the printer successfull ?
"""
# Is the printer online ? Is the communication with the printer successfull ? # TODO: This could happen directly when creating a new Printer class
if os.getenv("FLASK_DEBUG"): if os.getenv("FLASK_DEBUG"):
waiting_elapsed = 1 waiting_elapsed = 15
else: else:
waiting_elapsed = 10 waiting_elapsed = 10
@@ -122,10 +126,17 @@ class Printer(object):
return True return True
def print_sms(self, msg, signature="", bold=False): def _print_sms(self, msg, signature="", bold=False):
if not isinstance(msg,str):
self.app.logger.error("It is not possible to print a " + str(type(msg)) + ", only strings.")
raise ValueError
# We make sure that the signature is not something too goofy
clean_msg = str(msg) + "\n" clean_msg = str(msg) + "\n"
clean_signature = str(signature) clean_signature = str(signature)
# Make checks on the size of the message being printed
if len(clean_msg) > 4096: if len(clean_msg) > 4096:
self.app.logger.warning( self.app.logger.warning(
"Could not print message of this length: " + str(len(clean_msg)) "Could not print message of this length: " + str(len(clean_msg))
@@ -146,6 +157,8 @@ class Printer(object):
+ ", needs to be below 256 caracters long." + ", needs to be below 256 caracters long."
) )
# Do the actual printing
# We would pop the next element in the queue here, if it's a sms type
try: try:
self.printer.open(self.usb_args) self.printer.open(self.usb_args)
self.printer.set(align="center", font="a", bold=bold) self.printer.set(align="center", font="a", bold=bold)
@@ -160,14 +173,14 @@ class Printer(object):
self.app.logger.info("Printed text") self.app.logger.info("Printed text")
return True return True
def print_img(self, path, sign="", center=True, process=False): def _print_img(self, path, signature="", center=True, process=False):
clean_signature = str(sign) clean_signature = str(signature)
if len(sign) > 256: if len(signature) > 256:
self.app.logger.warning( self.app.logger.warning(
"Could not print signature of this length: " + str(len(clean_signature)) "Could not print signature of this length: " + str(len(clean_signature))
) )
raise Exception( raise ValueError(
"Could not print signature of this length :" "Could not print signature of this length :"
+ str(len(clean_signature)) + str(len(clean_signature))
+ ", needs to be below 256 caracters long." + ", needs to be below 256 caracters long."
@@ -175,7 +188,7 @@ class Printer(object):
if not os.path.isfile(str(path)): if not os.path.isfile(str(path)):
self.app.logger.warning("File does not exist : " + str(path)) self.app.logger.warning("File does not exist : " + str(path))
raise Exception( raise OSError(
"The file path for this image :" "The file path for this image :"
+ str(path) + str(path)
+ " wasn't found. Please try again." + " wasn't found. Please try again."
@@ -186,34 +199,40 @@ class Printer(object):
if process: if process:
try: try:
self.app.logger.debug("Proccessing the image") self.app.logger.debug("Proccessing the image")
path = process_image(self, path) path = _process_image(self, path)
except Exception as e: except RuntimeError as e:
self.app.logger.error(str(e)) self.app.logger.error("Error while processing the image, aborting print : %s",str(e))
return False raise e
else: else:
self.app.logger.warning("Not proccessing the image") self.app.logger.warning("Not proccessing the image")
try: try:
self.printer.open(self.usb_args) self.printer.open(self.usb_args)
self.printer.image(path, center=center) self.printer.image(path, center=center)
self.printer.textln(signature)
self.printer.close() self.printer.close()
self.app.logger.debug("Printed an image : " + str(path)) self.app.logger.debug("Printed an image : " + str(path))
os.remove(path)
self.app.logger.debug("Removed image : " + str(path))
except Exception as e: except Exception as e:
self.app.logger.error(str(e)) self.app.logger.error(str(e))
raise RuntimeError("Could not print the picture") from e raise RuntimeError("Could not print the picture") from e
finally: finally:
try:
os.remove(path)
except OSError as e:
raise e
self.app.logger.debug("Removed image : " + str(path))
try: try:
self.printer.close() self.printer.close()
except Exception as e: except Exception as e:
self.app.logger.error(str(e)) self.app.logger.error("Could not close the printer connexion %s", str(e))
raise RuntimeError("Could not close the printer connexion. ") from e raise RuntimeError("Could not close the printer connexion. ") from e
self.app.logger.info("Printed a picture") self.app.logger.info("Printed a picture")
return True return True
def qr(self, content): def _qr(self, content):
try: try:
self.printer.open(self.usb_args) self.printer.open(self.usb_args)
self.printer.qr(content, center=True) self.printer.qr(content, center=True)
@@ -226,7 +245,7 @@ class Printer(object):
self.app.logger.info("Printed a QR") self.app.logger.info("Printed a QR")
return True return True
def cut(self): def _cut(self):
try: try:
self.printer.open(self.usb_args) self.printer.open(self.usb_args)
self.printer.cut() self.printer.cut()
@@ -239,8 +258,20 @@ class Printer(object):
self.app.logger.info("Did a cut") self.app.logger.info("Did a cut")
return True return True
def print_task(self, task_type, data):
"""Execute actual print based on task type"""
match (task_type.value):
case ("text"):
self._print_sms(data["txt"],signature=data["sign"])
case ("image"):
self._print_img(data["img"], signature=data["sign"],process=data["process"])
case ("cut"):
self._cut()
case _:
raise RuntimeError("This task type is not supported")
def process_image(self, path):
def _process_image(self, path):
brightness_factor = 1.5 # Used only if image is too dark brightness_factor = 1.5 # Used only if image is too dark
brightness_threshold = 100 # Brightness threshold (0255) brightness_threshold = 100 # Brightness threshold (0255)
contrast_factor = 0.6 # Less than 1.0 = lower contrast contrast_factor = 0.6 # Less than 1.0 = lower contrast
@@ -254,7 +285,7 @@ def process_image(self, path):
original_img = original_img.convert("RGB") original_img = original_img.convert("RGB")
# Resize while maintaining aspect ratio # Resize while maintaining aspect ratio
original_img.thumbnail((max_width, max_height), Image.LANCZOS) original_img.thumbnail((max_width, max_height), Image.Resampling.LANCZOS)
self.app.logger.debug("Resized the image") self.app.logger.debug("Resized the image")
# # Convert to grayscale for dithering # # Convert to grayscale for dithering
@@ -286,18 +317,10 @@ def process_image(self, path):
# contrast_enhancer = ImageEnhance.Contrast(original_img) # contrast_enhancer = ImageEnhance.Contrast(original_img)
# original_img = contrast_enhancer.enhance(contrast_factor) # original_img = contrast_enhancer.enhance(contrast_factor)
# Final resize check
if original_img.height > max_height:
raise ValueError("Image is too long, sorry! Keep it below 575×1000 pixels.")
self.app.logger.error(
"Image is too long, sorry! Keep it below 575×1000 pixels."
)
return False
# Convert to JPEG and save # Convert to JPEG and save
jpeg_path = os.path.splitext(path)[0] + "_processed.jpg" jpeg_path = os.path.splitext(path)[0] + "_processed.jpg"
original_img.save(jpeg_path, format="JPEG", quality=95, optimize=True) original_img.save(jpeg_path, format="JPEG", quality=95, optimize=True)
app.logger.debug("Processed and saved image.") self.app.logger.debug("Processed and saved image.")
return jpeg_path return jpeg_path
@@ -387,6 +410,5 @@ def find_and_parse_borther_ql_printer():
print("No Brother QL printer found") print("No Brother QL printer found")
return None return None
def fint_and_parse_epson_printer(): def fint_and_parse_epson_printer():
pass pass

88
src/task.py Normal file
View File

@@ -0,0 +1,88 @@
# Here we define the types of tasks
# We are using Abstract Base Classes,
# like this we can define types of tasks ( text, images, ... )
# that all work with the same basic options
# The tasks are going to be injected into a Queue.
# It's a usefull way of storing information in our
# program, while making sure that things are indeed printed.
# It's also a way to prevent two concurrent connexions creating
# a access conflict on a single printer, like two people wanting
# to print at the same time.
# We can also delay and store printing tasks until a printer becomes
# available if none is online.
from abc import ABC, abstractmethod
## See https://docs.python.org/3/library/abc.html to learn more about this
# from dataclasses import dataclass
from enum import Enum
import uuid
## You can expand this if you want to take other types of tasks into account
class TaskType(Enum):
TEXT = "text"
IMAGE = "image"
CUT = "cut"
class PrintTask(ABC):
"""
A print task holds information about what we are looking to print.
"""
def __init__(self, task_type):
self.task_id = self._generate_id()
self.task_type = task_type
self.status = "pending" # pending, processing, completed, failed
print("Created a new " + str(self.task_type) + " with ID " + self.task_id)
@abstractmethod
def get_print_data(self):
"""Return data formatted for printer"""
pass
def _generate_id(self):
# Generate unique task ID
return str(uuid.uuid4())
class TextTask(PrintTask):
"""
This tasks represents a texte content, and it's signature.
"""
def __init__(self, content, signature):
super().__init__(TaskType.TEXT)
self.content = content
self.signature = signature
def get_print_data(self):
return { "txt": self.content, "sign": self.signature }
class ImageTask(PrintTask):
"""
This tasks represents a image content ( in the form of it's path ), and it's signature.
"""
def __init__(self, image_path, signature, process):
super().__init__(TaskType.IMAGE)
self.image_path = image_path
self.signature = signature
self.process = process
def get_print_data(self):
# Return image data in printer-compatible format
return { "img": self.image_path, "sign": self.signature, "process" : self.process }
class CutTask(PrintTask):
"""
This class activates the cutter on the printer if it exists
"""
def __init__(self):
super().__init__(TaskType.CUT)
# There is no print data,
# the task existence in itself is indication of what to do
def get_print_data(self):
return None

View File

@@ -1,16 +1,16 @@
from flask import Flask, request, flash from flask import flash
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
from printer import Printer
import time import time
import os import os
from task import TextTask, ImageTask, CutTask
class Web(object): class Web(object):
"""Web is the class that gets all of the information from web calls ( API and Web page ) and provides checks before sending stuff to printing""" """Web is the class that gets all of the information from web calls ( API and Web page ) and provides checks before sending stuff to printing"""
def __init__(self, app, printer): def __init__(self, app, print_queue):
super(Web).__init__() super(Web).__init__()
self.printer = printer self.print_queue = print_queue
self.app = app self.app = app
def print_sms(self, texte, sign: str) -> bool: def print_sms(self, texte, sign: str) -> bool:
@@ -18,13 +18,19 @@ class Web(object):
Get text and a signature, prints the text and cuts after that. Get text and a signature, prints the text and cuts after that.
""" """
self.app.logger.debug("Printing : " + str(texte) + " from " + str(sign)) self.app.logger.debug("Printing : " + str(texte) + " from " + str(sign))
try: try:
self.printer.print_sms(texte, sign) # We create two new tasks and add them directly to the queue
self.printer.cut() # TODO: this might need to be improved because
# !! there is no garantee !! that both the SMS task and the Cut task
# are added back to back, another task could be
# inserted between the two.
sms = self.print_queue.enqueue(TextTask(content=texte, signature=sign))
cut = self.print_queue.enqueue(CutTask())
except Exception as e: except Exception as e:
self.app.logger.error(e) self.app.logger.error(e)
raise RuntimeError("Could not print SMS, " + str(e)) from e raise RuntimeError("Could not add SMS to queue, " + str(e)) from e
self.app.logger.info("Added two new tasks at position %s and %s", sms, cut )
return True return True
def print_image(self, image, sign: str) -> bool: def print_image(self, image, sign: str) -> bool:
@@ -32,28 +38,28 @@ class Web(object):
Get an image and a signature, prints the image and cuts after that. Get an image and a signature, prints the image and cuts after that.
""" """
try: try:
self.upload_file(image) file_uploaded = self.upload_file(image)
except Exception as e: except Exception as e:
self.app.logger.error(e) self.app.logger.error(e)
raise RuntimeError("Could not upload file") from e raise RuntimeError("Could not upload file") from e
if file_uploaded:
self.app.logger.debug("File has been uploaded, printing...") self.app.logger.debug("File has been uploaded, printing...")
try: try:
self.printer.print_img( img = self.print_queue.enqueue(ImageTask(os.path.join(
os.path.join(
self.app.config["UPLOAD_FOLDER"], self.app.config["UPLOAD_FOLDER"],
secure_filename(image.filename), secure_filename(image.filename),
), ),
sign=sign, signature=sign,
process=True, process=True))
)
self.printer.cut() cut = self.print_queue.enqueue(CutTask())
except Exception as e: except Exception as e:
raise RuntimeError("Could not print file") from e raise RuntimeError("Could not add IMG to queue" + str(e)) from e
self.app.logger.info("Added two new tasks at position %s and %s", img, cut )
self.app.logger.debug("Image printed and cut !")
return True return True
def login(self, username: str, password: str) -> bool: def login(self, username: str, password: str) -> bool:
@@ -74,12 +80,13 @@ class Web(object):
def upload_file(self, image) -> bool: def upload_file(self, image) -> bool:
self.app.logger.debug("Validating file") self.app.logger.debug("Validating file")
if image and self.allowed_file(image.filename): if image:
if self.allowed_file(image.filename):
filename = secure_filename(image.filename) filename = secure_filename(image.filename)
self.app.logger.debug("File valid") self.app.logger.debug("File valid")
try: try:
image.save(os.path.join(self.app.config["UPLOAD_FOLDER"], filename)) image.save(os.path.join(self.app.config["UPLOAD_FOLDER"], filename))
except Exception as e: except OSError as e:
self.app.logger.error("Could not save file %s", e) self.app.logger.error("Could not save file %s", e)
return False return False
@@ -89,5 +96,12 @@ class Web(object):
) )
return True return True
else: else:
self.app.logger.error("Could not save file " + str(filename)) self.app.logger.error("Could not save file because the filename is forbidden")
return False return False
else:
self.app.logger.error("Could not save file, it seems to be null ? : " + str(filename))
return False
def get_queue_state(self):
"""Return current queue state"""
return self.print_queue.get_queue_state()

111
src/worker.py Normal file
View File

@@ -0,0 +1,111 @@
# This is the main printing thread
# As explained in the task file, this is where we command
# printing to happen.
import threading
import time
from task import TaskType
class PrintWorker(threading.Thread):
def __init__(self, app, print_queue, printer, socketio=None):
super().__init__(daemon=True)
self.app = app
self.print_queue = print_queue
self.printer = printer
self.socketio = socketio # Optional
self.running = True
self.state = "idle" # idle, printing, dead, drinking-a-beer
self.app.logger.debug("Ho great, I'm alive... I'm ready to work another day...")
def run(self):
"""Background thread that processes queue items"""
self.app.logger.info("Worker started working.")
while True:
if not self.running:
time.sleep(0.2)
continue
# TODO: This could be improved to simply no start
# the while loop as long as the printer is not ready.
# and maybe get out of it when the printer is not ready anymore ?
if not self.printer.ready:
self.app.logger.debug("Waiting for the printer to be ready...")
time.sleep(1)
continue
try:
task = self.print_queue.dequeue()
except Exception as e:
self.app.logger.error("Could not get a new task ! %s ", str(e))
raise RuntimeError("We could not get a new task because " + str(e)) from e
if task:
try:
self.app.logger.info("Got a new task")
self.app.logger.debug("Got task %s", task.task_id)
self.state = "printing"
task.status = "processing"
self._emit_status(task.task_id, "processing")
print_data = task.get_print_data()
try:
self.printer.print_task(task.task_type, print_data)
except Exception as e:
self.app.logger.error("Could not print : %s", str(e))
raise e
task.status = "completed"
self.print_queue.mark_completed(task.task_id, "completed")
self._emit_status(task.task_id, "completed")
except Exception as e:
task.status = "failed"
self.print_queue.mark_completed(task.task_id, "failed")
self._emit_status(task.task_id, "failed", error=str(e))
print(f"Print task {task.task_id} failed: {e}")
else:
# When they are no new tasks to handle, we put the thread to sleep.
self.state = "idle"
time.sleep(0.1)
def _emit_status(self, task_id, status, error=None):
"""Emit status update via Socket.IO if available"""
if not self.socketio:
return
room = f"task_{task_id}"
data = {
"task_id": task_id,
"status": status,
"position": None # Task no longer in queue
}
if error:
data["error"] = error
self.socketio.emit('task_status', data, room=room)
def stop_worker(self):
"""
Give the worker a break
"""
self.state = "drinking-a-beer"
self.running = False
def start_worker(self):
"""
Get the worker back to it
"""
self.state = "idle"
self.running = True
def current_state(self):
"""
Return the worker state
"""
return {
"is_running": self.running,
"queue_size": len(self.print_queue),
"state" : self.state
}