fair bit o changes! Images moved around, new participation trophy, etc

This commit is contained in:
2026-02-23 21:36:36 -05:00
parent 48979b52f6
commit 77fd097b4a
39 changed files with 2193 additions and 90 deletions

19
.gitignore vendored
View File

@ -1,17 +1,20 @@
\freeze_frame.jpg
qr.jpg
users.json
merged_image.jpg
drawing.png
to_print.zpl
tmp/*
merged_image.png
__pycache__/
**/__pycache__/
*.pyc
*.pyo
freeze_frame.png
image-temp.jpg
# Runtime-generated media files
media-assets/freeze_frame.jpg
media-assets/freeze_frame.png
media-assets/qr.png
media-assets/qr.jpg
media-assets/drawing.png
media-assets/merged_image.png
media-assets/merged_image.jpg
media-assets/to_print.zpl
media-assets/image-temp.jpg
# Config (use config.example.json as template)
config.json

View File

@ -8,8 +8,11 @@
},
"ribbon": {
"width": 450,
"margin": 50
"margin": 50,
"has_cutter": false
},
"darkness": 15,
"background_color": "#bcfef9",
"show_no_qr_ribbon": 0
"show_no_qr_ribbon": 0,
"developer_mode": false
}

1680
kiosk.py.backup Normal file

File diff suppressed because it is too large Load Diff

View File

@ -87,6 +87,7 @@ class Kiosk(tk.Tk):
GlobalVars.print_type = "neither"
GlobalVars.selected_user = None
GlobalVars.ribbon_size = None
GlobalVars.participation_type = None
# Switch to the home screen
self.switch_frame(Screen0)

View File

@ -3,14 +3,15 @@ import tkinter as tk
from tkinter import messagebox
from PIL import Image, ImageTk
import cv2
import os
import threading
import time
from qreader import QReader
from pyzbar.pyzbar import decode as pyzbar_decode
from kiosk.config import CONFIG, BG_COLOR
from kiosk.state import GlobalVars
from kiosk.utils import get_preferred_camera_index
from kiosk.utils import get_preferred_camera_index, MEDIA_DIR
from kiosk.widgets import RoundedLabel
@ -51,7 +52,7 @@ class Screen3(tk.Frame):
self.instruction_label.pack(pady=10)
self.done_button = tk.Button(self, text="Done", command=self.done, height=3, width=30, bg='peach puff', font=GlobalVars.BUTTON_FONT)
self.done_button.place(relx=0.9, rely=0.9, anchor='se')
# Done button starts hidden; revealed after a photo is taken
self.update_image()
@ -105,22 +106,21 @@ class Screen3(tk.Frame):
if ret:
rgb_image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
pil_image = Image.fromarray(rgb_image)
pil_image.save('freeze_frame.jpg')
pil_image.save(os.path.join(MEDIA_DIR, 'freeze_frame.jpg'))
self.photo_taken = True
self.display_taken_photo()
self.is_capturing = False
def display_taken_photo(self):
image = Image.open('freeze_frame.jpg')
image = Image.open(os.path.join(MEDIA_DIR, 'freeze_frame.jpg'))
self.last_photo = ImageTk.PhotoImage(image)
self.canvas.delete("all") # Clear the canvas
self.canvas.create_image(0, 0, image=self.last_photo, anchor='nw')
self.done_button.place(relx=0.9, rely=0.9, anchor='se')
self.button.config(text="Re-Take Photo")
def done(self):
if not self.photo_taken:
messagebox.showwarning("Photo Required", "Please take a photo first")
return
self.release_resources()
from kiosk.screens.description import Screen5
self.master.switch_frame(Screen5)
@ -182,7 +182,6 @@ class Screen14(tk.Frame):
font=GlobalVars.BUTTON_FONT)
self.cancel_button.pack(pady=20)
self.qreader = QReader()
# Prefer the external NexiGo USB webcam when available
self.cap = cv2.VideoCapture(get_preferred_camera_index(CONFIG["camera"]["preferred_name"]))
self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
@ -209,18 +208,17 @@ class Screen14(tk.Frame):
if self.frame_count % 3 == 0:
# Convert to grayscale for QR detection
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
decoded_text = self.qreader.detect_and_decode(image=gray)
results = pyzbar_decode(gray)
if decoded_text:
if isinstance(decoded_text, tuple) and len(decoded_text) > 0:
qr_value = next((item for item in decoded_text if item is not None), None)
if qr_value:
GlobalVars.qr_code_value = qr_value
print(f"QR code scanned: {GlobalVars.qr_code_value}")
self.cap.release()
from kiosk.screens.lookup import Screen12
self.master.switch_frame(Screen12)
return
if results:
qr_value = results[0].data.decode('utf-8')
if qr_value:
GlobalVars.qr_code_value = qr_value
print(f"QR code scanned: {GlobalVars.qr_code_value}")
self.cap.release()
from kiosk.screens.lookup import Screen12
self.master.switch_frame(Screen12)
return
# Display the original frame
rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)

View File

@ -8,6 +8,7 @@ import numpy as np
from kiosk.config import CONFIG, BG_COLOR
from kiosk.state import GlobalVars
from kiosk.utils import MEDIA_DIR
from kiosk.widgets import RoundedLabel
@ -60,16 +61,14 @@ class Screen10(tk.Frame):
def _get_video_file(self):
"""Determine which video to play based on print type and config."""
base_path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
if GlobalVars.print_type == "sticker":
return os.path.join(base_path, "sticker.mp4")
return os.path.join(MEDIA_DIR, "sticker.mp4")
elif GlobalVars.print_type == "ribbon":
has_cutter = CONFIG.get("ribbon", {}).get("has_cutter", False)
if has_cutter:
return os.path.join(base_path, "ribbon-no-cut.mp4")
return os.path.join(MEDIA_DIR, "ribbon-no-cut.mp4")
else:
return os.path.join(base_path, "ribbon-cut.mp4")
return os.path.join(MEDIA_DIR, "ribbon-cut.mp4")
return None
def _update_frame(self):

