checkpoint

This commit is contained in:
2026-02-24 19:29:15 -05:00
parent 77fd097b4a
commit b9ac998fb4
9 changed files with 283 additions and 11 deletions

View File

@ -43,7 +43,7 @@ class Kiosk(tk.Tk):
def add_home_button(self, frame):
# Create the "Start Over" button
home_button = tk.Button(frame, text="Start Over from the beginning", command=self.show_warning_dialog, bg='peach puff', width=24, font=GlobalVars.BUTTON_FONT)
home_button = tk.Button(frame, text="Start over from the beginning", command=self.show_warning_dialog, bg='peach puff', width=24, font=GlobalVars.BUTTON_FONT)
home_button.place(x=0, y=0) # top-left corner
def show_warning_dialog(self):

179
kiosk/screens/add_user.py Normal file
View File

@ -0,0 +1,179 @@
"""Add User screen - scan QR code of SSB public key."""
import tkinter as tk
from PIL import Image, ImageTk
import cv2
import json
import re
import time
from pyzbar.pyzbar import decode as pyzbar_decode
from kiosk.config import CONFIG, BG_COLOR
from kiosk.state import GlobalVars
from kiosk.utils import get_preferred_camera_index
from kiosk.widgets import RoundedLabel
SSB_KEY_REGEX = re.compile(r'^@[A-Za-z0-9+/]{43}=\.ed25519$')
class ScreenAddUser(tk.Frame):
"""Scan a QR code containing an SSB public key to add a new user."""
def __init__(self, master):
tk.Frame.__init__(self, master, bg=BG_COLOR)
master.add_home_button(self)
self.scanned_key = None
# Main container
self.main_container = tk.Frame(self, bg=BG_COLOR)
self.main_container.pack(fill='both', expand=True, pady=(60, 0))
# Left side: instructions
self.instruction_frame = tk.Frame(self.main_container, bg=BG_COLOR)
self.instruction_frame.pack(side='left', fill='both', expand=True, padx=20, pady=20)
self.instruction_label = RoundedLabel(
self.instruction_frame,
text="You can input your SSB public key into the kiosk via QR code. First, copy your public key from your Scuttlebutt client (in Manyverse you'll want to tap your key and then tap 'Copy cypherlink'.\n\nNext, navigate to www.cust.ooo/ssb and paste your key into the text box on that page and click Generate QR. This will generate a QR code for you. Hold this code in front of the kiosk to scan the code and load your public key into this kiosk.",
font=("Georgia", 16),
bg='white',
wraplength=400,
justify='left'
)
self.instruction_label.pack(expand=True)
# Right side container
self.right_frame = tk.Frame(self.main_container, bg=BG_COLOR)
self.right_frame.pack(side='right', fill='both', expand=True, padx=20, pady=20)
# Video feed
self.video = tk.Label(self.right_frame, bg='black')
self.video.pack(pady=(0, 10))
# Status/detected key text
self.status_label = tk.Label(
self.right_frame,
text="Waiting for QR code...",
font=("Georgia", 14),
bg=BG_COLOR, fg='black',
wraplength=500,
justify='center'
)
self.status_label.pack(pady=(0, 10))
# Button row
self.button_frame = tk.Frame(self.right_frame, bg=BG_COLOR)
self.button_frame.pack(pady=10)
# Cancel button
self.cancel_button = tk.Button(
self.button_frame,
text="Cancel",
command=self._go_back,
height=2, width=15,
bg='peach puff',
font=GlobalVars.BUTTON_FONT
)
self.cancel_button.pack(side='left', padx=10)
# Done button (hidden until valid key scanned)
self.done_button = tk.Button(
self.button_frame,
text="Yes!",
command=self._done,
height=2, width=15,
bg='peach puff',
font=GlobalVars.BUTTON_FONT
)
# Don't pack yet - shown after valid scan
# Start camera
self.cap = cv2.VideoCapture(get_preferred_camera_index(CONFIG["camera"]["preferred_name"]))
self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
self.frame_count = 0
self.TIMEOUT_SECONDS = 120
self.start_time = time.time()
self.update_frame()
def update_frame(self):
# Check for idle timeout
if time.time() - self.start_time >= self.TIMEOUT_SECONDS:
self.cap.release()
from kiosk.screens.home import Screen0
self.master.switch_frame(Screen0)
return
ret, frame = self.cap.read()
self.frame_count += 1
if ret:
# Process every 3rd frame for QR detection
if self.frame_count % 3 == 0 and self.scanned_key is None:
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
results = pyzbar_decode(gray)
if results:
qr_value = results[0].data.decode('utf-8')
if qr_value:
self.start_time = time.time() # Reset idle timer
if SSB_KEY_REGEX.match(qr_value):
self.scanned_key = qr_value
self.status_label.configure(
text=f"Is this your key?\n{qr_value}",
fg='dark green'
)
self.done_button.pack(side='left', padx=10)
else:
self.status_label.configure(
text="Sorry, we need a QR code of a valid Scuttlebutt public key",
fg='red'
)
# Display frame
rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
image = Image.fromarray(rgb_frame)
imgtk = ImageTk.PhotoImage(image=image)
self.video.imgtk = imgtk
self.video.configure(image=imgtk)
self.after(10, self.update_frame)
def _go_back(self):
self.cap.release()
from kiosk.screens.ssb_selection import Screen2
self.master.switch_frame(Screen2)
def _done(self):
if self.scanned_key is None:
return
GlobalVars.selected_user = self.scanned_key
self._save_user(self.scanned_key)
self.cap.release()
from kiosk.screens.camera import Screen3
self.master.switch_frame(Screen3)
def _save_user(self, key):
"""Append user to users.json if not already present."""
try:
with open('users.json') as f:
users = json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
users = []
# Check if already exists
if any(u['id'] == key for u in users):
return
users.append({"id": key, "alias": ""})
with open('users.json', 'w') as f:
json.dump(users, f)
def destroy(self):
if self.cap.isOpened():
self.cap.release()
super().destroy()

