fixed shit. now threading works fine and it looks better

This commit is contained in:
2025-09-13 17:29:30 +02:00
parent cf55db126d
commit d9efaa4620
3 changed files with 204 additions and 136 deletions

View File

@@ -727,7 +727,7 @@ class PfandCalculator:
about_uscan, about_uscan,
text=( text=(
"µScan - Der bessere Barcode Scanner\n" "µScan - Der bessere Barcode Scanner\n"
"Version 2.2.2\n" "Version 2.3.5\n"
"µScan erfordert einen UI Reload (Strg+R) in der Root Anwendung\n" "µScan erfordert einen UI Reload (Strg+R) in der Root Anwendung\n"
"µScan ist für mehrere Barcodes gemacht, die in einer kurzen Zeit gescannt werden sollten\n" "µScan ist für mehrere Barcodes gemacht, die in einer kurzen Zeit gescannt werden sollten\n"
"Beachte das µScan eine Kamera benötigt die mindestens 30FPS aufnehmen kann (Process-FPS können eingestellt werden!)" "Beachte das µScan eine Kamera benötigt die mindestens 30FPS aufnehmen kann (Process-FPS können eingestellt werden!)"

View File

@@ -44,22 +44,28 @@ class PfandScanner:
# Collapsible scan list state # Collapsible scan list state
self.scan_list_collapsed = tk.BooleanVar(value=False) self.scan_list_collapsed = tk.BooleanVar(value=False)
# Threading control
self.running = True
self.camera_thread = None
self.process_thread = None
self.init_gui() self.init_gui()
self.init_camera() self.init_camera()
self.queue = queue.Queue() self.queue = queue.Queue()
self.frame_queue = queue.Queue(maxsize=2) # Limit queue size to prevent memory issues
self.pfand_values = { self.pfand_values = {
"EINWEG": 0.25, "EINWEG": 0.25,
"MEHRWEG": 0.15, "MEHRWEG": 0.15,
"DOSE": 0.25, "DOSE": 0.25,
} }
self.update_preview() # Start threads
self.start_threads()
self.window.protocol("WM_DELETE_WINDOW", self.on_closing) self.window.protocol("WM_DELETE_WINDOW", self.on_closing)
self.process_queue()
def setup_styles(self): def setup_styles(self):
"""Setup custom styles for a modern look"""
self.style = ttk.Style() self.style = ttk.Style()
self.style.theme_use('clam') self.style.theme_use('clam')
@@ -84,7 +90,6 @@ class PfandScanner:
self.style.configure('Camera.TFrame', relief='solid', borderwidth=2) self.style.configure('Camera.TFrame', relief='solid', borderwidth=2)
def load_data(self): def load_data(self):
"""Load products from products.json and quantities from quantities.json"""
# Load products # Load products
if os.path.exists(self.products_file): if os.path.exists(self.products_file):
try: try:
@@ -132,10 +137,10 @@ class PfandScanner:
header_frame = ttk.Frame(self.main_frame) header_frame = ttk.Frame(self.main_frame)
header_frame.grid(row=0, column=0, columnspan=3, sticky="ew", pady=(0, 10)) header_frame.grid(row=0, column=0, columnspan=3, sticky="ew", pady=(0, 10))
title_label = ttk.Label(header_frame, text="µScan V2.3.0", style='Title.TLabel') title_label = ttk.Label(header_frame, text="µScan", style='Title.TLabel')
title_label.pack(side="left") title_label.pack(side="left")
status_label = ttk.Label(header_frame, text="Erweiterte Barcode-Scanner", style='Info.TLabel') status_label = ttk.Label(header_frame, text="v2.3.5", style='Info.TLabel')
status_label.pack(side="right") status_label.pack(side="right")
# Control Panel (Left) # Control Panel (Left)
@@ -417,11 +422,25 @@ class PfandScanner:
return frame return frame
def update_preview(self): def start_threads(self):
"""Start camera and processing threads"""
self.camera_thread = threading.Thread(target=self.camera_worker, daemon=True)
self.camera_thread.start()
self.process_thread = threading.Thread(target=self.process_worker, daemon=True)
self.process_thread.start()
# Start UI update loop
self.update_preview()
self.process_queue()
def camera_worker(self):
"""Worker thread for camera capture and processing"""
while self.running:
try: try:
if not hasattr(self, 'cap') or not self.cap.isOpened(): if not hasattr(self, 'cap') or not self.cap.isOpened():
self.window.after(100, self.update_preview) time.sleep(0.1)
return continue
ret, frame = self.cap.read() ret, frame = self.cap.read()
if ret: if ret:
@@ -442,8 +461,74 @@ class PfandScanner:
# Draw barcode outlines # Draw barcode outlines
frame_with_outlines = self.draw_barcode_outline(frame.copy(), self.current_barcodes) frame_with_outlines = self.draw_barcode_outline(frame.copy(), self.current_barcodes)
# Put frame in queue for UI thread
try:
if self.frame_queue.full():
self.frame_queue.get_nowait() # Discard old frame
self.frame_queue.put(frame_with_outlines, timeout=0.1)
except queue.Full:
pass # Skip this frame if queue is full
except Exception as e:
print(f"Fehler in der Kamera-Verarbeitung: {e}")
time.sleep(0.1)
def process_worker(self):
"""Worker thread for barcode processing"""
while self.running:
try:
barcode_data = self.queue.get(timeout=0.1)
now = datetime.now()
# Rate limiting
timestamps = self.barcode_times.get(barcode_data, [])
timestamps = [t for t in timestamps if now - t <= timedelta(seconds=5)]
if len(timestamps) >= 3:
continue
timestamps.append(now)
self.barcode_times[barcode_data] = timestamps
current_time = now.strftime("%H:%M:%S")
pfand_type = "EINWEG" if len(barcode_data) == 13 else "MEHRWEG" if len(barcode_data) == 8 else "DOSE"
deposit = self.pfand_values.get(pfand_type, 0.00)
# Update UI in main thread
self.window.after(0, self.add_to_treeview, current_time, barcode_data, pfand_type, deposit)
# Show product selection dialog
if barcode_data not in self.prompted_barcodes:
self.prompted_barcodes.add(barcode_data)
self.window.after(0, self.show_product_selection, barcode_data)
except queue.Empty:
pass
except Exception as e:
print(f"Fehler in der Warteschlangenverarbeitung: {e}")
def add_to_treeview(self, current_time, barcode_data, pfand_type, deposit):
"""Add item to treeview (called from main thread)"""
# Determine row color
row_count = len(self.tree.get_children())
tag = 'evenrow' if row_count % 2 == 0 else 'oddrow'
self.tree.insert("", 0, values=(current_time, barcode_data, pfand_type, f"{deposit:.2f}"), tags=(tag,))
# Update statistics
self.total_scans += 1
self.update_statistics()
# Sound notification
if self.beep_var.get():
self.window.bell()
def update_preview(self):
"""Update camera preview (called from main thread)"""
try:
# Get frame from queue
frame = self.frame_queue.get_nowait()
# Convert and display # Convert and display
cv2image = cv2.cvtColor(frame_with_outlines, cv2.COLOR_BGR2RGBA) cv2image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGBA)
img = Image.fromarray(cv2image) img = Image.fromarray(cv2image)
# Resize to fit label while maintaining aspect ratio # Resize to fit label while maintaining aspect ratio
@@ -456,13 +541,14 @@ class PfandScanner:
self.camera_label.imgtk = imgtk self.camera_label.imgtk = imgtk
self.camera_label.configure(image=imgtk) self.camera_label.configure(image=imgtk)
# Update session time except queue.Empty:
self.update_statistics() pass # No new frame available
except Exception as e: except Exception as e:
print(f"Fehler in der Video-Vorschau: {e}") print(f"Fehler in der Video-Vorschau: {e}")
self.window.after(10, self.update_preview) # Schedule next update
if self.running:
self.window.after(30, self.update_preview)
def update_statistics(self): def update_statistics(self):
session_time = datetime.now() - self.session_start session_time = datetime.now() - self.session_start
@@ -481,76 +567,96 @@ class PfandScanner:
self.product_win = tk.Toplevel(self.window) self.product_win = tk.Toplevel(self.window)
self.product_win.title("Produkt auswählen") self.product_win.title("Produkt auswählen")
self.product_win.geometry("500x400") self.product_win.geometry("500x400")
self.product_win.minsize(450, 350)
self.product_win.resizable(True, True) self.product_win.resizable(True, True)
# Center the window # Center the window
self.product_win.transient(self.window) self.product_win.transient(self.window)
self.product_win.grab_set() self.product_win.grab_set()
# Configure grid weights # Configure grid weights for proper expansion
self.product_win.columnconfigure(0, weight=1) self.product_win.columnconfigure(0, weight=1)
self.product_win.rowconfigure(1, weight=1) self.product_win.rowconfigure(1, weight=1)
# Header frame # Header frame
header_frame = ttk.Frame(self.product_win, padding="20") header_frame = ttk.Frame(self.product_win, padding="10")
header_frame.grid(row=0, column=0, sticky="ew") header_frame.grid(row=0, column=0, sticky="ew", padx=10, pady=(10, 5))
header_frame.columnconfigure(0, weight=1)
ttk.Label(header_frame, text="Produkt für Barcode auswählen:", style='Heading.TLabel').pack(pady=(0, 5)) ttk.Label(header_frame, text="Produkt für Barcode auswählen:",
ttk.Label(header_frame, text=f"'{barcode_data}'", style='Info.TLabel', font=('Courier', 10)).pack(pady=(0, 10)) style='Heading.TLabel').grid(row=0, column=0, sticky="w")
ttk.Label(header_frame, text=f"'{barcode_data}'",
style='Info.TLabel', font=('Courier', 10)).grid(row=1, column=0, sticky="w", pady=(0, 5))
# Main content frame # Main content frame with scrollable area
content_frame = ttk.Frame(self.product_win, padding="20") content_frame = ttk.Frame(self.product_win)
content_frame.grid(row=1, column=0, sticky="nsew") content_frame.grid(row=1, column=0, sticky="nsew", padx=10, pady=5)
content_frame.columnconfigure(0, weight=1) content_frame.columnconfigure(0, weight=1)
content_frame.rowconfigure(0, weight=1) content_frame.rowconfigure(0, weight=1)
selected_product = tk.StringVar() selected_product = tk.StringVar()
if self.products: # Create a canvas with scrollbar for the product list
# Create scrollable frame for products # Create a canvas with scrollbar for the product list
canvas = tk.Canvas(content_frame, bg='white') canvas = tk.Canvas(content_frame, bg='white', highlightthickness=0)
scrollbar = ttk.Scrollbar(content_frame, orient="vertical", command=canvas.yview) scrollbar = ttk.Scrollbar(content_frame, orient="vertical", command=canvas.yview)
scrollable_frame = ttk.Frame(canvas) canvas.configure(yscrollcommand=scrollbar.set)
# Frame inside canvas
scrollable_frame = ttk.Frame(canvas)
window_id = canvas.create_window((0, 0), window=scrollable_frame, anchor="nw")
# Update scrollregion when frame contents change
scrollable_frame.bind( scrollable_frame.bind(
"<Configure>", "<Configure>",
lambda e: canvas.configure(scrollregion=canvas.bbox("all")) lambda e: canvas.configure(scrollregion=canvas.bbox("all"))
) )
canvas.create_window((0, 0), window=scrollable_frame, anchor="nw") # Resize inner frame width when canvas resizes
canvas.configure(yscrollcommand=scrollbar.set) def on_canvas_configure(event):
canvas.itemconfig(window_id, width=event.width)
canvas.bind("<Configure>", on_canvas_configure)
# Place canvas + scrollbar
canvas.grid(row=0, column=0, sticky="nsew")
scrollbar.grid(row=0, column=1, sticky="ns")
# Add products to scrollable frame
if self.products:
for i, product in enumerate(self.products): for i, product in enumerate(self.products):
current_quantity = self.quantities.get(product, 0) current_quantity = self.quantities.get(product, 0)
price = self.prices.get(product, 0.00) price = self.prices.get(product, 0.00)
product_frame = ttk.Frame(scrollable_frame, padding="5") product_frame = ttk.Frame(scrollable_frame, padding="5")
product_frame.pack(fill="x", pady=2) product_frame.pack(fill="x", pady=2)
product_frame.columnconfigure(1, weight=1)
ttk.Radiobutton( ttk.Radiobutton(
product_frame, product_frame,
text=f"{product}", text=product,
variable=selected_product, variable=selected_product,
value=product value=product
).pack(side="left") ).grid(row=0, column=0, sticky="w")
info_text = f"(Aktuell: {current_quantity}, Preis: €{price:.2f})" info_text = f"Aktuell: {current_quantity}, Preis: €{price:.2f}"
ttk.Label(product_frame, text=info_text, style='Info.TLabel').pack(side="right") ttk.Label(product_frame, text=info_text, style='Info.TLabel').grid(row=0, column=1, sticky="e", padx=(10, 0))
canvas.grid(row=0, column=0, sticky="nsew")
scrollbar.grid(row=0, column=1, sticky="ns")
else: else:
ttk.Label(content_frame, text="Noch keine Produkte definiert.", style='Info.TLabel').pack(pady=20) no_products_label = ttk.Label(scrollable_frame, text="Noch keine Produkte definiert.",
style='Info.TLabel', justify="center")
no_products_label.pack(pady=20)
# Button frame # Button frame at the bottom
button_frame = ttk.Frame(self.product_win, padding="20") button_frame = ttk.Frame(self.product_win, padding="10")
button_frame.grid(row=2, column=0, sticky="ew") button_frame.grid(row=2, column=0, sticky="ew", padx=10, pady=(5, 10))
# Configure button frame columns # Configure button frame columns for proper spacing
button_frame.columnconfigure(0, weight=1) button_frame.columnconfigure(0, weight=1)
button_frame.columnconfigure(1, weight=0) button_frame.columnconfigure(1, weight=0)
button_frame.columnconfigure(2, weight=0) button_frame.columnconfigure(2, weight=0)
button_frame.columnconfigure(3, weight=0)
# Helper functions for buttons
def confirm(): def confirm():
product = selected_product.get() product = selected_product.get()
if product: if product:
@@ -573,7 +679,6 @@ class PfandScanner:
def cancel(): def cancel():
self.product_win.destroy() self.product_win.destroy()
# Add new product button
def add_new_product(): def add_new_product():
new_product = simpledialog.askstring("Neues Produkt", "Name des neuen Produkts:") new_product = simpledialog.askstring("Neues Produkt", "Name des neuen Produkts:")
if new_product and new_product.strip(): if new_product and new_product.strip():
@@ -603,66 +708,39 @@ class PfandScanner:
else: else:
messagebox.showwarning("Produkt existiert", "Dieses Produkt ist bereits vorhanden.") messagebox.showwarning("Produkt existiert", "Dieses Produkt ist bereits vorhanden.")
# Buttons with better layout # Buttons with proper layout
ttk.Button(button_frame, text="Neues Produkt", command=add_new_product).grid(row=0, column=0, sticky="w", padx=5) ttk.Button(button_frame, text="Neues Produkt", command=add_new_product).grid(row=0, column=0, sticky="w", padx=5)
ttk.Button(button_frame, text="Abbrechen", command=cancel).grid(row=0, column=1, padx=5) ttk.Button(button_frame, text="Abbrechen", command=cancel).grid(row=0, column=2, padx=5)
ttk.Button(button_frame, text="Bestätigen", command=confirm).grid(row=0, column=2, padx=5) ttk.Button(button_frame, text="Bestätigen", command=confirm).grid(row=0, column=3, padx=5)
# Bind Enter key to confirm # Bind Enter key to confirm
self.product_win.bind('<Return>', lambda e: confirm()) self.product_win.bind('<Return>', lambda e: confirm())
self.product_win.bind('<Escape>', lambda e: cancel()) self.product_win.bind('<Escape>', lambda e: cancel())
# Set focus to the window
self.product_win.focus_set()
def process_queue(self): def process_queue(self):
try: try:
barcode_data = self.queue.get(timeout=0.1) # This method is now handled by the process_worker thread
now = datetime.now()
# Rate limiting
timestamps = self.barcode_times.get(barcode_data, [])
timestamps = [t for t in timestamps if now - t <= timedelta(seconds=5)]
if len(timestamps) >= 3:
return
timestamps.append(now)
self.barcode_times[barcode_data] = timestamps
current_time = now.strftime("%H:%M:%S")
pfand_type = "EINWEG" if len(barcode_data) == 13 else "MEHRWEG" if len(barcode_data) == 8 else "DOSE"
deposit = self.pfand_values.get(pfand_type, 0.00)
# Determine row color
row_count = len(self.tree.get_children())
tag = 'evenrow' if row_count % 2 == 0 else 'oddrow'
self.tree.insert("", 0, values=(current_time, barcode_data, pfand_type, f"{deposit:.2f}"), tags=(tag,))
# Update statistics
self.total_scans += 1
# Note: Total value is updated in product selection dialog
# Sound notification
if self.beep_var.get():
self.window.bell()
# Show product selection dialog
if barcode_data not in self.prompted_barcodes:
self.prompted_barcodes.add(barcode_data)
self.window.after(0, self.show_product_selection, barcode_data)
except queue.Empty:
pass pass
except Exception as e: except Exception as e:
print(f"Fehler in der Warteschlangenverarbeitung: {e}") print(f"Fehler in der Warteschlangenverarbeitung: {e}")
finally:
# Schedule next check
if self.running:
self.window.after(100, self.process_queue) self.window.after(100, self.process_queue)
def on_closing(self): def on_closing(self):
"""Clean shutdown of the application"""
self.running = False self.running = False
# Wait for camera thread to finish # Wait for camera thread to finish
if self.camera_thread and self.camera_thread.is_alive(): if self.camera_thread and self.camera_thread.is_alive():
self.camera_thread.join(timeout=1.0) self.camera_thread.join(timeout=1.0)
if self.process_thread and self.process_thread.is_alive():
self.process_thread.join(timeout=1.0)
try: try:
if hasattr(self, 'cap') and self.cap and self.cap.isOpened(): if hasattr(self, 'cap') and self.cap and self.cap.isOpened():
self.cap.release() self.cap.release()
@@ -674,9 +752,9 @@ class PfandScanner:
if __name__ != "__main__": if __name__ != "__main__":
def launch_pfand_scanner(): def launch_pfand_scanner():
scanner_window = tk.Toplevel() scanner_window = tk.Toplevel()
PfandScanner(scanner_window, "µScan V2.3.0 - Verbesserte Deutsche Version") PfandScanner(scanner_window, "µScan V2.3.5")
else: else:
# For standalone testing # For standalone testing
root = tk.Tk() root = tk.Tk()
app = PfandScanner(root, "µScan V2.3.0 - Verbesserte Deutsche Version") app = PfandScanner(root, "µScan V2.3.5")
root.mainloop() root.mainloop()

View File

@@ -1,10 +0,0 @@
{
"Flaschen": 7,
"Bierflasche": 2,
"Kasten": 2,
"Dose": 3,
"Plastikflasche": 2,
"Monster": 2,
"Joghurt Glas": 2,
"nigger": 1
}