View File

@ -52,13 +52,18 @@ class ScreenRibbonSize(tk.Frame):
"""Select ribbon size screen."""
def __init__(self, master):
tk.Frame.__init__(self, master, bg=BG_COLOR)
master.add_home_button(self)
# Get ribbon config
self.ribbon_width = CONFIG.get("ribbon", {}).get("width", 450)
self.margin = CONFIG.get("ribbon", {}).get("margin", 50)
self.printable_width = self.ribbon_width - 50 # 25px margin each side
# Spacer to push content toward the bottom
spacer = tk.Frame(self, bg=BG_COLOR)
spacer.pack(fill='both', expand=True)
# Add home button after spacer so it isn't covered
master.add_home_button(self)
# Title
title_label = RoundedLabel(self, text="Select your ribbon size:",
font=GlobalVars.TEXT_FONT, bg='white', padx=40, wraplength=0)

View File

@ -2,9 +2,11 @@
import tkinter as tk
from tkinter import Canvas, filedialog
from PIL import Image, ImageTk
import os
from kiosk.config import CONFIG, BG_COLOR
from kiosk.state import GlobalVars
from kiosk.utils import MEDIA_DIR
from kiosk.widgets import RoundedLabel, DrawingMixin
@ -144,7 +146,7 @@ class Screen4(tk.Frame, DrawingMixin):
self.add_qr_box()
def next(self):
self.drawing.save("drawing.png")
self.drawing.save(os.path.join(MEDIA_DIR, "drawing.png"))
from kiosk.screens.print_flow import Screen13
self.master.switch_frame(Screen13)
@ -267,6 +269,6 @@ class Screen8(tk.Frame, DrawingMixin):
self.import_image_base()
def next(self):
self.drawing.save("drawing.png")
self.drawing.save(os.path.join(MEDIA_DIR, "drawing.png"))
from kiosk.screens.print_flow import Screen13
self.master.switch_frame(Screen13)

View File

