in a good spot
This commit is contained in:
8
.gitignore
vendored
8
.gitignore
vendored
@ -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
3
agents.md
Normal 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.
|
||||
204
archive/migration_items_feature.py
Normal file
204
archive/migration_items_feature.py
Normal 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
15
config.example.json
Normal 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
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.9 KiB |
77
install.sh
Executable file
77
install.sh
Executable 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
690
kiosk.py
@ -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
112
list_cameras.py
Executable 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()
|
||||
281
nylon-printer-setup-guide.md
Normal file
281
nylon-printer-setup-guide.md
Normal 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
11
package.json
Normal 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
6
requirements.txt
Normal file
@ -0,0 +1,6 @@
|
||||
Pillow
|
||||
opencv-python
|
||||
numpy
|
||||
qrcode
|
||||
qreader
|
||||
pyzbar
|
||||
18
run.sh
Executable file
18
run.sh
Executable 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
BIN
scan-tag.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.0 KiB |
199
sticker-printer-setup-guide.md
Normal file
199
sticker-printer-setup-guide.md
Normal 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
32
term-output.txt
Normal 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": "\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
|
||||
|
||||
|
||||
15
tozpl.py
15
tozpl.py
@ -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__":
|
||||
|
||||
Reference in New Issue
Block a user