fair bit o changes! Images moved around, new participation trophy, etc
19
.gitignore
vendored
@ -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
|
||||
|
||||
@ -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
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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():
|
||||
|
||||
@ -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())
|
||||
|
||||
|
||||
358
kiosk/screens/participation.py
Normal 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())
|
||||
@ -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())
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 381 KiB After Width: | Height: | Size: 381 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
BIN
media-assets/participation.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
media-assets/participation2.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 165 KiB After Width: | Height: | Size: 165 KiB |
@ -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
|
||||
@ -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)
|
||||
@ -2,5 +2,4 @@ Pillow
|
||||
opencv-python
|
||||
numpy
|
||||
qrcode
|
||||
qreader
|
||||
pyzbar
|
||||
|
||||
1
to_print.zpl
Normal file
6
tozpl.py
@ -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__":
|
||||
|
||||