View File

@ -21,7 +21,7 @@ class Screen3(tk.Frame):
tk.Frame.__init__(self, master, bg=BG_COLOR)
# Create the "Start Over" button
home_button = tk.Button(text="Start Over", command=self.show_warning_dialog, bg='peach puff', font=GlobalVars.BUTTON_FONT)
home_button = tk.Button(text="Start over from the beginning", command=self.show_warning_dialog, bg='peach puff', font=GlobalVars.BUTTON_FONT)
home_button.place(x=0, y=0) # top-left corner
# Prefer the external NexiGo USB webcam when available

View File

@ -199,8 +199,8 @@ class Screen8(tk.Frame, DrawingMixin):
col1_frame.pack(side='left', padx=(15, 25), anchor='n', pady=20)
# Start Over button (replaces home button) - Screen8
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))
tk.Button(col1_frame, text="Start over from the beginning", command=master.show_warning_dialog,
height=2, width=24, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(pady=(0, 20))
# Instruction label
self.label = RoundedLabel(col1_frame, text="Draw your\nribbon :)",

View File

@ -177,8 +177,8 @@ class NoQRDrawingScreen(tk.Frame, DrawingMixin):
col1_frame.pack(side='left', padx=(15, 25), anchor='n', pady=20)
# Start Over button - NoQRDrawingScreen
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))
tk.Button(col1_frame, text="Start over from the beginning", command=master.show_warning_dialog,
height=2, width=24, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(pady=(0, 20))
# Instruction label
self.label = RoundedLabel(col1_frame, text="Draw your\nribbon :)",

View File

@ -93,8 +93,8 @@ class ParticipationStickerDrawingScreen(tk.Frame, DrawingMixin):
col1_frame = tk.Frame(main_container, bg=BG_COLOR)
col1_frame.pack(side='left', padx=(15, 25), anchor='n', pady=20)
tk.Button(col1_frame, text="Start Over", command=master.show_warning_dialog,
height=2, width=14, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(pady=(0, 20))
tk.Button(col1_frame, text="Start over from the beginning", command=master.show_warning_dialog,
height=2, width=24, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(pady=(0, 20))
self.label = RoundedLabel(col1_frame, text="Draw your\nsticker :)",
wraplength=150, font=GlobalVars.TEXT_FONT, bg='white')
@ -181,8 +181,8 @@ class ParticipationRibbonDrawingScreen(tk.Frame, DrawingMixin):
col1_frame = tk.Frame(main_container, bg=BG_COLOR)
col1_frame.pack(side='left', padx=(5, 10), anchor='n', pady=20)
tk.Button(col1_frame, text="Start Over", command=master.show_warning_dialog,
height=2, width=14, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(pady=(0, 20))
tk.Button(col1_frame, text="Start over from the beginning", command=master.show_warning_dialog,
height=2, width=24, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(pady=(0, 20))
self.label = RoundedLabel(col1_frame, text="Draw your\nribbon :)",
wraplength=150, font=GlobalVars.TEXT_FONT, bg='white')

View File

@ -26,7 +26,7 @@ class Screen13(tk.Frame):
container.place(relx=0.5, rely=0.5, anchor='center')
# instructions
RoundedLabel(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)
RoundedLabel(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 from the beginning if you like.", wraplength=600, font=GlobalVars.TEXT_FONT, bg='white').grid(row=0, column=0, columnspan=2)
# buttons
master.add_home_button(self)

View File

@ -118,6 +118,15 @@ class Screen2(tk.Frame):
GlobalVars.selected_user = None
go_to_screen3()
# The 'Add User' button to scan a QR code
def go_to_add_user():
from kiosk.screens.add_user import ScreenAddUser
master.switch_frame(ScreenAddUser)
self.add_user_button = tk.Button(self.button_frame, text="Add User", command=go_to_add_user,
height=2, width=20, bg='peach puff', font=GlobalVars.BUTTON_FONT)
self.add_user_button.pack(side="left", padx=(10, 10))
# The 'Cancel' button to skip without selecting a user
self.cancel_button = tk.Button(self.button_frame, text="Cancel", command=cancel_selection,
height=2, width=20, bg='peach puff', font=GlobalVars.BUTTON_FONT)

84
ssb-qr.html Normal file

File diff suppressed because one or more lines are too long