@ -9,7 +9,7 @@ import time
from kiosk.config import CONFIG, BG_COLOR
from kiosk.state import GlobalVars
from kiosk.widgets import RoundedLabel
from kiosk.utils import check_ssb_health, restart_ssb_service
from kiosk.utils import check_ssb_health, restart_ssb_service, MEDIA_DIR
class Screen0(tk.Frame):
@ -28,7 +28,7 @@ class Screen0(tk.Frame):
self.grid_columnconfigure(1, weight=1) # For right frame
# Title and buttons on the left side - using custo.png image (scaled to 1/4)
logo_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), 'custo.png')
logo_path = os.path.join(MEDIA_DIR, 'custo.png')
logo_image = Image.open(logo_path)
new_size = (logo_image.width // 4, logo_image.height // 4)
logo_image = logo_image.resize(new_size, Image.LANCZOS)

View File

@ -5,6 +5,7 @@ import os
from kiosk.config import BG_COLOR
from kiosk.state import GlobalVars
from kiosk.utils import MEDIA_DIR
from kiosk.widgets import RoundedLabel
@ -23,15 +24,15 @@ class HaveYouGeneratedTagScreen(tk.Frame):
label.pack(pady=50)
# Deferred imports
def go_to_screen1():
from kiosk.screens.ssb_selection import Screen1
master.switch_frame(Screen1)
def go_to_participation():
from kiosk.screens.participation import ParticipationScreen
master.switch_frame(ParticipationScreen)
def go_to_info1():
master.switch_frame(InfoPage1)
# Buttons
tk.Button(container, text="yea", command=go_to_screen1,
tk.Button(container, text="yea", command=go_to_participation,
height=3, width=20, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(pady=20)
tk.Button(container, text="not yet!", command=go_to_info1,
height=3, width=20, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(pady=20)
@ -53,7 +54,7 @@ class InfoPage1(tk.Frame):
self.grid_rowconfigure(0, weight=1)
# Left side: tags.jpg image
image_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), 'tags.jpg')
image_path = os.path.join(MEDIA_DIR, 'tags.jpg')
img = Image.open(image_path)
# Scale to fit nicely (max 300px height)
max_height = 400
@ -82,7 +83,7 @@ It can print stickers and ribbons to affix to an item you care about."""
nav_frame.grid(row=1, column=0, columnspan=2, sticky='ew', pady=20)
# Countdown number indicator
self.countdown_label = tk.Label(nav_frame, text="8", font=("Helvetica", 20), bg=BG_COLOR, fg='gray40')
self.countdown_label = tk.Label(nav_frame, text="8", font=("Helvetica", 60), bg=BG_COLOR, fg='white')
self.countdown_label.pack(side='left', expand=True)
self.countdown = 8
@ -121,7 +122,7 @@ class InfoPage2(tk.Frame):
self.grid_rowconfigure(0, weight=1)
# Left side: hard.jpg image
image_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), 'hard.jpg')
image_path = os.path.join(MEDIA_DIR, 'hard.jpg')
img = Image.open(image_path)
# Scale to fit nicely (max 300px height)
max_height = 500
@ -151,7 +152,7 @@ by tagging an item you are saying to it, 'you matter, I will take care of you'""
height=2, width=15, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(side='left', padx=40)
# Countdown number indicator
self.countdown_label = tk.Label(nav_frame, text="8", font=("Helvetica", 20), bg=BG_COLOR, fg='gray40')
self.countdown_label = tk.Label(nav_frame, text="8", font=("Helvetica", 60), bg=BG_COLOR, fg='white')
self.countdown_label.pack(side='left', expand=True)
self.countdown = 8
@ -199,9 +200,9 @@ The QR codes contain a Scuttlebutt messageID, scanning it works best at www.cust
text_label.pack()
# Deferred import
def go_to_screen1():
from kiosk.screens.ssb_selection import Screen1
master.switch_frame(Screen1)
def go_to_participation():
from kiosk.screens.participation import ParticipationScreen
master.switch_frame(ParticipationScreen)
# Bottom: Back and Forward buttons (forward delayed)
nav_frame = tk.Frame(self, bg=BG_COLOR)
@ -211,7 +212,7 @@ The QR codes contain a Scuttlebutt messageID, scanning it works best at www.cust
height=2, width=15, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(side='left', padx=40)
# Countdown number indicator
self.countdown_label = tk.Label(nav_frame, text="8", font=("Helvetica", 20), bg=BG_COLOR, fg='gray40')
self.countdown_label = tk.Label(nav_frame, text="8", font=("Helvetica", 60), bg=BG_COLOR, fg='white')
self.countdown_label.pack(side='left', expand=True)
self.countdown = 8
@ -223,7 +224,7 @@ The QR codes contain a Scuttlebutt messageID, scanning it works best at www.cust
self.after(1000, update_countdown)
self.forward_btn = tk.Button(nav_frame, text="Forward →", command=go_to_screen1,
self.forward_btn = tk.Button(nav_frame, text="Forward →", command=go_to_participation,
height=2, width=15, bg='peach puff', font=GlobalVars.BUTTON_FONT)
def show_forward():

View File

@ -1,6 +1,7 @@
"""No QR ribbon flow screens."""
import tkinter as tk
from tkinter import Canvas, filedialog
import os
import subprocess
import traceback
@ -9,6 +10,7 @@ import tozpl
from kiosk.config import CONFIG, BG_COLOR
from kiosk.state import GlobalVars
from kiosk.utils import MEDIA_DIR
from kiosk.widgets import RoundedLabel, DrawingMixin
@ -244,7 +246,7 @@ class NoQRDrawingScreen(tk.Frame, DrawingMixin):
self.import_image_base()
def go_to_print(self):
self.drawing.save("drawing.png")
self.drawing.save(os.path.join(MEDIA_DIR, "drawing.png"))
self.master.switch_frame(NoQRPrintScreen)
@ -360,7 +362,7 @@ class NoQRImportImageScreen(tk.Frame):
def go_to_print(self):
if self.processed_image:
self.processed_image.save("drawing.png")
self.processed_image.save(os.path.join(MEDIA_DIR, "drawing.png"))
# Set ribbon_size to 'import' to signal variable height
GlobalVars.ribbon_size = 'import'
self.master.switch_frame(NoQRPrintScreen)
@ -391,7 +393,7 @@ class NoQRPrintScreen(tk.Frame):
printable_width = ribbon_width - 50 # 25px side margins
# Load the drawing
drawing = Image.open("drawing.png")
drawing = Image.open(os.path.join(MEDIA_DIR, "drawing.png"))
drawing_width, drawing_height = drawing.size
# For fixed sizes, use multiplier; for imports, use actual image dimensions
@ -406,21 +408,22 @@ class NoQRPrintScreen(tk.Frame):
merged_image = Image.new('L', (ribbon_width, total_height), "white")
# Paste drawing 25px in from left, top_margin down from top
merged_image.paste(drawing, (25, top_margin))
merged_image.save("merged_image.png")
merged_image.save(os.path.join(MEDIA_DIR, "merged_image.png"))
# Get the ZPL code for the image
zpl_code = tozpl.print_to_zpl("merged_image.png",
zpl_code = tozpl.print_to_zpl(os.path.join(MEDIA_DIR, "merged_image.png"),
print_width=ribbon_width,
label_length=total_height)
label_length=total_height,
darkness=CONFIG.get("darkness"))
# Save the ZPL
with open("to_print.zpl", "w") as file:
with open(os.path.join(MEDIA_DIR, "to_print.zpl"), "w") as file:
file.write(zpl_code)
# Print to ribbon printer
GlobalVars.last_print_printer = "ribbon"
try:
result = subprocess.Popen(f'lp -d {CONFIG["printers"]["ribbon"]} -o raw to_print.zpl', shell=True, stdout=subprocess.PIPE)
result = subprocess.Popen(f'lp -d {CONFIG["printers"]["ribbon"]} -o raw {os.path.join(MEDIA_DIR, "to_print.zpl")}', shell=True, stdout=subprocess.PIPE)
except:
print('traceback.format_exc():\n%s' % traceback.format_exc())

View File

@ -0,0 +1,358 @@
"""Participation trophy flow screens."""
import tkinter as tk
from tkinter import Canvas
import os
import subprocess
import traceback
from PIL import Image, ImageTk
import tozpl
from kiosk.config import CONFIG, BG_COLOR
from kiosk.state import GlobalVars
from kiosk.utils import MEDIA_DIR
from kiosk.widgets import RoundedLabel, DrawingMixin
class ParticipationScreen(tk.Frame):
"""Do you have time and an item to archive?"""
def __init__(self, master):
tk.Frame.__init__(self, master, bg=BG_COLOR)
master.add_home_button(self)
# Center container
container = tk.Frame(self, bg=BG_COLOR)
container.place(relx=0.5, rely=0.5, anchor='center')
# Question label
label = RoundedLabel(container,
text="Do you have an item currently in-hand that you are prepared to forever immortalize on many people's computers? It can take up to 10 minutes to complete.",
font=GlobalVars.TEXT_FONT, bg='white', padx=20, pady=10, wraplength=700)
label.pack(pady=50)
# Deferred imports
def go_to_screen1():
from kiosk.screens.ssb_selection import Screen1
master.switch_frame(Screen1)
def go_to_sticker_or_ribbon():
master.switch_frame(StickerOrRibbonScreen)
# Buttons
tk.Button(container, text="not right now, let me play", command=go_to_sticker_or_ribbon,
height=3, width=40, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(pady=20)
tk.Button(container, text="I have the item with me and I am prepared to archive",
command=go_to_screen1,
height=3, width=60, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(pady=20)
class StickerOrRibbonScreen(tk.Frame):
"""Would you like a sticker or a ribbon?"""
def __init__(self, master):
tk.Frame.__init__(self, master, bg=BG_COLOR)
master.add_home_button(self)
# Center container
container = tk.Frame(self, bg=BG_COLOR)
container.place(relx=0.5, rely=0.5, anchor='center')
# Question label
label = RoundedLabel(container,
text="Would you like to design a sticker or a ribbon?",
font=GlobalVars.TEXT_FONT, bg='white', padx=20, pady=10, wraplength=600)
label.pack(pady=50)
def go_sticker():
GlobalVars.participation_type = "sticker"
master.switch_frame(ParticipationStickerDrawingScreen)
def go_ribbon():
GlobalVars.participation_type = "ribbon"
master.switch_frame(ParticipationRibbonDrawingScreen)
# Buttons
tk.Button(container, text="sticker", command=go_sticker,
height=3, width=20, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(pady=20)
tk.Button(container, text="ribbon", command=go_ribbon,
height=3, width=20, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(pady=20)
class ParticipationStickerDrawingScreen(tk.Frame, DrawingMixin):
"""Draw a participation sticker (422x343 canvas)."""
def __init__(self, master):
tk.Frame.__init__(self, master, bg=BG_COLOR)
width = 422
height = 343 # 375 - 32 = 343, leaves 32px of participation visible at bottom
# Main container to hold all columns
main_container = tk.Frame(self, bg=BG_COLOR)
main_container.place(relx=0.5, rely=0.5, anchor='center')
# Column 1: Start Over button and instruction text
col1_frame = tk.Frame(main_container, bg=BG_COLOR)
col1_frame.pack(side='left', padx=(15, 25), anchor='n', pady=20)
tk.Button(col1_frame, text="Start Over", command=master.show_warning_dialog,
height=2, width=14, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(pady=(0, 20))
self.label = RoundedLabel(col1_frame, text="Draw your\nsticker :)",
wraplength=150, font=GlobalVars.TEXT_FONT, bg='white')
self.label.pack(pady=5)
# Column 2: Drawing canvas
col2_frame = tk.Frame(main_container, bg=BG_COLOR)
col2_frame.pack(side='left', padx=(10, 30))
self.canvas = Canvas(col2_frame, width=width, height=height, bg='white')
self.canvas.bind("<B1-Motion>", self.draw_line)
self.canvas.bind("<ButtonRelease-1>", self.reset_last_draw)
self.canvas.pack()
# Initialize drawing via mixin (grid_size=1 for stickers)
self.init_drawing(width, height, grid_size=1)
self.draw_size = 4
# Right panel: Pen tools + action buttons
right_panel = tk.Frame(main_container, bg=BG_COLOR)
right_panel.pack(side='left', padx=15)
# Pen tools grid
pen_grid = tk.Frame(right_panel, bg=BG_COLOR)
pen_grid.pack()
tk.Label(pen_grid, text="Pen\nSize", font=GlobalVars.TEXT_FONT, bg=BG_COLOR).grid(
row=0, column=0, rowspan=5, padx=(0, 10), sticky='n', pady=(5, 0))
pen_sizes = [(".", 1), ("*", 2), ("", 3), ("", 4), ("", 5)]
for i, (text, size) in enumerate(pen_sizes):
tk.Button(pen_grid, text=text, command=lambda s=size: self.set_draw_size(s),
height=2, width=5, bg='peach puff').grid(row=i, column=1, pady=2)
tk.Label(pen_grid, text="Pen\nColor", font=GlobalVars.TEXT_FONT, bg=BG_COLOR).grid(
row=6, column=0, rowspan=3, padx=(0, 10), sticky='n', pady=(15, 0))
colors = ['black', 'gray', 'white']
for i, color in enumerate(colors):
tk.Button(pen_grid, height=2, width=5, bg=color,
command=lambda c=color: self.set_draw_color(c)).grid(row=6+i, column=1, pady=2)
# Action buttons
action_frame = tk.Frame(right_panel, bg=BG_COLOR)
action_frame.pack(pady=(20, 0))
tk.Button(action_frame, text="Clear Drawing", command=self.clear_drawing,
height=2, width=14, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(pady=5)
tk.Button(action_frame, text="Done", command=self.go_to_print,
height=2, width=14, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(pady=5)
def go_to_print(self):
self.drawing.save(os.path.join(MEDIA_DIR, "drawing.png"))
self.master.switch_frame(ParticipationPrintScreen)
class ParticipationRibbonDrawingScreen(tk.Frame, DrawingMixin):
"""Draw a participation ribbon (422 x (usable_width-32) canvas)."""
def __init__(self, master):
tk.Frame.__init__(self, master, bg=BG_COLOR)
# Calculate dimensions
ribbon_width_config = CONFIG.get("ribbon", {}).get("width", 450)
printable_width = ribbon_width_config - 50 # 25px margin each side
usable_width = printable_width # e.g. 400
width = 422
height = usable_width - 32 # e.g. 368
# Adaptive display scaling
max_display_height = 700
ideal_scale = max_display_height / height
scale_factor = min(2, max(1, int(ideal_scale)))
display_width = width * scale_factor
display_height = height * scale_factor
# Main container to hold all columns
main_container = tk.Frame(self, bg=BG_COLOR)
main_container.place(relx=0.5, rely=0.5, anchor='center')
# Column 1: Start Over button and instruction text
col1_frame = tk.Frame(main_container, bg=BG_COLOR)
col1_frame.pack(side='left', padx=(5, 10), anchor='n', pady=20)
tk.Button(col1_frame, text="Start Over", command=master.show_warning_dialog,
height=2, width=14, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(pady=(0, 20))
self.label = RoundedLabel(col1_frame, text="Draw your\nribbon :)",
wraplength=150, font=GlobalVars.TEXT_FONT, bg='white')
self.label.pack(pady=5)
# Column 2: Drawing canvas
col2_frame = tk.Frame(main_container, bg=BG_COLOR)
col2_frame.pack(side='left', padx=(5, 10))
self.canvas = Canvas(col2_frame, width=display_width, height=display_height, bg='white')
self.canvas.bind("<B1-Motion>", self.draw_line)
self.canvas.bind("<ButtonRelease-1>", self.reset_last_draw)
self.canvas.pack()
# Initialize drawing via mixin
self.init_drawing(width, height, grid_size=scale_factor)
self.draw_size = 4
# Right panel: Pen tools + action buttons
right_panel = tk.Frame(main_container, bg=BG_COLOR)
right_panel.pack(side='left', padx=5)
# Pen tools grid
pen_grid = tk.Frame(right_panel, bg=BG_COLOR)
pen_grid.pack()
tk.Label(pen_grid, text="Pen\nSize", font=GlobalVars.TEXT_FONT, bg=BG_COLOR).grid(
row=0, column=0, rowspan=5, padx=(0, 10), sticky='n', pady=(5, 0))
pen_sizes = [(".", 1), ("*", 2), ("", 3), ("", 4), ("", 5)]
for i, (text, size) in enumerate(pen_sizes):
tk.Button(pen_grid, text=text, command=lambda s=size: self.set_draw_size(s),
height=2, width=5, bg='peach puff').grid(row=i, column=1, pady=2)
tk.Label(pen_grid, text="Pen\nColor", font=GlobalVars.TEXT_FONT, bg=BG_COLOR).grid(
row=6, column=0, rowspan=3, padx=(0, 10), sticky='n', pady=(15, 0))
colors = ['black', 'gray', 'white']
for i, color in enumerate(colors):
tk.Button(pen_grid, height=2, width=5, bg=color,
command=lambda c=color: self.set_draw_color(c)).grid(row=6+i, column=1, pady=2)
# Action buttons
action_frame = tk.Frame(right_panel, bg=BG_COLOR)
action_frame.pack(pady=(20, 0))
tk.Button(action_frame, text="Clear Drawing", command=self.clear_drawing,
height=2, width=14, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(pady=5)
tk.Button(action_frame, text="Done", command=self.go_to_print,
height=2, width=14, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(pady=5)
def go_to_print(self):
self.drawing.save(os.path.join(MEDIA_DIR, "drawing.png"))
self.master.switch_frame(ParticipationPrintScreen)
class ParticipationPrintScreen(tk.Frame):
"""Print screen for participation sticker/ribbon."""
def __init__(self, master):
tk.Frame.__init__(self, master, bg=BG_COLOR)
master.add_home_button(self)
# Create a container to hold the widgets
container = tk.Frame(self)
container.place(relx=0.5, rely=0.5, anchor='center')
# Instructions
RoundedLabel(container, text="Ready to print!", wraplength=600,
font=GlobalVars.TEXT_FONT, bg='white').grid(row=0, column=0, columnspan=2)
# Print button
tk.Button(container, text="Print", command=self.do_print, height=3, width=30,
bg='peach puff', font=GlobalVars.BUTTON_FONT).grid(row=2, column=0, pady=20)
def do_print(self):
participation_path = os.path.join(MEDIA_DIR, 'participation.png')
if GlobalVars.participation_type == "sticker":
self._print_sticker(participation_path)
else:
self._print_ribbon(participation_path)
from kiosk.screens.completion import Screen10
self.master.switch_frame(Screen10)
def _print_sticker(self, participation_path):
"""Composite and print a participation sticker."""
# Load participation.png as grayscale (600x268)
participation = Image.open(participation_path).convert('L')
# Load user's drawing (422x343) as grayscale
drawing = Image.open(os.path.join(MEDIA_DIR, "drawing.png")).convert('L')
# Full sticker: 600x375 (2"x1.25" at 300 DPI)
merged = Image.new('L', (600, 375), "white")
# Paste participation at bottom (bottom-aligned)
merged.paste(participation, (0, 375 - participation.size[1]))
# Paste drawing at top-left
merged.paste(drawing, (0, 0))
merged.save(os.path.join(MEDIA_DIR, "merged_image.png"))
# Generate ZPL
zpl_code = tozpl.print_to_zpl(os.path.join(MEDIA_DIR, "merged_image.png"),
print_width=600, label_length=375,
darkness=CONFIG.get("darkness"))
with open(os.path.join(MEDIA_DIR, "to_print.zpl"), "w") as f:
f.write(zpl_code)
# Print to sticker printer
GlobalVars.print_type = "sticker"
GlobalVars.last_print_printer = "sticker"
try:
subprocess.Popen(
f'lp -d {CONFIG["printers"]["sticker"]} -o raw {os.path.join(MEDIA_DIR, "to_print.zpl")}',
shell=True, stdout=subprocess.PIPE)
except:
print('traceback.format_exc():\n%s' % traceback.format_exc())
def _print_ribbon(self, participation_path):
"""Composite and print a participation ribbon."""
# Get config values
ribbon_width = CONFIG.get("ribbon", {}).get("width", 450)
margin = CONFIG.get("ribbon", {}).get("margin", 50)
has_cutter = CONFIG.get("ribbon", {}).get("has_cutter", False)
top_margin = margin + (0 if has_cutter else 100)
printable_width = ribbon_width - 50 # 25px margin each side
usable_width = printable_width # e.g. 400
# Load participation.png (600x268)
participation = Image.open(participation_path).convert('L')
# Load user's drawing (422 x (usable_width-32))
drawing = Image.open(os.path.join(MEDIA_DIR, "drawing.png")).convert('L')
# Create 600 x usable_width white image
composite = Image.new('L', (600, usable_width), "white")
# Paste participation.png aligned to bottom (use actual image height)
composite.paste(participation, (0, usable_width - participation.size[1]))
# Paste user's drawing at (0, 0) on top
composite.paste(drawing, (0, 0))
# Rotate 90 degrees → becomes usable_width x 600
rotated = composite.rotate(90, expand=True)
# Create ribbon template
total_height = top_margin + 600 + margin
merged_image = Image.new('L', (ribbon_width, total_height), "white")
# Paste rotated image at (25, top_margin)
merged_image.paste(rotated, (25, top_margin))
merged_image.save(os.path.join(MEDIA_DIR, "merged_image.png"))
# Generate ZPL
zpl_code = tozpl.print_to_zpl(os.path.join(MEDIA_DIR, "merged_image.png"),
print_width=ribbon_width,
label_length=total_height,
darkness=CONFIG.get("darkness"))
with open(os.path.join(MEDIA_DIR, "to_print.zpl"), "w") as f:
f.write(zpl_code)
# Print to ribbon printer
GlobalVars.print_type = "ribbon"
GlobalVars.last_print_printer = "ribbon"
try:
subprocess.Popen(
f'lp -d {CONFIG["printers"]["ribbon"]} -o raw {os.path.join(MEDIA_DIR, "to_print.zpl")}',
shell=True, stdout=subprocess.PIPE)
except:
print('traceback.format_exc():\n%s' % traceback.format_exc())

View File

@ -11,6 +11,7 @@ import addtoDB
from kiosk.config import CONFIG, BG_COLOR
from kiosk.state import GlobalVars
from kiosk.utils import MEDIA_DIR
from kiosk.widgets import RoundedLabel
@ -34,7 +35,7 @@ class Screen13(tk.Frame):
def printy(self):
"""Go ahead and print the thing."""
# Specify the path to your image file
path_to_image = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), 'freeze_frame.jpg')
path_to_image = os.path.join(MEDIA_DIR, 'freeze_frame.jpg')
# Get QR data from the main application
QRX = self.master.QRX
@ -92,12 +93,12 @@ class Screen13(tk.Frame):
qr.make(fit=True)
img = qr.make_image()
whereToSaveQR = 'qr.png'
whereToSaveQR = os.path.join(MEDIA_DIR, 'qr.png')
img.convert('1').save(whereToSaveQR)
# compose image for tag
drawing = Image.open("drawing.png") # drawing
qr_img = Image.open("qr.png").convert("L") # qr
drawing = Image.open(os.path.join(MEDIA_DIR, "drawing.png")) # drawing
qr_img = Image.open(os.path.join(MEDIA_DIR, "qr.png")).convert("L") # qr
#### merge em
@ -109,7 +110,7 @@ class Screen13(tk.Frame):
QRY = 217
# Load and rotate scan-tag 90 degrees clockwise
scan_tag = Image.open("scan-tag.png").convert("L")
scan_tag = Image.open(os.path.join(MEDIA_DIR, "scan-tag.png")).convert("L")
scan_tag_rotated = scan_tag.rotate(-90, expand=True) # -90 = clockwise, 53×357
# 300 DPI printer, 2.25" label = 675 dots
@ -126,7 +127,7 @@ class Screen13(tk.Frame):
scan_tag_y = (sticker_height - scan_tag_rotated.height) // 2 # Center vertically
merged_image.paste(scan_tag_rotated, (scan_tag_x, scan_tag_y))
merged_image.save("merged_image.png")
merged_image.save(os.path.join(MEDIA_DIR, "merged_image.png"))
# if ribbon
if GlobalVars.print_type == "ribbon":
@ -141,7 +142,7 @@ class Screen13(tk.Frame):
drawing_height = int(printable_width * multiplier)
# Load scan-tag image
scan_tag = Image.open("scan-tag.png").convert("L")
scan_tag = Image.open(os.path.join(MEDIA_DIR, "scan-tag.png")).convert("L")
scan_tag_height = scan_tag.height
# Use actual QR dimensions (already correctly sized from generation)
@ -160,22 +161,24 @@ class Screen13(tk.Frame):
# Center QR horizontally within the full ribbon width, below scan-tag
qr_x = (ribbon_width - qr_size) // 2
merged_image.paste(qr_img, (qr_x, top_margin + drawing_height + scan_tag_height))
merged_image.save("merged_image.png")
merged_image.save(os.path.join(MEDIA_DIR, "merged_image.png"))
# Get the ZPL code for the image
if GlobalVars.print_type == "ribbon":
zpl_code = tozpl.print_to_zpl("merged_image.png",
zpl_code = tozpl.print_to_zpl(os.path.join(MEDIA_DIR, "merged_image.png"),
print_width=ribbon_width,
label_length=total_height)
label_length=total_height,
darkness=CONFIG.get("darkness"))
else:
# Sticker: specify dimensions for proper positioning on 2.25" (675 dot @ 300 DPI) label
zpl_code = tozpl.print_to_zpl("merged_image.png",
zpl_code = tozpl.print_to_zpl(os.path.join(MEDIA_DIR, "merged_image.png"),
print_width=sticker_width,
label_length=sticker_height)
label_length=sticker_height,
darkness=CONFIG.get("darkness"))
# save the zpl
# Open the file in write mode
with open("to_print.zpl", "w") as file:
with open(os.path.join(MEDIA_DIR, "to_print.zpl"), "w") as file:
# Write the string to the file
file.write(zpl_code)
@ -183,7 +186,7 @@ class Screen13(tk.Frame):
if GlobalVars.print_type == "sticker":
GlobalVars.last_print_printer = "sticker"
try:
result = subprocess.Popen(f'lp -d {CONFIG["printers"]["sticker"]} -o raw to_print.zpl', shell=True, stdout=subprocess.PIPE, )
result = subprocess.Popen(f'lp -d {CONFIG["printers"]["sticker"]} -o raw {os.path.join(MEDIA_DIR, "to_print.zpl")}', shell=True, stdout=subprocess.PIPE, )
except:
print('traceback.format_exc():\n%s' % traceback.format_exc())
exit()
@ -191,7 +194,7 @@ class Screen13(tk.Frame):
if GlobalVars.print_type == "ribbon":
GlobalVars.last_print_printer = "ribbon"
try:
result = subprocess.Popen(f'lp -d {CONFIG["printers"]["ribbon"]} -o raw to_print.zpl', shell=True, stdout=subprocess.PIPE, )
result = subprocess.Popen(f'lp -d {CONFIG["printers"]["ribbon"]} -o raw {os.path.join(MEDIA_DIR, "to_print.zpl")}', shell=True, stdout=subprocess.PIPE, )
except:
print('traceback.format_exc():\n%s' % traceback.format_exc())
exit()
@ -246,10 +249,10 @@ thanks!"""
def _reprint(self):
"""Re-send the last print job."""
if GlobalVars.last_print_printer and os.path.exists("to_print.zpl"):
if GlobalVars.last_print_printer and os.path.exists(os.path.join(MEDIA_DIR, "to_print.zpl")):
try:
printer_name = CONFIG["printers"][GlobalVars.last_print_printer]
subprocess.Popen(f'lp -d {printer_name} -o raw to_print.zpl',
subprocess.Popen(f'lp -d {printer_name} -o raw {os.path.join(MEDIA_DIR, "to_print.zpl")}',
shell=True, stdout=subprocess.PIPE)
except:
print('traceback.format_exc():\n%s' % traceback.format_exc())

View File

@ -11,3 +11,4 @@ class GlobalVars:
BUTTON_FONT = None
TEXT_FONT = None
last_print_printer = None # "sticker" or "ribbon" - for re-print functionality
participation_type = None # "sticker" or "ribbon" - for participation trophy flow

View File

@ -4,8 +4,11 @@ import re
import glob
import json
import subprocess
from typing import Optional, Tuple
import cv2
MEDIA_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "media-assets")
def get_preferred_camera_index(preferred_name_fragment=None):
"""
@ -77,7 +80,7 @@ def get_preferred_camera_index(preferred_name_fragment=None):
return 0
def check_ssb_health() -> tuple[bool, str | None]:
def check_ssb_health() -> Tuple[bool, Optional[str]]:
"""
Run 'ssb-server whoami' and verify it returns valid JSON with an 'id' field.
Returns: (True, None) on success, (False, error_message) on failure.
@ -105,7 +108,7 @@ def check_ssb_health() -> tuple[bool, str | None]:
return (False, str(e))
def restart_ssb_service() -> tuple[bool, str | None]:
def restart_ssb_service() -> Tuple[bool, Optional[str]]:
"""
Restart ssb-server.service using systemctl --user.
Returns: (True, None) on success, (False, error_message) on failure.

View File

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 63 KiB

View File

Before

Width:  |  Height:  |  Size: 381 KiB

After

Width:  |  Height:  |  Size: 381 KiB

View File

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

Before

Width:  |  Height:  |  Size: 165 KiB

After

Width:  |  Height:  |  Size: 165 KiB

View File

@ -41,10 +41,14 @@ lsusb | grep -i zebra
6. Name it something memorable (e.g., `nylon`)
7. For the driver/model, select **"Raw Queue"** or use command line:
7. **CRITICAL:** For the driver/model, select **"Raw Queue"**. Do NOT use the Zebra CUPS driver — it will intercept ZPL commands instead of passing them through, causing config commands to print blank labels instead of changing settings. Use command line:
```bash
sudo lpadmin -p nylon -E -v usb://Zebra%20Technologies/ZTC%20GX430t?serial=YOUR_SERIAL -m raw
```
If the printer was previously set up with a driver, fix it with:
```bash
sudo lpadmin -p nylon -m raw
```
8. Restart CUPS:
```bash

View File

@ -41,10 +41,14 @@ lsusb | grep -i zebra
6. Name it something memorable (e.g., `sticker`)
7. For the driver/model, select **"Raw Queue"** or use command line:
7. **CRITICAL:** For the driver/model, select **"Raw Queue"**. Do NOT use the Zebra CUPS driver — it will intercept ZPL commands instead of passing them through, causing every `^XA...^XZ` command to print a blank label instead of configuring the printer. Use command line:
```bash
sudo lpadmin -p sticker -E -v usb://Zebra%20Technologies/ZTC%20GX430t?serial=YOUR_SERIAL -m raw
```
If the printer was previously set up with a driver, fix it with:
```bash
sudo lpadmin -p sticker -m raw
```
8. Restart CUPS:
```bash
@ -99,7 +103,7 @@ Test print and adjust:
- If resin sticks or ribbon tears: decrease
- **If checkerboard prints darker than solid black: DECREASE darkness (counterintuitive!)**
See `printer-troubleshooting/darkness-troubleshooting.md` for detailed guidance.
See `darkness-troubleshooting.md` for detailed guidance.
**WARNING:** Avoid `^MD` commands - they stack and can corrupt saved settings. Stick to `~SD` for absolute control.
@ -115,7 +119,24 @@ The printer needs to detect gaps between labels:
The printer will feed several labels as it calibrates the gap sensor.
### Step 7: Test Print
### Step 7: Disable Head-Close Auto-Feed
By default, the printer feeds a label every time the printhead is opened and closed. Disable this to avoid wasting labels:
```bash
# Disable auto-feed on both power-up and head close
echo "^XA^MFN,N^JUS^XZ" | lp -d sticker
```
The `^MFN,N` command:
- `^MF` = Media Feed action control
- First `N` = Power Up action: No Motion
- Second `N` = Head Close action: No Motion
- `^JUS` = Save to EEPROM (persists across power cycles)
Other `^MF` options: `F` = feed to first web, `C` = calibrate, `L` = feed to label length.
### Step 8: Test Print
Simple text test:
@ -136,7 +157,7 @@ This prints a 5-dot border around the label edge. If misaligned, adjust with:
echo "^XA^LS20^JUS^XZ" | lp -d sticker
```
### Step 8: Print from ZPL File
### Step 9: Print from ZPL File
```bash
lp -d sticker your_file.zpl
@ -144,7 +165,11 @@ lp -d sticker your_file.zpl
### Troubleshooting
See `printer-troubleshooting/` directory for detailed guides and test prints.
See `darkness-troubleshooting.md` and `test-*.zpl` files in this directory for detailed guides and test prints.
**Problem: Printer feeds labels when printhead is opened/closed**
- Solution: Disable head-close auto-feed: `echo "^XA^MFN,N^JUS^XZ" | lp -d sticker`
- This saves to EEPROM so it persists across power cycles
**Problem: Ribbon pools/doesn't wind onto uptake spool**
- Solution: Make sure you ran `^MNN,Y` (uptake enabled)
@ -152,7 +177,7 @@ See `printer-troubleshooting/` directory for detailed guides and test prints.
**Problem: Checkerboard/dithered areas darker than solid black**
- Solution: DECREASE darkness (too much heat!)
- See `printer-troubleshooting/darkness-troubleshooting.md`
- See `darkness-troubleshooting.md`
**Problem: Prints are fuzzy or gray**
- Solution: Increase darkness with `~SD` command (not `^MD`)
@ -173,6 +198,11 @@ See `printer-troubleshooting/` directory for detailed guides and test prints.
- Solution: Printer isn't configured as raw queue
- Reconfigure: `sudo lpadmin -p sticker -m raw`
**Problem: ZPL config commands (^MF, ~SD, etc.) print blank labels instead of changing settings**
- Cause: Printer is using the Zebra CUPS driver instead of a raw queue. The driver interprets each `^XA...^XZ` block as a print job rather than passing ZPL commands through to the printer.
- Symptoms: Every command you send prints a blank label; config label (hold feed button 3s) is also blank; `lpoptions -p sticker -l` shows Zebra-specific options like `zeMediaTracking`, `zePrintMode`, etc.
- Solution: Reconfigure as raw queue: `sudo lpadmin -p sticker -m raw`
**Problem: Blank labels after adjusting darkness**
- Solution: May have corrupted ^MD offset - see darkness troubleshooting guide
@ -200,6 +230,10 @@ echo "Setting darkness to 10 (adjust as needed)..."
echo "^XA~SD10^JUS^XZ" | lp -d $PRINTER_NAME
sleep 2
echo "Disabling head-close auto-feed..."
echo "^XA^MFN,N^JUS^XZ" | lp -d $PRINTER_NAME
sleep 2
echo "Configuration complete. Run physical calibration (2-blink method) now."
echo "Test with: echo \"^XA^PW675^LL375~SD10^FO50,50^ADN,36,20^FDTest^FS^XZ\" | lp -d $PRINTER_NAME"
```
@ -216,6 +250,7 @@ echo "Test with: echo \"^XA^PW675^LL375~SD10^FO50,50^ADN,36,20^FDTest^FS^XZ\" |
- `^FD` - Field data (the actual text/content)
- `^FS` - Field separator (end of field)
- `^GB` - Graphic box
- `^MFN,N` - Disable auto-feed on power up and head close
- `^JUS` - Save configuration
- `^JUF` - Factory reset
- `~JC` - Auto-calibration (doesn't always work)

BIN
qr.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1012 B

View File

@ -2,5 +2,4 @@ Pillow
opencv-python
numpy
qrcode
qreader
pyzbar

1
to_print.zpl Normal file

File diff suppressed because one or more lines are too long

View File

@ -11,6 +11,7 @@ class ZPLConveter:
self.compress_hex = False
self.print_width = None
self.label_length = None
self.darkness = None
self.map_code = {1: 'G', 2: 'H', 3: 'I', 4: 'J', 5: 'K', 6: 'L', 7: 'M', 8: 'N',
9: 'O', 10: 'P', 11: 'Q', 12: 'R', 13: 'S', 14: 'T', 15: 'U', 16: 'V',
17: 'W', 18: 'X', 19: 'Y', 20: 'g', 40: 'h', 60: 'i', 80: 'j', 100: 'k',
@ -115,6 +116,8 @@ class ZPLConveter:
def head_doc(self):
zpl = "^XA"
if self.darkness is not None:
zpl += f"~SD{self.darkness}"
if self.print_width:
zpl += f"^PW{self.print_width}"
if self.label_length:
@ -133,11 +136,12 @@ class ZPLConveter:
def set_blackness_limit_percentage(self, percentage):
self.black_limit = (percentage * 768 // 100)
def print_to_zpl(img_path, print_width=None, label_length=None):
def print_to_zpl(img_path, print_width=None, label_length=None, darkness=None):
converter = ZPLConveter()
converter.set_compress_hex(True)
converter.print_width = print_width
converter.label_length = label_length
converter.darkness = darkness
return converter.convert_from_img(img_path)
if __name__ == "__main__":