in a good spot

This commit is contained in:
trav
2026-01-23 12:17:24 -08:00
parent d78f84d8ff
commit cbcef9888b
17 changed files with 1595 additions and 76 deletions

8
.gitignore vendored
View File

@ -9,3 +9,11 @@ merged_image.png
__pycache__/*
freeze_frame.png
image-temp.jpg
# Config (use config.example.json as template)
config.json
# Dependencies
node_modules/
venv/
package-lock.json

3
agents.md Normal file
View File

@ -0,0 +1,3 @@
kiosk.py is the main program here. It's the front-end to a physical kiosk with 2 Zebra GX430t thermal transfer pritners in it.
the whole thing is graphical so it's hard for you to test so you'll need to ask the user to test.

View File

@ -0,0 +1,204 @@
"""
Archive of Migration Items Feature
==================================
This feature was removed from the kiosk application.
Archived on: 2026-01-17
The migration items feature allowed users to enter a ticket number when creating
items, and would save copies of photos and descriptions to a separate directory.
"""
# =============================================================================
# Configuration that was in kiosk.py line 116
# =============================================================================
# MIGRATION_ITEMS_DIR = CONFIG["paths"]["migration_items_dir"]
# =============================================================================
# Global variable that was in GlobalVars class (line 122)
# =============================================================================
# GlobalVars.migration_ticket = ""
# =============================================================================
# Code from start_over() method (lines 167-183)
# This would delete migration files when user started over
# =============================================================================
"""
def start_over(self):
global migration_ticket
global MIGRATION_ITEMS_DIR
# Check if migration_ticket is defined
#if 'migration_ticket' in globals():
# Delete any existing migration files
# if migration_ticket:
# text_file = os.path.join(MIGRATION_ITEMS_DIR, f"{migration_ticket}.txt")
# image_file = os.path.join(MIGRATION_ITEMS_DIR, f"{migration_ticket}.jpg")
# if os.path.exists(text_file):
# os.remove(text_file)
# if os.path.exists(image_file):
# os.remove(image_file)
# Reset migration_ticket
# migration_ticket = ""
"""
# =============================================================================
# Commented Screen15 button in Screen0 (line 222)
# =============================================================================
# tk.Button(right_frame, text="Create Item", command=lambda: master.switch_frame(Screen15), height=4, width=75, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(side='top', pady=30)
# =============================================================================
# Code from Screen3.done() method (lines 667-678)
# This would copy the photo to migration items directory
# =============================================================================
"""
def done(self):
global migration_ticket
global MIGRATION_ITEMS_DIR
# If migration_ticket is set, copy the photo to MIGRATION_ITEMS_DIR
#if migration_ticket:
# source_path = 'freeze_frame.jpg'
# destination_path = os.path.join(MIGRATION_ITEMS_DIR, f"{migration_ticket}.jpg")
# try:
# shutil.copy2(source_path, destination_path)
# print(f"Photo saved as {destination_path}")
#except Exception as e:
# print(f"Error saving photo: {e}")
"""
# =============================================================================
# Code from Screen5.save_info_and_switch() method (lines 920-944)
# This would save text and image to migration items directory
# =============================================================================
"""
def save_info_and_switch(self):
global info_text
global migration_ticket
global MIGRATION_ITEMS_DIR
info_text = self.info_entry.get("1.0", "end-1c")
info_text = info_text.replace('\n', '\\n')
# If migration_ticket is set (not empty), save the text and image to MIGRATION_ITEMS_DIR
#if migration_ticket:
# Save text
# text_file_path = os.path.join(MIGRATION_ITEMS_DIR, f"{migration_ticket}.txt")
# try:
# with open(text_file_path, 'w') as f:
# f.write(info_text)
# print(f"Text saved as {text_file_path}")
# except Exception as e:
# print(f"Error saving text: {e}")
# Save image
# source_image_path = 'freeze_frame.jpg'
# destination_image_path = os.path.join(MIGRATION_ITEMS_DIR, f"{migration_ticket}.jpg")
# try:
# shutil.copy2(source_image_path, destination_image_path)
# print(f"Photo saved as {destination_image_path}")
# except Exception as e:
# print(f"Error saving photo: {e}")
"""
# =============================================================================
# Code from Screen13.printy() method (lines 1353, 1367-1374)
# This would append message ID to migration description file
# =============================================================================
"""
def printy(self):
global print_type, migration_ticket, MIGRATION_ITEMS_DIR
# ... other code ...
# If we have a migration ticket, append the message ID to the description file
#if migration_ticket:
# description_file_path = os.path.join(MIGRATION_ITEMS_DIR, f"{migration_ticket}.txt")
# try:
# with open(description_file_path, 'a') as f:
# f.write(f"\n\n\n{key}")
# print(f"Appended message ID to description file: {description_file_path}")
# except Exception as e:
# print(f"Error appending message ID to description file: {e}")
"""
# =============================================================================
# Screen15 class (lines 1547-1595)
# The main screen for entering migration ticket numbers
# =============================================================================
import tkinter as tk
from tkinter import messagebox
import re
import os
# Note: These would need to be imported from the main kiosk module:
# from kiosk import GlobalVars, Screen1, MIGRATION_ITEMS_DIR
class Screen15(tk.Frame):
"""
Screen for Create Migration Item.
This screen allowed users to enter a ticket number (1-4 digits) to associate
with their item for migration tracking purposes. Files would be saved to
MIGRATION_ITEMS_DIR with the ticket number as the filename.
"""
def __init__(self, master):
tk.Frame.__init__(self, master, bg='#bcfef9')
# Create a main container frame
main_frame = tk.Frame(self, bg='#bcfef9')
main_frame.place(relx=0.5, rely=0.5, anchor='center')
# Add the home button to the main frame (this will be at the top)
master.add_home_button(self)
# Instructions (now in the centered main frame)
tk.Label(main_frame, text="If you're archiving an Item of Migration, please enter the last 3 digits of your ticket number. Otherwise, you can leave this blank.", font=GlobalVars.TEXT_FONT, bg='#bcfef9', wraplength=500).pack(pady=20)
# Text box for entering ticket number (now in the centered main frame)
self.ticket_entry = tk.Entry(main_frame, font=GlobalVars.TEXT_FONT)
self.ticket_entry.pack(pady=20)
# Done button (now in the centered main frame)
tk.Button(main_frame, text="Done", command=self.process_ticket, height=3, width=30, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(pady=20)
# Set focus to the ticket entry field
self.ticket_entry.focus_set()
def process_ticket(self):
global migration_ticket
ticket = self.ticket_entry.get().strip()
if ticket == "":
# If the field is blank, proceed without setting migration_ticket
migration_ticket = ""
self.master.switch_frame(Screen1) # Go to the Scuttlebutt username selection screen
elif re.match(r'^\d{1,4}$', ticket):
if self.is_ticket_available(ticket):
migration_ticket = ticket
self.master.switch_frame(Screen1) # Go to the Scuttlebutt username selection screen
else:
messagebox.showerror("Invalid Input", "This migration number is already in use. Please try a different number.")
else:
messagebox.showerror("Invalid Input", "Please check your number and try again. It should be a 1-4 digit number or left blank.")
def is_ticket_available(self, ticket):
# Get all files in the migration items directory
files = os.listdir(MIGRATION_ITEMS_DIR)
# Strip file extensions and check if the ticket number exists
existing_numbers = [os.path.splitext(f)[0] for f in files]
return ticket not in existing_numbers
# =============================================================================
# Config section that was in config.json and config.example.json
# =============================================================================
"""
{
"paths": {
"migration_items_dir": "/home/trav/Documents/migration_items"
}
}
"""

15
config.example.json Normal file
View File

@ -0,0 +1,15 @@
{
"printers": {
"sticker": "sticker_printer",
"ribbon": "tag-printer"
},
"camera": {
"preferred_name": "NexiGo"
},
"ribbon": {
"width": 450,
"margin": 50
},
"background_color": "#bcfef9",
"show_no_qr_ribbon": 0
}

BIN
custo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

77
install.sh Executable file
View File

@ -0,0 +1,77 @@
#!/bin/bash
# Install script for custodisco-kiosk
# Run this after cloning the repo to install all dependencies
set -e
echo "=== Custodisco Kiosk Installation ==="
echo
# Check for required system tools
echo "Checking system dependencies..."
if ! command -v python3 &> /dev/null; then
echo "ERROR: python3 is not installed. Please install Python 3 first."
exit 1
fi
if ! command -v node &> /dev/null; then
echo "WARNING: node is not installed. Scuttlebutt features won't work."
echo " Install Node.js if you want SSB integration."
fi
if ! command -v npm &> /dev/null; then
echo "WARNING: npm is not installed. Scuttlebutt features won't work."
echo " Install npm if you want SSB integration."
fi
# Install system packages (Debian/Ubuntu)
echo
echo "Installing system dependencies (may require sudo)..."
if command -v apt-get &> /dev/null; then
sudo apt-get update
sudo apt-get install -y jpegoptim jq libzbar0 python3-venv python3-tk
else
echo "WARNING: Not a Debian/Ubuntu system. Please manually install:"
echo " - jpegoptim (image optimization)"
echo " - jq (JSON processing)"
echo " - libzbar0 (QR code reading)"
echo " - python3-venv (virtual environments)"
echo " - python3-tk (tkinter GUI)"
fi
# Create and activate virtual environment
echo
echo "Setting up Python virtual environment..."
python3 -m venv venv
source venv/bin/activate
# Install Python dependencies
echo
echo "Installing Python dependencies..."
pip install --upgrade pip
pip install -r requirements.txt
# Install Node.js dependencies (if npm is available)
if command -v npm &> /dev/null; then
echo
echo "Installing Node.js dependencies..."
npm install
else
echo
echo "Skipping Node.js dependencies (npm not found)"
fi
echo
echo "=== Installation Complete ==="
echo
echo "To run the kiosk:"
echo " ./run.sh"
echo
echo "Or manually:"
echo " source venv/bin/activate"
echo " python kiosk.py"
echo
echo "Notes:"
echo " - Make sure ssb-server is running before using SSB features"
echo

690
kiosk.py
View File

@ -138,7 +138,7 @@ class Kiosk(tk.Tk):
# Initialize fonts
GlobalVars.BUTTON_FONT = tkfont.Font(size=24, family='Helvetica')
GlobalVars.TEXT_FONT = tkfont.Font(size=30, family='Helvetica')
GlobalVars.TEXT_FONT = tkfont.Font(size=30, family='Georgia')
self.switch_frame(Screen0)
@ -187,9 +187,19 @@ class Screen0(tk.Frame):
self.grid_columnconfigure(0, weight=1, minsize=800) # For left frame
self.grid_columnconfigure(1, weight=1) # For right frame
# Title and buttons on the left side
title_label = tk.Label(left_frame, text="Custodisco", bg=BG_COLOR, fg='#800080', font=('Helvetica', 64)) # dark purple color
title_label.pack(side='top', pady=200) # adjust to your needs
# 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.abspath(__file__)), '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)
self.logo_photo = ImageTk.PhotoImage(logo_image)
title_label = tk.Label(left_frame, image=self.logo_photo, bg=BG_COLOR)
title_label.pack(side='top', pady=50)
# Info text below logo
info_label = tk.Label(left_frame, text="this is a touchscreen kiosk\n\n for more information on the project see www.cust.ooo",
bg='white', font=GlobalVars.TEXT_FONT, wraplength=650, justify='center')
info_label.pack(side='top', pady=20)
# Welcome message on the left side
# welcome_text = """"""
@ -201,7 +211,11 @@ class Screen0(tk.Frame):
tk.Button(right_frame, text="Create Item", command=lambda: master.switch_frame(Screen1), height=4, width=75, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(side='top', pady=30)
tk.Button(right_frame, text="Lookup Item", command=lambda: master.switch_frame(Screen14), height=4, width=75, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(side='top', pady=30)
# No QR Ribbon button (only show if enabled in config)
if CONFIG.get("show_no_qr_ribbon", 0) == 1:
tk.Button(right_frame, text="No QR Ribbon", command=lambda: master.switch_frame(NoQRRibbonSizeScreen), height=4, width=75, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(side='top', pady=30)
# Create the quit button
tk.Button(right_frame, text="Quit", command=self.quit_program, height=3, width=30, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(pady=10)
@ -217,7 +231,7 @@ class Screen1(tk.Frame):
master.add_home_button(self)
# Create the label widget with the text
label = tk.Label(self, text="Would you like to associate your item with a Scuttlebutt account?", font=GlobalVars.TEXT_FONT)
label = tk.Label(self, text="Would you like to associate your item with a Scuttlebutt account?", font=GlobalVars.TEXT_FONT, bg='white')
label.pack(pady=90)
tk.Button(self, text="Yes, and I already have an account", command=lambda: master.switch_frame(Screen2), height=3, width=30, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(pady=20)
@ -280,11 +294,11 @@ class Screen2(tk.Frame):
self.top_frame.pack(side="top", fill="x", pady=(60, 10))
# Add a label with text wrapping
self.label = tk.Label(self.top_frame,
text="Start typing your public key or alias to find yourself in the list then click on your key to select it.",
self.label = tk.Label(self.top_frame,
text="slowly type your public key or alias one letter at a time to find yourself in the list then click on your key to select it.",
font=GlobalVars.TEXT_FONT,
wraplength=800,
bg=BG_COLOR)
bg='white')
self.label.pack(side="top", pady=(0, 10))
# Add text box to the top frame
@ -356,10 +370,10 @@ class Screen2(tk.Frame):
alias = self.unescape_unicode(user.get('alias', ''))
id = user.get('id', '')
alias_label = tk.Label(frame, text=alias, font=('Helvetica', 14, 'bold'), width=20, anchor='w', bg=BG_COLOR)
alias_label = tk.Label(frame, text=alias, font=('Georgia', 14, 'bold'), width=20, anchor='w', bg='white')
alias_label.pack(side='left', padx=(0, 10))
id_label = tk.Label(frame, text=id, font=('Helvetica', 14), anchor='w', bg=BG_COLOR)
id_label = tk.Label(frame, text=id, font=('Georgia', 14), anchor='w', bg='white')
id_label.pack(side='left', expand=True, fill='x')
frame.bind('<Button-1>', lambda e, u=user, f=frame: self.on_user_clicked(f, u))
@ -507,7 +521,7 @@ class Screen3(tk.Frame):
# Info and button on the right
self.text_frame = tk.Frame(self, bg=BG_COLOR)
self.text_frame.pack(side="right", fill="both", expand=True)
tk.Label(self.text_frame, text="Now we will take a picture of your item to show up on Scuttlebutt.\n\nIf you tap Take Photo a second time it will re-take the photo\nbut wont show you a preview during the countdown (this is a bug)", font=("Helvetica", 16), bg=BG_COLOR).pack(pady=10)
tk.Label(self.text_frame, text="Now we will take a picture of your item to show up on Scuttlebutt.\n\nIf you tap Take Photo a second time it will re-take the photo\nbut wont show you a preview during the countdown (this is a bug)", font=("Georgia", 16), bg='white').pack(pady=10)
self.button = tk.Button(self.text_frame, text="Take Photo", command=self.take_photo, height=3, width=37, bg='peach puff', font=GlobalVars.BUTTON_FONT)
self.button.pack(pady=10)
@ -537,7 +551,7 @@ class Screen3(tk.Frame):
# Info and button on the right
self.text_frame = tk.Frame(self, bg=BG_COLOR)
self.text_frame.pack(side="right", fill="both", expand=True)
tk.Label(self.text_frame, text="Now we will take a picture of your item to show up on Scuttlebutt.\n\nIf you tap Take Photo a second time it will re-take the photo\nbut wont show you a preview during the countdown (this is a bug)", font=("Helvetica", 16), bg=BG_COLOR).pack(pady=10)
tk.Label(self.text_frame, text="Now we will take a picture of your item to show up on Scuttlebutt.\n\nIf you tap Take Photo a second time it will re-take the photo\nbut wont show you a preview during the countdown (this is a bug)", font=("Georgia", 16), bg='white').pack(pady=10)
self.button = tk.Button(self.text_frame, text="Take Photo", command=self.take_photo, height=3, width=37, bg='peach puff', font=GlobalVars.BUTTON_FONT)
self.button.pack(pady=10)
@ -609,7 +623,7 @@ class Screen3(tk.Frame):
if self.countdown_text:
self.canvas.create_text(self.canvas.winfo_width() // 2, self.canvas.winfo_height() // 2,
text=self.countdown_text, fill="white", font=("Helvetica", 120))
text=self.countdown_text, fill="white", font=("Georgia", 120))
self.after(10, self.update_image)
@ -681,7 +695,7 @@ class Screen4(tk.Frame):
tk.Button(self.left_frame, text="Import Image", command=self.import_image, height=2, width=15, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(pady=10)
# Add instructions
self.label = tk.Label(self.left_frame, text="You may now draw your sticker :)", wraplength=650, font=GlobalVars.TEXT_FONT)
self.label = tk.Label(self.left_frame, text="You may now draw your sticker :)", wraplength=650, font=GlobalVars.TEXT_FONT, bg='white')
self.label.pack(pady=2)
# Drawing area
@ -867,11 +881,11 @@ class Screen5(tk.Frame):
# Adding the information label
self.info_label = tk.Label(self, text="Please enter a description of your item.", font=GlobalVars.TEXT_FONT, wraplength=500)
self.info_label = tk.Label(self, text="Please enter a description of your item.", font=GlobalVars.TEXT_FONT, wraplength=500, bg='white')
self.info_label.pack(pady=70)
# Adding the text entry field
self.info_entry = tk.Text(self, height=10, width=50, font=("Helvetica", 16))
self.info_entry = tk.Text(self, height=10, width=50, font=("Georgia", 16))
self.info_entry.pack(pady=10)
# Adding the done button
@ -906,7 +920,7 @@ class Screen7(tk.Frame):
master.add_home_button(self)
# Assume there's a method to manage the text entry
self.info_label = tk.Label(self, text="Hiii sorry this hasn't been implemented yet!", font=("Helvetica", 16), wraplength=500)
self.info_label = tk.Label(self, text="Hiii sorry this hasn't been implemented yet!", font=("Georgia", 16), wraplength=500, bg='white')
self.info_label.pack()
#tk.Button(self, text="Done", command=lambda: master.switch_frame(Screen6), height=3, width=30, bg='peach puff').pack(pady=10)
@ -939,29 +953,31 @@ class Screen8(tk.Frame):
self.display_width = self.original_width * self.scale_factor
self.display_height = self.original_height * self.scale_factor
# Add the home button
master.add_home_button(self)
# 4-Column Layout for Large ribbons to fit on 1366x768 screens
# Col 1: Start Over + Instructions | Col 2: Canvas | Col 3: Pen buttons | Col 4: Labels + Actions
# Main container to hold all elements
# Main container to hold all 4 columns
main_container = tk.Frame(self, bg=BG_COLOR)
main_container.place(relx=0.5, rely=0.5, anchor='center')
# Left frame for drawing area and import button
left_frame = tk.Frame(main_container, bg=BG_COLOR)
left_frame.pack(side='left', padx=(0, 20))
# 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)
# Right frame for tools and buttons
right_frame = tk.Frame(main_container, bg=BG_COLOR)
right_frame.pack(side='right')
# Start Over button (replaces home button)
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))
# Import Image Button
tk.Button(left_frame, text="Import Image", command=self.import_image, height=2, width=15, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(pady=10)
# Instruction label
self.label = tk.Label(col1_frame, text="Draw your\nribbon :)",
wraplength=150, font=GlobalVars.TEXT_FONT, bg=BG_COLOR)
self.label.pack(pady=5)
# Simplified instructions
self.label = tk.Label(left_frame, text="You may now draw your ribbon :)", wraplength=300, font=GlobalVars.TEXT_FONT, bg=BG_COLOR)
self.label.pack(pady=10)
# Column 2: Drawing canvas
col2_frame = tk.Frame(main_container, bg=BG_COLOR)
col2_frame.pack(side='left', padx=(10, 30))
# Drawing area (doubled size for display)
# Drawing area
self.drawing = Image.new('1', (self.original_width, self.original_height), 1)
self.draw = ImageDraw.Draw(self.drawing)
self.last_draw = None
@ -970,43 +986,55 @@ class Screen8(tk.Frame):
self.draw_color = 'black'
self.draw_size = 1
# Canvas for drawing (doubled size for display)
self.canvas = Canvas(left_frame, width=self.display_width, height=self.display_height, bg='white')
# Canvas for drawing
self.canvas = Canvas(col2_frame, width=self.display_width, height=self.display_height, bg='white')
self.canvas.bind("<B1-Motion>", self.draw_line)
self.canvas.bind("<ButtonRelease-1>", self.reset_last_draw)
self.canvas.pack(pady=10)
self.canvas.pack()
# Pen size frame
pen_size_frame = tk.Frame(right_frame, bg=BG_COLOR)
pen_size_frame.pack(pady=10)
# Right panel: Pen tools (grid layout) + action buttons
right_panel = tk.Frame(main_container, bg=BG_COLOR)
right_panel.pack(side='left', padx=15)
tk.Label(pen_size_frame, text="Pen Size", font=GlobalVars.TEXT_FONT, bg=BG_COLOR).pack()
# Pen tools grid (labels align with buttons)
pen_grid = tk.Frame(right_panel, bg=BG_COLOR)
pen_grid.pack()
# Pen size buttons
# Pen Size label - spans vertically alongside pen size buttons
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 size buttons (5 buttons in grid column 1)
pen_sizes = [(".", 1), ("*", 2), ("", 3), ("", 4), ("", 5)]
for text, size in pen_sizes:
tk.Button(pen_size_frame, text=text, command=lambda s=size: self.set_draw_size(s),
height=2, width=5, bg='peach puff').pack(pady=2)
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)
# Pen color frame
pen_color_frame = tk.Frame(right_frame, bg=BG_COLOR)
pen_color_frame.pack(pady=10)
# Pen Color label - spans vertically alongside color buttons
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))
tk.Label(pen_color_frame, text="Pen Color", font=GlobalVars.TEXT_FONT, bg=BG_COLOR).pack()
# Color buttons
# Pen color buttons (3 buttons in grid column 1)
colors = ['black', 'gray', 'white']
for color in colors:
tk.Button(pen_color_frame, height=2, width=5, bg=color,
command=lambda c=color: self.set_draw_color(c)).pack(pady=2)
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 below the grid
action_frame = tk.Frame(right_panel, bg=BG_COLOR)
action_frame.pack(pady=(20, 0))
# Import Image Button
tk.Button(action_frame, text="Import Image", command=self.import_image,
height=2, width=14, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(pady=5)
# Clear Drawing Button
tk.Button(right_frame, text="Clear Drawing", command=self.clear_drawing,
height=2, width=15, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(pady=10)
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)
# Done button
tk.Button(right_frame, text="Done", command=self.next,
height=3, width=10, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(pady=10)
tk.Button(action_frame, text="Done", command=self.next,
height=2, width=14, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(pady=5)
def draw_line(self, event):
x, y = event.x // self.grid_size, event.y // self.grid_size
@ -1131,7 +1159,7 @@ class Screen10(tk.Frame):
GlobalVars.selected_user = None # Reset the selected user
tk.Frame.__init__(self, master, bg=BG_COLOR)
tk.Label(self, text="Thank you!", bg=BG_COLOR, font=('Helvetica', 48)).pack()
tk.Label(self, text="Thank you!", bg='white', font=('Georgia', 48)).pack()
tk.Button(self, text="Done", command=lambda: master.switch_frame(Screen0), height=3, width=30, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(pady=10)
# Sticker or tag?
@ -1143,7 +1171,8 @@ class Screen11(tk.Frame):
# Instructions
self.label = tk.Label(self, text="Which type of tag would you like to design?",
wraplength=400, # adjust to suit needs
font=GlobalVars.TEXT_FONT)
font=GlobalVars.TEXT_FONT,
bg='white')
self.label.pack(pady=50)
# Button functions
@ -1176,7 +1205,7 @@ class ScreenRibbonSize(tk.Frame):
# Title
title_label = tk.Label(self, text="Select your ribbon size:",
font=GlobalVars.TEXT_FONT, bg=BG_COLOR)
font=GlobalVars.TEXT_FONT, bg='white')
title_label.pack(pady=30)
# Container for the three size options
@ -1227,7 +1256,7 @@ class ScreenRibbonSize(tk.Frame):
preview_width - side_margin_preview, margin_preview + drawing_preview_height,
fill='#e0e0e0', outline='black')
canvas.create_text(preview_width // 2, margin_preview + drawing_preview_height // 2,
text="Drawing", font=('Helvetica', 10))
text="Drawing", font=('Georgia', 10))
# Draw QR indicator (centered)
qr_x = (preview_width - qr_preview_size) // 2
@ -1236,7 +1265,7 @@ class ScreenRibbonSize(tk.Frame):
qr_x + qr_preview_size, qr_y + qr_preview_size,
fill='#808080', outline='black')
canvas.create_text(preview_width // 2, qr_y + qr_preview_size // 2,
text="QR", font=('Helvetica', 8), fill='white')
text="QR", font=('Georgia', 8), fill='white')
# Draw bottom margin area
canvas.create_rectangle(0, qr_y + qr_preview_size, preview_width, preview_height,
@ -1251,7 +1280,7 @@ class ScreenRibbonSize(tk.Frame):
# Description label
desc_label = tk.Label(option_frame, text=description,
font=('Helvetica', 12), bg=BG_COLOR)
font=('Georgia', 12), bg='white')
desc_label.pack()
def select_size(self, size, master):
@ -1290,7 +1319,7 @@ class Screen12(tk.Frame):
error_frame = tk.Frame(self, bg=BG_COLOR)
error_frame.pack(expand=True, fill='both', padx=20, pady=20)
tk.Label(error_frame, text=message, bg=BG_COLOR, font=GlobalVars.TEXT_FONT, wraplength=500).pack(pady=50)
tk.Label(error_frame, text=message, bg='white', font=GlobalVars.TEXT_FONT, wraplength=500).pack(pady=50)
# Add "Go Back" button for error cases
tk.Button(error_frame, text="Go Back", command=lambda: self.master.switch_frame(Screen0),
@ -1352,18 +1381,18 @@ class Screen12(tk.Frame):
text_content = message_content.get('content', {}).get('text', '')
# Remove markdown image syntax
text_content = re.sub(r'!\[.*?\]\(.*?\)', '', text_content).strip()
tk.Label(scrollable_frame, text=text_content, wraplength=650, justify='left', bg=BG_COLOR, font=("Helvetica", 28)).pack(pady=5, padx=(5, 0))
tk.Label(scrollable_frame, text=text_content, wraplength=650, justify='left', bg='white', font=("Georgia", 28)).pack(pady=5, padx=(5, 0))
# Display replies
if replies:
tk.Label(scrollable_frame, text="Replies:", wraplength=650, justify='left', bg=BG_COLOR, font=("Helvetica", 24, "bold")).pack(pady=(20, 5), padx=(5, 0))
tk.Label(scrollable_frame, text="Replies:", wraplength=650, justify='left', bg='white', font=("Georgia", 24, "bold")).pack(pady=(20, 5), padx=(5, 0))
for reply in replies:
author_id = reply.get('value', {}).get('author', 'Unknown')
author_alias = self.get_alias(author_id)
author_display = f"{author_alias} ({author_id})" if author_alias else author_id
reply_text = reply.get('value', {}).get('content', {}).get('text', '')
tk.Label(scrollable_frame, text=f"{author_display}:", wraplength=650, justify='left', bg=BG_COLOR, font=("Helvetica", 20, "bold")).pack(pady=(10, 0), padx=(5, 0))
tk.Label(scrollable_frame, text=reply_text, wraplength=650, justify='left', bg=BG_COLOR, font=("Helvetica", 18)).pack(pady=(0, 10), padx=(5, 0))
tk.Label(scrollable_frame, text=f"{author_display}:", wraplength=650, justify='left', bg='white', font=("Georgia", 20, "bold")).pack(pady=(10, 0), padx=(5, 0))
tk.Label(scrollable_frame, text=reply_text, wraplength=650, justify='left', bg='white', font=("Georgia", 18)).pack(pady=(0, 10), padx=(5, 0))
canvas.pack(side="left", fill="both", expand=True)
scrollbar.pack(side="right", fill="y")
@ -1388,7 +1417,7 @@ class Screen13(tk.Frame):
container.place(relx=0.5, rely=0.5, anchor='center')
# instructions
tk.Label(container, text="Wonderful! It is now time to post your item to Scuttlebutt and to print your tag. You can still cancel by hitting Start Over if you like.", wraplength=600, font=GlobalVars.TEXT_FONT).grid(row=0, column=0, columnspan=2)
tk.Label(container, text="Wonderful! It is now time to post your item to Scuttlebutt and to print your tag. You can still cancel by hitting Start Over if you like.", wraplength=600, font=GlobalVars.TEXT_FONT, bg='white').grid(row=0, column=0, columnspan=2)
# buttons
master.add_home_button(self)
@ -1400,7 +1429,7 @@ class Screen13(tk.Frame):
global print_type
# Specify the path to your image file
path_to_image = "/home/trav/Documents/custodiosk/freeze_frame.jpg"
path_to_image = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'freeze_frame.jpg')
# Get QR data from the main application
QRX = self.master.QRX
@ -1552,9 +1581,9 @@ class Screen14(tk.Frame):
self.video.pack(fill='both', expand=True)
# Setup the instruction
self.instruction = tk.Label(self.instruction_frame,
text="Hold the QR code up to the camera",
font=("Helvetica", 36),
self.instruction = tk.Label(self.instruction_frame,
text="Hold the QR code up to the camera",
font=("Georgia", 36),
bg='white', fg='black',
wraplength=500,
justify='center')
@ -1615,6 +1644,519 @@ class Screen14(tk.Frame):
super().destroy()
# No QR Ribbon - Size Selection
class NoQRRibbonSizeScreen(tk.Frame):
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
# Title
title_label = tk.Label(self, text="Select your ribbon size:",
font=GlobalVars.TEXT_FONT, bg='white')
title_label.pack(pady=30)
# Container for size options
options_frame = tk.Frame(self, bg=BG_COLOR)
options_frame.pack(pady=20)
# Size options with multipliers
sizes = [
('Small', 0.5, 'Half-height drawing'),
('Medium', 1.0, 'Square drawing'),
('Large', 1.5, 'Tall drawing')
]
for size_name, multiplier, description in sizes:
self.create_size_option(options_frame, size_name, multiplier, description, master)
# Add Import Image option
self.create_import_option(options_frame, master)
def create_import_option(self, parent, master):
"""Create the Import Image option column"""
option_frame = tk.Frame(parent, bg=BG_COLOR, padx=20)
option_frame.pack(side='left', padx=20)
# Create preview canvas (show a simplified "variable height" preview)
preview_scale = 0.3
preview_width = int(self.ribbon_width * preview_scale)
# Use medium size for base preview height
base_height = int((self.margin + self.printable_width + self.margin) * preview_scale)
margin_preview = int(self.margin * preview_scale)
side_margin_preview = int(25 * preview_scale)
canvas = tk.Canvas(option_frame, width=preview_width, height=base_height,
bg='white', highlightthickness=2, highlightbackground='gray')
canvas.pack(pady=10)
# Draw top margin
canvas.create_rectangle(0, 0, preview_width, margin_preview,
fill='#f5f5f5', outline='')
# Draw image area (dashed to indicate variable)
drawing_height = base_height - 2 * margin_preview
canvas.create_rectangle(side_margin_preview, margin_preview,
preview_width - side_margin_preview, margin_preview + drawing_height,
fill='#d0e8d0', outline='black', dash=(4, 2))
canvas.create_text(preview_width // 2, margin_preview + drawing_height // 2,
text="Your\nImage", font=('Georgia', 10))
# Draw bottom margin
canvas.create_rectangle(0, margin_preview + drawing_height, preview_width, base_height,
fill='#f5f5f5', outline='')
# Import button
button_text = f"Import Image\n{self.printable_width}px wide"
btn = tk.Button(option_frame, text=button_text,
command=lambda: master.switch_frame(NoQRImportImageScreen),
height=3, width=15, bg='peach puff', font=GlobalVars.BUTTON_FONT)
btn.pack(pady=10)
# Description
desc_label = tk.Label(option_frame, text="Variable height",
font=('Georgia', 12), bg='white')
desc_label.pack()
def create_size_option(self, parent, size_name, multiplier, description, master):
# Frame for each option
option_frame = tk.Frame(parent, bg=BG_COLOR, padx=20)
option_frame.pack(side='left', padx=20)
# Calculate dimensions for this size (based on printable width)
drawing_height = int(self.printable_width * multiplier)
# Total height includes top/bottom margins (no QR)
total_height = self.margin + drawing_height + self.margin
# Create preview canvas (scaled down for display)
preview_scale = 0.3
preview_width = int(self.ribbon_width * preview_scale)
preview_height = int(total_height * preview_scale)
margin_preview = int(self.margin * preview_scale)
drawing_preview_height = int(drawing_height * preview_scale)
side_margin_preview = int(25 * preview_scale) # 25px side margins
canvas = tk.Canvas(option_frame, width=preview_width, height=preview_height,
bg='white', highlightthickness=2, highlightbackground='gray')
canvas.pack(pady=10)
# Draw top margin area
canvas.create_rectangle(0, 0, preview_width, margin_preview,
fill='#f5f5f5', outline='')
# Draw drawing area (with side margins indicated)
canvas.create_rectangle(side_margin_preview, margin_preview,
preview_width - side_margin_preview, margin_preview + drawing_preview_height,
fill='#e0e0e0', outline='black')
canvas.create_text(preview_width // 2, margin_preview + drawing_preview_height // 2,
text="Drawing", font=('Georgia', 10))
# Draw bottom margin area
canvas.create_rectangle(0, margin_preview + drawing_preview_height, preview_width, preview_height,
fill='#f5f5f5', outline='')
# Size name and dimensions button (show printable dimensions)
button_text = f"{size_name}\n{self.printable_width}×{drawing_height}px"
btn = tk.Button(option_frame, text=button_text,
command=lambda s=size_name.lower(): self.select_size(s, master),
height=3, width=15, bg='peach puff', font=GlobalVars.BUTTON_FONT)
btn.pack(pady=10)
# Description label
desc_label = tk.Label(option_frame, text=description,
font=('Georgia', 12), bg='white')
desc_label.pack()
def select_size(self, size, master):
GlobalVars.ribbon_size = size
master.switch_frame(NoQRDrawingScreen)
# No QR Ribbon - Drawing Screen
class NoQRDrawingScreen(tk.Frame):
def __init__(self, master):
tk.Frame.__init__(self, master, bg=BG_COLOR)
# Get ribbon width from config and calculate dimensions based on selected size
self.ribbon_width = CONFIG.get("ribbon", {}).get("width", 450)
self.printable_width = self.ribbon_width - 50 # 25px margin each side
multiplier = {'small': 0.5, 'medium': 1.0, 'large': 1.5}.get(GlobalVars.ribbon_size, 1.0)
self.original_width = self.printable_width
self.original_height = int(self.printable_width * multiplier)
# Adaptive display scaling based on size
max_display_height = 700
ideal_scale = max_display_height / self.original_height
self.scale_factor = min(2, max(1, int(ideal_scale)))
self.grid_size = self.scale_factor
# Display dimensions
self.display_width = self.original_width * self.scale_factor
self.display_height = self.original_height * self.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=(15, 25), anchor='n', pady=20)
# Start Over button
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))
# Instruction label
self.label = tk.Label(col1_frame, text="Draw your\nribbon :)",
wraplength=150, font=GlobalVars.TEXT_FONT, bg=BG_COLOR)
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))
# Drawing area
self.drawing = Image.new('1', (self.original_width, self.original_height), 1)
self.draw = ImageDraw.Draw(self.drawing)
self.last_draw = None
# Set initial drawing color to black and size to 1
self.draw_color = 'black'
self.draw_size = 1
# Canvas for drawing
self.canvas = Canvas(col2_frame, width=self.display_width, height=self.display_height, bg='white')
self.canvas.bind("<B1-Motion>", self.draw_line)
self.canvas.bind("<ButtonRelease-1>", self.reset_last_draw)
self.canvas.pack()
# 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()
# Pen Size label
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 size buttons
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)
# Pen Color label
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))
# Pen color buttons
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))
# Import Image Button
tk.Button(action_frame, text="Import Image", command=self.import_image,
height=2, width=14, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(pady=5)
# Clear Drawing Button
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)
# Print button (goes directly to print)
tk.Button(action_frame, text="Print", command=self.go_to_print,
height=2, width=14, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(pady=5)
def draw_line(self, event):
x, y = event.x // self.grid_size, event.y // self.grid_size
if self.last_draw:
points = self.get_points_on_line(*self.last_draw, x, y)
for px, py in points:
if self.draw_color == 'gray':
self.draw_dithered_point(px, py)
else:
self.draw_point(px, py)
self.last_draw = (x, y)
def get_points_on_line(self, x0, y0, x1, y1):
points = []
dx = abs(x1 - x0)
dy = abs(y1 - y0)
sx = 1 if x0 < x1 else -1
sy = 1 if y0 < y1 else -1
err = dx - dy
while True:
points.append((x0, y0))
if x0 == x1 and y0 == y1:
break
e2 = 2 * err
if e2 > -dy:
err -= dy
x0 += sx
if e2 < dx:
err += dx
y0 += sy
return points
def draw_point(self, x, y):
color = 0 if self.draw_color == 'black' else 1
for dx in range(self.draw_size):
for dy in range(self.draw_size):
self.canvas.create_rectangle(
(x+dx)*self.grid_size, (y+dy)*self.grid_size,
(x+dx+1)*self.grid_size, (y+dy+1)*self.grid_size,
fill=self.draw_color, outline=''
)
self.draw.point((x+dx, y+dy), fill=color)
def draw_dithered_point(self, x, y):
for dx in range(self.draw_size):
for dy in range(self.draw_size):
if (x+dx+y+dy) % 2 == 0:
self.canvas.create_rectangle(
(x+dx)*self.grid_size, (y+dy)*self.grid_size,
(x+dx+1)*self.grid_size, (y+dy+1)*self.grid_size,
fill='black', outline=''
)
self.draw.point((x+dx, y+dy), fill=0)
else:
self.canvas.create_rectangle(
(x+dx)*self.grid_size, (y+dy)*self.grid_size,
(x+dx+1)*self.grid_size, (y+dy+1)*self.grid_size,
fill='white', outline=''
)
self.draw.point((x+dx, y+dy), fill=1)
def reset_last_draw(self, event):
self.last_draw = None
def set_draw_color(self, color):
self.draw_color = color
def set_draw_size(self, size):
self.draw_size = size
def clear_drawing(self):
self.canvas.delete("all")
self.drawing = Image.new('1', (self.original_width, self.original_height), 1)
self.draw = ImageDraw.Draw(self.drawing)
def import_image(self):
file_path = filedialog.askopenfilename(filetypes=[("Image files", "*.jpg *.jpeg *.png")])
if not file_path:
return
img = Image.open(file_path)
img = img.convert('1')
if img.size != (self.original_width, self.original_height):
img = img.resize((self.original_width, self.original_height), Image.LANCZOS)
self.drawing = img
self.draw = ImageDraw.Draw(self.drawing)
# Display the image at scaled size
display_img = img.resize((self.display_width, self.display_height), Image.NEAREST)
self.imported_img = ImageTk.PhotoImage(display_img)
self.canvas.delete("all")
self.canvas.create_image(0, 0, image=self.imported_img, anchor='nw')
def go_to_print(self):
self.drawing.save("drawing.png")
self.master.switch_frame(NoQRPrintScreen)
# No QR Ribbon - Import Image Screen
class NoQRImportImageScreen(tk.Frame):
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
# Main container
self.container = tk.Frame(self, bg=BG_COLOR)
self.container.place(relx=0.5, rely=0.5, anchor='center')
# Title
self.title_label = tk.Label(self.container, text="Import an image for your ribbon",
font=GlobalVars.TEXT_FONT, bg=BG_COLOR)
self.title_label.pack(pady=20)
# Preview area (will show scaled image)
self.preview_frame = tk.Frame(self.container, bg=BG_COLOR)
self.preview_frame.pack(pady=20)
self.preview_label = tk.Label(self.preview_frame, text="No image selected",
font=('Georgia', 14), bg='white', width=40, height=15)
self.preview_label.pack()
# Info label for dimensions
self.info_label = tk.Label(self.container, text="",
font=('Georgia', 12), bg=BG_COLOR)
self.info_label.pack(pady=10)
# Buttons frame
buttons_frame = tk.Frame(self.container, bg=BG_COLOR)
buttons_frame.pack(pady=20)
# Select Image button
self.select_btn = tk.Button(buttons_frame, text="Select Image",
command=self.select_image,
height=2, width=15, bg='peach puff',
font=GlobalVars.BUTTON_FONT)
self.select_btn.pack(side='left', padx=10)
# Print button (disabled until image selected)
self.print_btn = tk.Button(buttons_frame, text="Print",
command=self.go_to_print,
height=2, width=15, bg='peach puff',
font=GlobalVars.BUTTON_FONT, state='disabled')
self.print_btn.pack(side='left', padx=10)
# Cancel button
self.cancel_btn = tk.Button(buttons_frame, text="Cancel",
command=lambda: master.switch_frame(NoQRRibbonSizeScreen),
height=2, width=15, bg='peach puff',
font=GlobalVars.BUTTON_FONT)
self.cancel_btn.pack(side='left', padx=10)
# Store the processed image
self.processed_image = None
self.preview_photo = None
# Auto-open file dialog
self.after(100, self.select_image)
def select_image(self):
file_path = filedialog.askopenfilename(
filetypes=[("Image files", "*.jpg *.jpeg *.png *.bmp *.gif")]
)
if not file_path:
return
# Open and process the image
img = Image.open(file_path)
# Calculate new dimensions maintaining aspect ratio
original_width, original_height = img.size
aspect_ratio = original_height / original_width
new_width = self.printable_width
new_height = int(self.printable_width * aspect_ratio)
# Resize maintaining aspect ratio
img = img.resize((new_width, new_height), Image.LANCZOS)
# Convert to 1-bit black/white
img = img.convert('1')
# Store the processed image
self.processed_image = img
# Update info label
self.info_label.config(text=f"Size: {new_width} x {new_height} pixels")
# Create preview (scaled for display)
max_preview_height = 400
max_preview_width = 500
preview_scale = min(max_preview_width / new_width, max_preview_height / new_height, 1.0)
preview_width = int(new_width * preview_scale)
preview_height = int(new_height * preview_scale)
preview_img = img.resize((preview_width, preview_height), Image.NEAREST)
self.preview_photo = ImageTk.PhotoImage(preview_img)
# Update preview label to show image
self.preview_label.config(image=self.preview_photo, text="", width=preview_width, height=preview_height)
# Enable print button
self.print_btn.config(state='normal')
def go_to_print(self):
if self.processed_image:
self.processed_image.save("drawing.png")
# Set ribbon_size to 'import' to signal variable height
GlobalVars.ribbon_size = 'import'
self.master.switch_frame(NoQRPrintScreen)
# No QR Ribbon - Print Screen (no QR, no SSB, no photo, no description)
class NoQRPrintScreen(tk.Frame):
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
tk.Label(container, text="Ready to print your ribbon!", wraplength=600, font=GlobalVars.TEXT_FONT, bg='white').grid(row=0, column=0, columnspan=2)
# Print button
tk.Button(container, text="Print", command=self.print_ribbon, height=3, width=30, bg='peach puff', font=GlobalVars.BUTTON_FONT).grid(row=2, column=0, pady=20)
def print_ribbon(self):
# Get config values
ribbon_width = CONFIG.get("ribbon", {}).get("width", 450)
margin = CONFIG.get("ribbon", {}).get("margin", 50)
printable_width = ribbon_width - 50 # 25px side margins
# Load the drawing
drawing = Image.open("drawing.png")
drawing_width, drawing_height = drawing.size
# For fixed sizes, use multiplier; for imports, use actual image dimensions
if GlobalVars.ribbon_size != 'import':
multiplier = {'small': 0.5, 'medium': 1.0, 'large': 1.5}.get(GlobalVars.ribbon_size, 1.0)
drawing_height = int(printable_width * multiplier)
# Total label height: just margins + drawing (no QR, no scan-tag)
total_height = margin + drawing_height + margin
# Create full-width image with margins
merged_image = Image.new('L', (ribbon_width, total_height), "white")
# Paste drawing 25px in from left, margin down from top
merged_image.paste(drawing, (25, margin))
merged_image.save("merged_image.png")
# Get the ZPL code for the image
zpl_code = tozpl.print_to_zpl("merged_image.png",
print_width=ribbon_width,
label_length=total_height)
# Save the ZPL
with open("to_print.zpl", "w") as file:
file.write(zpl_code)
# Print to ribbon printer
try:
result = subprocess.Popen(f'lp -d {CONFIG["printers"]["ribbon"]} -o raw to_print.zpl', shell=True, stdout=subprocess.PIPE)
except:
print('traceback.format_exc():\n%s' % traceback.format_exc())
self.master.switch_frame(Screen10)
if __name__ == "__main__":
app = Kiosk()
app.mainloop()

112
list_cameras.py Executable file
View File

@ -0,0 +1,112 @@
#!/usr/bin/env python3
"""
Camera Listing Utility for Custodisco Kiosk
This script scans for available cameras and displays information about them.
Use this to find the correct camera name to set in config.json.
"""
import os
import re
import glob
import cv2
def get_camera_info():
"""
Scan for available cameras and return information about each.
Returns a list of dicts with keys: index, device_name, status
"""
cameras = []
by_id_dir = "/dev/v4l/by-id"
found_indices = set()
# First, scan /dev/v4l/by-id for symlinks to cameras
if os.path.isdir(by_id_dir):
paths = sorted(glob.glob(os.path.join(by_id_dir, "*")))
for path in paths:
device_name = os.path.basename(path)
target = os.path.realpath(path)
match = re.search(r"video(\d+)", target)
if not match:
continue
idx = int(match.group(1))
if idx in found_indices:
continue
# Test if camera works
status = test_camera(idx)
cameras.append({
"index": idx,
"device_name": device_name,
"status": status
})
found_indices.add(idx)
# Fallback: probe indices 0-5 for cameras not in /dev/v4l/by-id
for idx in range(6):
if idx in found_indices:
continue
status = test_camera(idx)
if status == "Working":
cameras.append({
"index": idx,
"device_name": "(built-in or unknown)",
"status": status
})
found_indices.add(idx)
# Sort by index
cameras.sort(key=lambda x: x["index"])
return cameras
def test_camera(index):
"""Test if a camera index can be opened."""
try:
cap = cv2.VideoCapture(index)
if cap.isOpened():
cap.release()
return "Working"
cap.release()
return "Not available"
except Exception as e:
return f"Error: {e}"
def print_camera_table(cameras):
"""Print a formatted table of cameras."""
if not cameras:
print("No cameras found.")
print("\nTroubleshooting tips:")
print(" - Check that your camera is connected")
print(" - Try unplugging and replugging the camera")
print(" - Check dmesg for USB device errors: dmesg | tail -20")
return
print("\nAvailable Cameras:")
print(" {:<6} {:<45} {}".format("Index", "Device Name", "Status"))
print(" {:<6} {:<45} {}".format("-----", "-----------", "------"))
for cam in cameras:
print(" {:<6} {:<45} {}".format(
cam["index"],
cam["device_name"][:45],
cam["status"]
))
print("\nTo use a camera, set \"preferred_name\" in config.json to part of the device name.")
print("Example: \"preferred_name\": \"NexiGo\"")
def main():
print("Scanning for cameras...")
cameras = get_camera_info()
print_camera_table(cameras)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,281 @@
# Nylon Printer Setup Guide
## Zebra GX430t - Thermal Transfer - 1.5" Continuous Nylon with Cutter
### Hardware Requirements
- Zebra GX430t printer with cutter module (300dpi)
- 1.5" continuous nylon ribbon roll
- Full resin thermal transfer ribbon
- DIY uptake spool for ribbon
- USB connection to Debian machine
### Step 1: Factory Reset Printer
Before starting, wipe any previous owner settings:
```bash
echo "^XA^JUF^XZ" | lp -d YOUR_PRINTER_NAME
```
Wait 5-10 seconds for the printer to reset.
### Step 2: Add Printer to CUPS
1. Install CUPS if not already installed:
```bash
sudo apt update
sudo apt install cups cups-client
sudo usermod -aG lpadmin $USER
```
Log out and back in after adding to lpadmin group.
2. Connect printer via USB and verify detection:
```bash
lsusb | grep -i zebra
```
3. Access CUPS web interface at `http://localhost:631`
4. Go to **Administration → Add Printer**
5. Select the Zebra printer from USB devices
6. Name it something memorable (e.g., `nylon`)
7. For the driver/model, select **"Raw Queue"** or use command line:
```bash
sudo lpadmin -p nylon -E -v usb://Zebra%20Technologies/ZTC%20GX430t?serial=YOUR_SERIAL -m raw
```
8. Restart CUPS:
```bash
sudo systemctl restart cups
```
9. Verify printer exists:
```bash
lpstat -p
```
### Step 3: Configure Thermal Transfer Mode
**CRITICAL:** Enable ribbon uptake, otherwise used ribbon will pool inside printer.
```bash
# Set thermal transfer mode with uptake enabled
echo "^XA^MNN,Y^JUS^XZ" | lp -d nylon
# Verify settings saved
echo "^XA^JUA^XZ" | lp -d nylon
```
The `^MNN,Y` command:
- First `N` = Thermal transfer (not direct thermal)
- Second `N` = Continuous media handling (no gap detection needed)
- `Y` = Enable ribbon uptake motor
### Step 4: Set Media Width
For 1.5" wide continuous nylon at 300dpi:
```bash
# Width: 1.5" × 300dpi = 450 dots
echo "^XA^PW450^JUS^XZ" | lp -d nylon
```
**Note:** Label length (`^LL`) will be set per-print based on content height.
### Step 5: Configure Cutter
Enable cutter to cut after each print:
```bash
# MMC = Cutter mode
# ^MMC,Y = Enable cutter at end of print
echo "^XA^MMC,Y^JUS^XZ" | lp -d nylon
```
Alternative cutter commands if needed:
- `^MMC,N` - Disable cutter
- `^CN##` - Cut immediately, advance N dots after cut
### Step 6: Configure Darkness
Full resin on nylon typically needs similar darkness to labels. Start with MD18:
```bash
echo "^XA^MD18^JUS^XZ" | lp -d nylon
```
Test and adjust:
- Too light/gray? Increase to MD20 or MD22
- Resin sticking to nylon? Decrease to MD15
**Note:** Nylon may require different darkness than paper labels. Test thoroughly.
### Step 7: Set Continuous Media Mode
Since there are no gaps in continuous nylon:
```bash
# Web sensing off, continuous media
echo "^XA^MNN,Y^JUS^XZ" | lp -d nylon
```
No calibration needed for continuous media - the printer just cuts at the length you specify.
### Step 8: Test Print
Simple text test (creates ~1" tall tag):
```bash
echo "^XA^PW450^LL300^MD18^FO50,50^ADN,36,20^FDNylon Test^FS^XZ" | lp -d nylon
```
This will:
- Print "Nylon Test" text
- Create a 1.5" wide x 1" tall tag (300 dots / 300dpi)
- Cut after printing
Longer test with border:
```bash
echo "^XA^PW450^LL600^MD18^FO10,10^GB430,580,5^FS^FO50,50^ADN,36,20^FDLonger Tag^FS^XZ" | lp -d nylon
```
This creates a 1.5" wide x 2" tall tag with border.
### Step 9: Print from ZPL File
```bash
lp -d nylon your_file.zpl
```
**Important:** Your ZPL files MUST include `^LL` (label length) for each print, since continuous media has no fixed length.
Example ZPL structure:
```zpl
^XA
^PW450 # Width: 1.5"
^LL450 # Length: 1.5" (creates square tag)
^MD18 # Darkness
^FO50,50^ADN,36,20^FDContent Here^FS
^XZ
```
### Troubleshooting
**Problem: Ribbon pools/doesn't wind onto uptake spool**
- Solution: Make sure you ran `^MNN,Y` (uptake enabled)
- Check DIY uptake spool is properly engaged with drive gear
- Factory reset and reconfigure if needed
**Problem: Cutter doesn't activate**
- Solution: Verify cutter is enabled: `echo "^XA^MMC,Y^JUS^XZ" | lp -d nylon`
- Check if cutter module is physically installed
- Try immediate cut command: `echo "^XA^CN5^XZ" | lp -d nylon`
**Problem: Prints are fuzzy or gray**
- Solution: Increase darkness (`^MD20` or higher)
- Nylon may require different heat than paper
- Check print head cleanliness (wipe with isopropyl alcohol)
**Problem: Resin sticks to nylon when handling**
- Solution: Decrease darkness (`^MD15` or lower)
- Slow down print speed: `echo "^XA^PR3^JUS^XZ" | lp -d nylon`
**Problem: Tags are wrong length**
- Solution: Check your ZPL includes correct `^LL` value
- Remember: dots = inches × 300
- 1" = 300 dots, 2" = 600 dots, etc.
**Problem: ZPL commands print as literal text**
- Solution: Printer isn't configured as raw queue
- Reconfigure: `sudo lpadmin -p nylon -m raw`
### Complete Configuration Script
Save this as a shell script for easy re-setup:
```bash
#!/bin/bash
PRINTER_NAME="nylon"
echo "Factory resetting printer..."
echo "^XA^JUF^XZ" | lp -d $PRINTER_NAME
sleep 5
echo "Configuring thermal transfer with uptake..."
echo "^XA^MNN,Y^JUS^XZ" | lp -d $PRINTER_NAME
sleep 2
echo "Setting media width (1.5\")..."
echo "^XA^PW450^JUS^XZ" | lp -d $PRINTER_NAME
sleep 2
echo "Enabling cutter..."
echo "^XA^MMC,Y^JUS^XZ" | lp -d $PRINTER_NAME
sleep 2
echo "Setting darkness to 18..."
echo "^XA^MD18^JUS^XZ" | lp -d $PRINTER_NAME
sleep 2
echo "Configuration complete."
echo "Test with: echo \"^XA^PW450^LL300^MD18^FO50,50^ADN,36,20^FDTest^FS^XZ\" | lp -d $PRINTER_NAME"
```
### Quick Reference: Continuous Media vs Gap Media
**Key Differences:**
| Feature | Gap Media (Sticker Printer) | Continuous Media (Nylon Printer) |
|---------|----------------------------|----------------------------------|
| Media Type | Pre-cut labels with gaps | Continuous roll |
| Calibration | Required (2-blink method) | Not needed |
| Label Length | Fixed per media type | Variable per print |
| `^LL` Command | Same for all prints | Must specify each time |
| Sensor | Gap/notch detection | None (cuts at specified length) |
| Cutter | Optional | Required |
### Calculating Tag Length in ZPL
For 300dpi printer:
- 1 inch = 300 dots
- 2 inches = 600 dots
- 0.5 inches = 150 dots
Formula: `dots = inches × 300`
Example for 2.5" tall tag:
```bash
echo "^XA^PW450^LL750^MD18^FO50,50^ADN,36,20^FDContent^FS^XZ" | lp -d nylon
```
### Common ZPL Commands for Continuous Media
- `^XA` / `^XZ` - Start/end ZPL command block
- `^PW###` - Print width in dots (450 for 1.5")
- `^LL###` - Label length in dots (REQUIRED for each print)
- `^MD##` - Darkness (0-30)
- `^MMC,Y` - Enable cutter
- `^CN##` - Cut now, advance ## dots
- `^FO###,###` - Field origin (X,Y position in dots)
- `^ADN,H,W` - Font: N=default, H=height, W=width
- `^FD` - Field data (the actual text/content)
- `^FS` - Field separator (end of field)
- `^GB` - Graphic box
- `^JUS` - Save configuration
- `^JUF` - Factory reset
### Testing Different Tag Lengths
```bash
# 1" tall tag
echo "^XA^PW450^LL300^MD18^FO50,50^ADN,36,20^FD1 inch^FS^XZ" | lp -d nylon
# 2" tall tag
echo "^XA^PW450^LL600^MD18^FO50,50^ADN,36,20^FD2 inches^FS^XZ" | lp -d nylon
# 3" tall tag
echo "^XA^PW450^LL900^MD18^FO50,50^ADN,36,20^FD3 inches^FS^XZ" | lp -d nylon
```

11
package.json Normal file
View File

@ -0,0 +1,11 @@
{
"name": "custodisco-kiosk",
"version": "1.0.0",
"description": "Kiosk application for Custodisco - creates item tags with QR codes and posts to Scuttlebutt",
"private": true,
"dependencies": {
"ssb-client": "^4.9.0",
"ssb-keys": "^8.5.0",
"pull-stream": "^3.7.0"
}
}

6
requirements.txt Normal file
View File

@ -0,0 +1,6 @@
Pillow
opencv-python
numpy
qrcode
qreader
pyzbar

18
run.sh Executable file
View File

@ -0,0 +1,18 @@
#!/bin/bash
# Run the Custodisco Kiosk
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
if [ ! -d "venv" ]; then
echo "Virtual environment not found. Run ./install.sh first."
exit 1
fi
source venv/bin/activate
if [ "$1" = "--list-cameras" ]; then
python list_cameras.py
else
python kiosk.py
fi

BIN
scan-tag.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -0,0 +1,199 @@
# Sticker Printer Setup Guide
## Zebra GX430t - Thermal Transfer - 2.25" x 1.25" Tear-Off Labels
### Hardware Requirements
- Zebra GX430t printer (300dpi)
- 2.25" x 1.25" tear-off labels (gap-separated)
- Full resin thermal transfer ribbon
- Uptake spool (or DIY replacement)
- USB connection to Debian machine
### Step 1: Factory Reset Printer
Before starting, wipe any previous owner settings:
```bash
echo "^XA^JUF^XZ" | lp -d YOUR_PRINTER_NAME
```
Wait 5-10 seconds for the printer to reset.
### Step 2: Add Printer to CUPS
1. Install CUPS if not already installed:
```bash
sudo apt update
sudo apt install cups cups-client
sudo usermod -aG lpadmin $USER
```
Log out and back in after adding to lpadmin group.
2. Connect printer via USB and verify detection:
```bash
lsusb | grep -i zebra
```
3. Access CUPS web interface at `http://localhost:631`
4. Go to **Administration → Add Printer**
5. Select the Zebra printer from USB devices
6. Name it something memorable (e.g., `sticker`)
7. For the driver/model, select **"Raw Queue"** or use command line:
```bash
sudo lpadmin -p sticker -E -v usb://Zebra%20Technologies/ZTC%20GX430t?serial=YOUR_SERIAL -m raw
```
8. Restart CUPS:
```bash
sudo systemctl restart cups
```
9. Verify printer exists:
```bash
lpstat -p
```
### Step 3: Configure Thermal Transfer Mode
**CRITICAL:** Enable ribbon uptake, otherwise used ribbon will pool inside printer.
```bash
# Set thermal transfer mode with uptake enabled
echo "^XA^MNN,Y^JUS^XZ" | lp -d sticker
# Verify settings saved
echo "^XA^JUA^XZ" | lp -d sticker
```
The `^MNN,Y` command:
- First `N` = Thermal transfer (not direct thermal)
- Second `N` = Continuous media handling
- `Y` = Enable ribbon uptake motor
### Step 4: Set Label Dimensions
For 2.25" wide x 1.25" tall labels at 300dpi:
```bash
# Width: 2.25" × 300dpi = 675 dots
# Height: 1.25" × 300dpi = 375 dots
echo "^XA^PW675^LL375^JUS^XZ" | lp -d sticker
```
### Step 5: Configure Darkness
Full resin ribbon needs moderate heat. Start with MD18:
```bash
echo "^XA^MD18^JUS^XZ" | lp -d sticker
```
If prints are too light/gray, increase to MD20 or MD22.
If resin sticks to labels when peeling, decrease to MD15.
### Step 6: Physical Calibration
The printer needs to detect gaps between labels:
1. **Turn printer OFF**
2. **Hold down the feed button**
3. **Turn printer ON while still holding button**
4. **Keep holding until printer blinks twice** (usually ~5 seconds)
5. **Release button**
The printer will feed several labels as it calibrates the gap sensor.
### Step 7: Test Print
Simple text test:
```bash
echo "^XA^PW675^LL375^MD18^FO50,50^ADN,36,20^FDTest Print^FS^XZ" | lp -d sticker
```
Border alignment test:
```bash
echo "^XA^PW675^LL375^MD18^FO10,10^GB655,355,5^FS^XZ" | lp -d sticker
```
This prints a 5-dot border around the label edge. If misaligned, adjust with:
```bash
# Shift print area right by N dots
echo "^XA^LS20^JUS^XZ" | lp -d sticker
```
### Step 8: Print from ZPL File
```bash
lp -d sticker your_file.zpl
```
### Troubleshooting
**Problem: Ribbon pools/doesn't wind onto uptake spool**
- Solution: Make sure you ran `^MNN,Y` (uptake enabled)
- Factory reset and reconfigure if needed
**Problem: Prints are fuzzy or gray**
- Solution: Increase darkness (`^MD20` or higher)
- Check print head cleanliness (wipe with isopropyl alcohol)
**Problem: Printing across multiple labels**
- Solution: Re-run physical calibration (2-blink method)
- Verify label length matches actual label+gap size
**Problem: Resin sticks to label when peeling**
- Solution: Decrease darkness (`^MD15` or lower)
- Ensure print speed isn't too slow: `echo "^XA^PR4^JUS^XZ" | lp -d sticker`
**Problem: ZPL commands print as literal text**
- Solution: Printer isn't configured as raw queue
- Reconfigure: `sudo lpadmin -p sticker -m raw`
### Complete Configuration Script
Save this as a shell script for easy re-setup:
```bash
#!/bin/bash
PRINTER_NAME="sticker"
echo "Factory resetting printer..."
echo "^XA^JUF^XZ" | lp -d $PRINTER_NAME
sleep 5
echo "Configuring thermal transfer with uptake..."
echo "^XA^MNN,Y^JUS^XZ" | lp -d $PRINTER_NAME
sleep 2
echo "Setting label dimensions (2.25\" x 1.25\")..."
echo "^XA^PW675^LL375^JUS^XZ" | lp -d $PRINTER_NAME
sleep 2
echo "Setting darkness to 18..."
echo "^XA^MD18^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^MD18^FO50,50^ADN,36,20^FDTest^FS^XZ\" | lp -d $PRINTER_NAME"
```
### Quick Reference: Common ZPL Commands
- `^XA` / `^XZ` - Start/end ZPL command block
- `^PW###` - Print width in dots
- `^LL###` - Label length in dots
- `^MD##` - Darkness (0-30)
- `^FO###,###` - Field origin (X,Y position in dots)
- `^ADN,H,W` - Font: N=default, H=height, W=width
- `^FD` - Field data (the actual text/content)
- `^FS` - Field separator (end of field)
- `^GB` - Graphic box
- `^JUS` - Save configuration
- `^JUF` - Factory reset
- `~JC` - Auto-calibration (doesn't always work)

32
term-output.txt Normal file
View File

@ -0,0 +1,32 @@
trav@custo-kiosk-2:~/Documents/custodisco-kiosk$ ./run.sh
Selected camera index 0 from /dev/v4l/by-id/usb-Ricoh_Company_Ltd._Integrated_Camera-video-index0
cat: /home/trav/Documents/custodiosk/freeze_frame.jpg: No such file or directory
{
"key": "%1dirA93bTh7bxizYIBRXRfs2e+IwwY40AFb7GaXCSnQ=.sha256",
"value": {
"previous": "%C4KbmsEEOgAyhXoXkekhnajJIQcOKRinbztiqp2pAKM=.sha256",
"sequence": 28,
"author": "@apIwMs+ElLT6h4d9nUqqFSWUXAHII1d+Wz9SopR26Lo=.ed25519",
"timestamp": 1769050814594,
"hash": "sha256",
"content": {
"type": "post",
"text": "![photo.jpg](&47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=.sha256)\n\nuhbyhbyg\n\n a #custodisco item ",
"custodisco": "true",
"nft": "mint",
"mentions": [
{
"name": "photo.jpg",
"type": "image/jpeg",
"link": "&47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=.sha256"
}
]
},
"signature": "ww9R9j+UMOsCzQpDYkbPZLe2BWUz/qRiUqdvbqWs4CYZyrOZuO6AmeFsmqGWnT0KxP+EkjFhqDOEEK3LxIfGAw==.sig.ed25519"
},
"timestamp": 1769050814596
}
%1dirA93bTh7bxizYIBRXRfs2e+IwwY40AFb7GaXCSnQ=.sha256

View File

@ -9,6 +9,8 @@ class ZPLConveter:
self.total = 0
self.width_bytes = 0
self.compress_hex = False
self.print_width = None
self.label_length = 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',
@ -112,7 +114,14 @@ class ZPLConveter:
return ''.join(sb_code)
def head_doc(self):
return "^XA " + "^FO0,0^GFA," + str(self.total) + "," + str(self.total) + "," + str(self.width_bytes) + ", "
zpl = "^XA"
if self.print_width:
zpl += f"^PW{self.print_width}"
if self.label_length:
zpl += f"^LL{self.label_length}"
zpl += "^MNN" # Continuous media (no gaps)
zpl += f" ^FO0,0^GFA,{self.total},{self.total},{self.width_bytes}, "
return zpl
@staticmethod
def foot_doc():
@ -124,9 +133,11 @@ class ZPLConveter:
def set_blackness_limit_percentage(self, percentage):
self.black_limit = (percentage * 768 // 100)
def print_to_zpl(img_path):
def print_to_zpl(img_path, print_width=None, label_length=None):
converter = ZPLConveter()
converter.set_compress_hex(True)
converter.print_width = print_width
converter.label_length = label_length
return converter.convert_from_img(img_path)
if __name__ == "__main__":