new fixes, yay
This commit is contained in:
176
docs/SCREENSAVER.md
Normal file
176
docs/SCREENSAVER.md
Normal file
@ -0,0 +1,176 @@
|
||||
# Custo Screensaver
|
||||
|
||||
A custom screensaver for the kiosk that displays animated "custo" text and manages display power.
|
||||
|
||||
## How It Works
|
||||
|
||||
The screensaver has two stages:
|
||||
|
||||
```
|
||||
0 min ──────────────────> 10 min ──────────────────> 30 min
|
||||
(user active) "custo" appears screen blanks
|
||||
(wavy animation) (backlight off)
|
||||
```
|
||||
|
||||
1. **Stage 1 - Animation** (after 10 minutes idle): A fullscreen black window appears with the word "custo" in large white text. The text gently waves up and down and fades in/out.
|
||||
|
||||
2. **Stage 2 - Screen Blank** (after 30 minutes total / 20 more minutes): The display backlight turns off completely to save power.
|
||||
|
||||
**Wake triggers**: Any of these will restore the screen and exit the screensaver:
|
||||
- Touch/tap the screen
|
||||
- Press any key
|
||||
- Move the mouse significantly (>50 pixels)
|
||||
|
||||
## Components
|
||||
|
||||
| File | Location | Purpose |
|
||||
|------|----------|---------|
|
||||
| `custo-screensaver.sh` | `~/.local/bin/` | Monitors idle time via GNOME D-Bus, launches display |
|
||||
| `custo-display.py` | `~/.local/bin/` | GTK4 app showing animated text, handles blanking |
|
||||
| `custo-screensaver.desktop` | `~/.config/autostart/` | Starts the monitor on login |
|
||||
| `90-backlight.rules` | `/etc/udev/rules.d/` | Allows non-root backlight control |
|
||||
|
||||
## Configuration
|
||||
|
||||
Edit `~/.local/bin/custo-screensaver.sh`:
|
||||
|
||||
```bash
|
||||
start_after=10 # minutes until screensaver appears
|
||||
blank_after=20 # additional minutes until screen blanks
|
||||
```
|
||||
|
||||
To change the wave animation amplitude, edit `~/.local/bin/custo-display.py`:
|
||||
|
||||
```python
|
||||
wave_offset = 60 * math.sin(self.time * 0.4) # ±60 pixels
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
### Automatic
|
||||
|
||||
Run the install script from the repo:
|
||||
|
||||
```bash
|
||||
./install.sh
|
||||
```
|
||||
|
||||
This will:
|
||||
- Copy screensaver files to `~/.local/bin/`
|
||||
- Set up autostart
|
||||
- Install the udev rule (requires sudo)
|
||||
- Kill any existing screensaver instances
|
||||
|
||||
### Manual
|
||||
|
||||
1. Copy the scripts:
|
||||
```bash
|
||||
cp screensaver/custo-screensaver.sh ~/.local/bin/
|
||||
cp screensaver/custo-display.py ~/.local/bin/
|
||||
chmod +x ~/.local/bin/custo-screensaver.sh
|
||||
chmod +x ~/.local/bin/custo-display.py
|
||||
```
|
||||
|
||||
2. Set up autostart:
|
||||
```bash
|
||||
mkdir -p ~/.config/autostart
|
||||
cp screensaver/custo-screensaver.desktop ~/.config/autostart/
|
||||
```
|
||||
|
||||
3. Install udev rule for backlight control:
|
||||
```bash
|
||||
sudo cp screensaver/90-backlight.rules /etc/udev/rules.d/
|
||||
sudo udevadm control --reload-rules
|
||||
sudo udevadm trigger
|
||||
```
|
||||
|
||||
4. Add your user to the video group (if not already):
|
||||
```bash
|
||||
sudo usermod -aG video $USER
|
||||
```
|
||||
Log out and back in for group changes to take effect.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Check if screensaver is running
|
||||
|
||||
```bash
|
||||
ps aux | grep custo-screensaver
|
||||
```
|
||||
|
||||
Should show exactly ONE instance of `custo-screensaver.sh`.
|
||||
|
||||
### Kill all screensaver instances
|
||||
|
||||
```bash
|
||||
pkill -f custo-screensaver.sh
|
||||
pkill -f custo-display.py
|
||||
```
|
||||
|
||||
### Test the screensaver manually
|
||||
|
||||
```bash
|
||||
# Test the display (Ctrl+C or tap to exit)
|
||||
python3 ~/.local/bin/custo-display.py --blank-after=1
|
||||
|
||||
# Start the monitor with short timeout for testing
|
||||
start_after=1 ~/.local/bin/custo-screensaver.sh
|
||||
```
|
||||
|
||||
### Screen doesn't blank (permission denied)
|
||||
|
||||
The backlight file requires write permission. Check:
|
||||
|
||||
```bash
|
||||
ls -la /sys/class/backlight/intel_backlight/brightness
|
||||
```
|
||||
|
||||
If it shows `-rw-r--r--` (no group write), the udev rule isn't active:
|
||||
|
||||
```bash
|
||||
sudo udevadm control --reload-rules
|
||||
sudo udevadm trigger
|
||||
```
|
||||
|
||||
Or manually fix permissions:
|
||||
|
||||
```bash
|
||||
sudo chgrp video /sys/class/backlight/intel_backlight/brightness
|
||||
sudo chmod g+w /sys/class/backlight/intel_backlight/brightness
|
||||
```
|
||||
|
||||
### Multiple instances running
|
||||
|
||||
The script now auto-kills duplicates on startup. If duplicates persist:
|
||||
|
||||
```bash
|
||||
pkill -f custo-screensaver.sh
|
||||
~/.local/bin/custo-screensaver.sh &
|
||||
```
|
||||
|
||||
### Screensaver not starting on login
|
||||
|
||||
Check autostart is enabled:
|
||||
|
||||
```bash
|
||||
cat ~/.config/autostart/custo-screensaver.desktop
|
||||
```
|
||||
|
||||
The `X-GNOME-Autostart-enabled` should be `true`.
|
||||
|
||||
### Check idle time
|
||||
|
||||
```bash
|
||||
dbus-send --print-reply --dest=org.gnome.Mutter.IdleMonitor \
|
||||
/org/gnome/Mutter/IdleMonitor/Core \
|
||||
org.gnome.Mutter.IdleMonitor.GetIdletime
|
||||
```
|
||||
|
||||
Returns idle time in milliseconds.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- GNOME/Mutter (for idle monitoring via D-Bus)
|
||||
- GTK4 (for the display window)
|
||||
- Python 3 with PyGObject
|
||||
- `bc` (for bash arithmetic)
|
||||
42
install.sh
42
install.sh
@ -62,6 +62,46 @@ else
|
||||
echo "Skipping Node.js dependencies (npm not found)"
|
||||
fi
|
||||
|
||||
# Install screensaver
|
||||
echo
|
||||
echo "Installing screensaver..."
|
||||
|
||||
# Kill any existing screensaver instances
|
||||
echo " Stopping existing screensaver instances..."
|
||||
pkill -f custo-screensaver.sh 2>/dev/null || true
|
||||
pkill -f custo-display.py 2>/dev/null || true
|
||||
|
||||
# Create directories
|
||||
mkdir -p ~/.local/bin
|
||||
mkdir -p ~/.config/autostart
|
||||
|
||||
# Copy screensaver files
|
||||
echo " Copying screensaver files..."
|
||||
cp screensaver/custo-screensaver.sh ~/.local/bin/
|
||||
cp screensaver/custo-display.py ~/.local/bin/
|
||||
chmod +x ~/.local/bin/custo-screensaver.sh
|
||||
chmod +x ~/.local/bin/custo-display.py
|
||||
|
||||
# Set up autostart
|
||||
cp screensaver/custo-screensaver.desktop ~/.config/autostart/
|
||||
|
||||
# Install udev rule for backlight control
|
||||
echo " Installing udev rule for backlight control (requires sudo)..."
|
||||
if [ -f screensaver/90-backlight.rules ]; then
|
||||
sudo cp screensaver/90-backlight.rules /etc/udev/rules.d/
|
||||
sudo udevadm control --reload-rules
|
||||
sudo udevadm trigger
|
||||
fi
|
||||
|
||||
# Check if user is in video group
|
||||
if ! groups | grep -q video; then
|
||||
echo " Adding $USER to video group for backlight control..."
|
||||
sudo usermod -aG video $USER
|
||||
echo " NOTE: Log out and back in for group changes to take effect."
|
||||
fi
|
||||
|
||||
echo " Screensaver installed. It will start automatically on next login."
|
||||
|
||||
echo
|
||||
echo "=== Installation Complete ==="
|
||||
echo
|
||||
@ -74,4 +114,6 @@ echo " python kiosk.py"
|
||||
echo
|
||||
echo "Notes:"
|
||||
echo " - Make sure ssb-server is running before using SSB features"
|
||||
echo " - Screensaver: activates after 10 min idle, blanks screen after 30 min total"
|
||||
echo " - See docs/SCREENSAVER.md for screensaver configuration"
|
||||
echo
|
||||
|
||||
@ -99,3 +99,12 @@ journalctl --user -u ssb-server.service --no-pager -n 50
|
||||
```bash
|
||||
ps aux | grep kiosk.py
|
||||
```
|
||||
|
||||
### SSB Health Check (Automatic)
|
||||
|
||||
When users click "Generate Tag" or "Lookup Item", the kiosk automatically:
|
||||
1. Runs `ssb-server whoami` to verify the server is running
|
||||
2. Checks the response is valid JSON with an `id` field
|
||||
3. If check fails, shows "SSB rebooting..." and runs `systemctl --user restart ssb-server.service`
|
||||
4. Waits 2 seconds and retries `ssb-server whoami`
|
||||
5. If still failing, shows a popup error asking user to contact kiosk steward
|
||||
|
||||
@ -29,6 +29,7 @@ class Screen3(tk.Frame):
|
||||
self.freeze_frame = None
|
||||
self.countdown_text = None
|
||||
self.last_photo = None
|
||||
self.photo_taken = False
|
||||
|
||||
# Video feed
|
||||
self.canvas = tk.Canvas(self, width=self.vid.get(cv2.CAP_PROP_FRAME_WIDTH), height=self.vid.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
||||
@ -105,6 +106,7 @@ class Screen3(tk.Frame):
|
||||
rgb_image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
||||
pil_image = Image.fromarray(rgb_image)
|
||||
pil_image.save('freeze_frame.jpg')
|
||||
self.photo_taken = True
|
||||
self.display_taken_photo()
|
||||
|
||||
self.is_capturing = False
|
||||
@ -116,6 +118,9 @@ class Screen3(tk.Frame):
|
||||
self.canvas.create_image(0, 0, image=self.last_photo, anchor='nw')
|
||||
|
||||
def done(self):
|
||||
if not self.photo_taken:
|
||||
messagebox.showwarning("Photo Required", "Please take a photo first")
|
||||
return
|
||||
self.release_resources()
|
||||
from kiosk.screens.description import Screen5
|
||||
self.master.switch_frame(Screen5)
|
||||
@ -184,9 +189,18 @@ class Screen14(tk.Frame):
|
||||
self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
|
||||
|
||||
self.frame_count = 0
|
||||
self.TIMEOUT_SECONDS = 120 # 2 minutes idle timeout
|
||||
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
|
||||
|
||||
|
||||
@ -14,7 +14,7 @@ class Screen5(tk.Frame):
|
||||
master.add_home_button(self)
|
||||
|
||||
# Adding the information label
|
||||
self.info_label = RoundedLabel(self, text="Please enter a description of your item (maybe some stories of its history or why it's important to you or identifying features). You may can use Markdown formatting. Once posted this text is on the internet FOREVER.", font=GlobalVars.TEXT_FONT, wraplength=900, bg='white')
|
||||
self.info_label = RoundedLabel(self, text="Please enter a description of your item (maybe some stories of its history or why it's important to you or identifying features). You can use Markdown formatting. Once posted this text is on the internet FOREVER.", font=GlobalVars.TEXT_FONT, wraplength=900, bg='white')
|
||||
self.info_label.pack(pady=40)
|
||||
|
||||
# Adding the text entry field
|
||||
|
||||
@ -1,12 +1,15 @@
|
||||
"""Home screen (Screen0)."""
|
||||
import tkinter as tk
|
||||
from tkinter import font as tkfont
|
||||
from tkinter import messagebox
|
||||
from PIL import Image, ImageTk
|
||||
import os
|
||||
import time
|
||||
|
||||
from kiosk.config import CONFIG, BG_COLOR
|
||||
from kiosk.state import GlobalVars
|
||||
from kiosk.widgets import RoundedLabel
|
||||
from kiosk.utils import check_ssb_health, restart_ssb_service
|
||||
|
||||
|
||||
class Screen0(tk.Frame):
|
||||
@ -41,10 +44,52 @@ class Screen0(tk.Frame):
|
||||
# Deferred imports to avoid circular dependencies
|
||||
def go_to_generate_tag():
|
||||
from kiosk.screens.intro import HaveYouGeneratedTagScreen
|
||||
|
||||
# Quick SSB health check
|
||||
success, error = check_ssb_health()
|
||||
if not success:
|
||||
# Show reboot message, attempt restart
|
||||
info_label.set_text("SSB rebooting...")
|
||||
self.update() # Force UI update
|
||||
|
||||
restart_success, restart_error = restart_ssb_service()
|
||||
if restart_success:
|
||||
time.sleep(2) # Give service time to stabilize
|
||||
success, error = check_ssb_health()
|
||||
|
||||
if not success:
|
||||
info_label.set_text("this is a touchscreen kiosk\n\n more information on the project at\n\nwww.cust.ooo")
|
||||
messagebox.showerror(
|
||||
"SSB Error",
|
||||
"SSB service is not functioning.\n\nPlease contact the kiosk steward for assistance."
|
||||
)
|
||||
return # Don't proceed to next screen
|
||||
|
||||
master.switch_frame(HaveYouGeneratedTagScreen)
|
||||
|
||||
def go_to_lookup():
|
||||
from kiosk.screens.camera import Screen14
|
||||
|
||||
# Quick SSB health check
|
||||
success, error = check_ssb_health()
|
||||
if not success:
|
||||
# Show reboot message, attempt restart
|
||||
info_label.set_text("SSB rebooting...")
|
||||
self.update() # Force UI update
|
||||
|
||||
restart_success, restart_error = restart_ssb_service()
|
||||
if restart_success:
|
||||
time.sleep(2) # Give service time to stabilize
|
||||
success, error = check_ssb_health()
|
||||
|
||||
if not success:
|
||||
info_label.set_text("this is a touchscreen kiosk\n\n more information on the project at\n\nwww.cust.ooo")
|
||||
messagebox.showerror(
|
||||
"SSB Error",
|
||||
"SSB service is not functioning.\n\nPlease contact the kiosk steward for assistance."
|
||||
)
|
||||
return # Don't proceed to next screen
|
||||
|
||||
master.switch_frame(Screen14)
|
||||
|
||||
def go_to_no_qr():
|
||||
|
||||
@ -81,24 +81,24 @@ It can print stickers and ribbons to affix to an item you care about."""
|
||||
nav_frame = tk.Frame(self, bg=BG_COLOR)
|
||||
nav_frame.grid(row=1, column=0, columnspan=2, sticky='ew', pady=20)
|
||||
|
||||
# Dot progress indicator
|
||||
self.dots_label = tk.Label(nav_frame, text="", font=GlobalVars.TEXT_FONT, bg=BG_COLOR)
|
||||
self.dots_label.pack(side='left', expand=True)
|
||||
self.dot_count = 0
|
||||
# Countdown number indicator
|
||||
self.countdown_label = tk.Label(nav_frame, text="8", font=("Helvetica", 20), bg=BG_COLOR, fg='gray40')
|
||||
self.countdown_label.pack(side='left', expand=True)
|
||||
self.countdown = 8
|
||||
|
||||
def add_dot():
|
||||
self.dot_count += 1
|
||||
self.dots_label.config(text="." * self.dot_count)
|
||||
if self.dot_count < 8:
|
||||
self.after(1000, add_dot)
|
||||
def update_countdown():
|
||||
self.countdown -= 1
|
||||
if self.countdown > 0:
|
||||
self.countdown_label.config(text=str(self.countdown))
|
||||
self.after(1000, update_countdown)
|
||||
|
||||
self.after(1000, add_dot)
|
||||
self.after(1000, update_countdown)
|
||||
|
||||
self.forward_btn = tk.Button(nav_frame, text="Forward →", command=lambda: master.switch_frame(InfoPage2),
|
||||
height=2, width=15, bg='peach puff', font=GlobalVars.BUTTON_FONT)
|
||||
|
||||
def show_forward():
|
||||
self.dots_label.pack_forget()
|
||||
self.countdown_label.pack_forget()
|
||||
self.forward_btn.pack(side='right', padx=40)
|
||||
|
||||
# Show forward button after 8 second delay
|
||||
@ -150,24 +150,24 @@ by tagging an item you are saying to it, 'you matter, I will take care of you'""
|
||||
tk.Button(nav_frame, text="← Back", command=lambda: master.switch_frame(InfoPage1),
|
||||
height=2, width=15, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(side='left', padx=40)
|
||||
|
||||
# Dot progress indicator (centered)
|
||||
self.dots_label = tk.Label(nav_frame, text="", font=GlobalVars.TEXT_FONT, bg=BG_COLOR)
|
||||
self.dots_label.pack(side='left', expand=True)
|
||||
self.dot_count = 0
|
||||
# Countdown number indicator
|
||||
self.countdown_label = tk.Label(nav_frame, text="8", font=("Helvetica", 20), bg=BG_COLOR, fg='gray40')
|
||||
self.countdown_label.pack(side='left', expand=True)
|
||||
self.countdown = 8
|
||||
|
||||
def add_dot():
|
||||
self.dot_count += 1
|
||||
self.dots_label.config(text="." * self.dot_count)
|
||||
if self.dot_count < 8:
|
||||
self.after(1000, add_dot)
|
||||
def update_countdown():
|
||||
self.countdown -= 1
|
||||
if self.countdown > 0:
|
||||
self.countdown_label.config(text=str(self.countdown))
|
||||
self.after(1000, update_countdown)
|
||||
|
||||
self.after(1000, add_dot)
|
||||
self.after(1000, update_countdown)
|
||||
|
||||
self.forward_btn = tk.Button(nav_frame, text="Forward →", command=lambda: master.switch_frame(InfoPage3),
|
||||
height=2, width=15, bg='peach puff', font=GlobalVars.BUTTON_FONT)
|
||||
|
||||
def show_forward():
|
||||
self.dots_label.pack_forget()
|
||||
self.countdown_label.pack_forget()
|
||||
self.forward_btn.pack(side='right', padx=40)
|
||||
|
||||
# Show forward button after 8 second delay
|
||||
@ -210,24 +210,24 @@ The QR codes contain a Scuttlebutt messageID, scanning it works best at www.cust
|
||||
tk.Button(nav_frame, text="← Back", command=lambda: master.switch_frame(InfoPage2),
|
||||
height=2, width=15, bg='peach puff', font=GlobalVars.BUTTON_FONT).pack(side='left', padx=40)
|
||||
|
||||
# Dot progress indicator (centered)
|
||||
self.dots_label = tk.Label(nav_frame, text="", font=GlobalVars.TEXT_FONT, bg=BG_COLOR)
|
||||
self.dots_label.pack(side='left', expand=True)
|
||||
self.dot_count = 0
|
||||
# Countdown number indicator
|
||||
self.countdown_label = tk.Label(nav_frame, text="8", font=("Helvetica", 20), bg=BG_COLOR, fg='gray40')
|
||||
self.countdown_label.pack(side='left', expand=True)
|
||||
self.countdown = 8
|
||||
|
||||
def add_dot():
|
||||
self.dot_count += 1
|
||||
self.dots_label.config(text="." * self.dot_count)
|
||||
if self.dot_count < 8:
|
||||
self.after(1000, add_dot)
|
||||
def update_countdown():
|
||||
self.countdown -= 1
|
||||
if self.countdown > 0:
|
||||
self.countdown_label.config(text=str(self.countdown))
|
||||
self.after(1000, update_countdown)
|
||||
|
||||
self.after(1000, add_dot)
|
||||
self.after(1000, update_countdown)
|
||||
|
||||
self.forward_btn = tk.Button(nav_frame, text="Forward →", command=go_to_screen1,
|
||||
height=2, width=15, bg='peach puff', font=GlobalVars.BUTTON_FONT)
|
||||
|
||||
def show_forward():
|
||||
self.dots_label.pack_forget()
|
||||
self.countdown_label.pack_forget()
|
||||
self.forward_btn.pack(side='right', padx=40)
|
||||
|
||||
# Show forward button after 8 second delay
|
||||
|
||||
@ -386,6 +386,8 @@ class NoQRPrintScreen(tk.Frame):
|
||||
# Get config values
|
||||
ribbon_width = CONFIG.get("ribbon", {}).get("width", 450)
|
||||
margin = CONFIG.get("ribbon", {}).get("margin", 50)
|
||||
has_cutter = CONFIG.get("ribbon", {}).get("has_cutter", False)
|
||||
top_margin = margin + (0 if has_cutter else 100)
|
||||
printable_width = ribbon_width - 50 # 25px side margins
|
||||
|
||||
# Load the drawing
|
||||
@ -398,12 +400,12 @@ class NoQRPrintScreen(tk.Frame):
|
||||
drawing_height = int(printable_width * multiplier)
|
||||
|
||||
# Total label height: just margins + drawing (no QR, no scan-tag)
|
||||
total_height = margin + drawing_height + margin
|
||||
total_height = top_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))
|
||||
# Paste drawing 25px in from left, top_margin down from top
|
||||
merged_image.paste(drawing, (25, top_margin))
|
||||
merged_image.save("merged_image.png")
|
||||
|
||||
# Get the ZPL code for the image
|
||||
|
||||
@ -133,6 +133,8 @@ class Screen13(tk.Frame):
|
||||
# Get config values
|
||||
ribbon_width = CONFIG.get("ribbon", {}).get("width", 450)
|
||||
margin = CONFIG.get("ribbon", {}).get("margin", 50)
|
||||
has_cutter = CONFIG.get("ribbon", {}).get("has_cutter", False)
|
||||
top_margin = margin + (0 if has_cutter else 100)
|
||||
printable_width = ribbon_width - 50 # 25px side margins
|
||||
|
||||
multiplier = {'small': 0.5, 'medium': 1.0, 'large': 1.5}.get(GlobalVars.ribbon_size, 1.0)
|
||||
@ -146,18 +148,18 @@ class Screen13(tk.Frame):
|
||||
qr_size = qr_img.width
|
||||
|
||||
# Total label height includes top/bottom margins and scan-tag
|
||||
total_height = margin + drawing_height + scan_tag_height + qr_size + margin
|
||||
total_height = top_margin + drawing_height + scan_tag_height + qr_size + 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))
|
||||
# Paste drawing 25px in from left, top_margin down from top
|
||||
merged_image.paste(drawing, (25, top_margin))
|
||||
# Paste scan-tag centered below drawing
|
||||
scan_tag_x = (ribbon_width - scan_tag.width) // 2
|
||||
merged_image.paste(scan_tag, (scan_tag_x, margin + drawing_height))
|
||||
merged_image.paste(scan_tag, (scan_tag_x, top_margin + drawing_height))
|
||||
# Center QR horizontally within the full ribbon width, below scan-tag
|
||||
qr_x = (ribbon_width - qr_size) // 2
|
||||
merged_image.paste(qr_img, (qr_x, margin + drawing_height + scan_tag_height))
|
||||
merged_image.paste(qr_img, (qr_x, top_margin + drawing_height + scan_tag_height))
|
||||
merged_image.save("merged_image.png")
|
||||
|
||||
# Get the ZPL code for the image
|
||||
@ -194,6 +196,10 @@ class Screen13(tk.Frame):
|
||||
print('traceback.format_exc():\n%s' % traceback.format_exc())
|
||||
exit()
|
||||
|
||||
# Delete the photo to prevent reuse in next session
|
||||
if os.path.exists(path_to_image):
|
||||
os.remove(path_to_image)
|
||||
|
||||
from kiosk.screens.completion import Screen10
|
||||
self.master.switch_frame(Screen10) # Switching to Screen10 after Done
|
||||
|
||||
@ -209,7 +215,8 @@ class PrintFailedScreen(tk.Frame):
|
||||
|
||||
# Troubleshooting instructions
|
||||
troubleshoot_text = """if print failed:
|
||||
|
||||
0. if the issue is simply a vertical misalignment (the print is not vertically centered on the sticker), a simple reprint may work.
|
||||
for more involved errors:
|
||||
1. un-hook the red nylon cord and carefully pull out printer.
|
||||
2. there are teal latches on either side of the printer: pull them towards you and lift the lid all the way open.
|
||||
3. make sure there is enough resin and printable nylon/stickers and they are lined up straight
|
||||
@ -217,8 +224,7 @@ class PrintFailedScreen(tk.Frame):
|
||||
5. close printer and press the button on top of printer to stop the green light from flashing
|
||||
6. put printer back, re-hook the nylon cord and try printing again.
|
||||
|
||||
if you're still having trouble you could try searching the internet for Zebra GX430T troubleshooting
|
||||
AND OR
|
||||
if you're still having trouble you could try searching the internet for Zebra GX430T troubleshooting AND OR
|
||||
please email maintenance@cust.ooo
|
||||
|
||||
thanks!"""
|
||||
|
||||
@ -2,6 +2,8 @@
|
||||
import os
|
||||
import re
|
||||
import glob
|
||||
import json
|
||||
import subprocess
|
||||
import cv2
|
||||
|
||||
|
||||
@ -75,6 +77,55 @@ def get_preferred_camera_index(preferred_name_fragment=None):
|
||||
return 0
|
||||
|
||||
|
||||
def check_ssb_health() -> tuple[bool, str | None]:
|
||||
"""
|
||||
Run 'ssb-server whoami' and verify it returns valid JSON with an 'id' field.
|
||||
Returns: (True, None) on success, (False, error_message) on failure.
|
||||
"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['ssb-server', 'whoami'],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10
|
||||
)
|
||||
if result.returncode == 0:
|
||||
data = json.loads(result.stdout)
|
||||
if data.get('id'): # Just check that id exists and is truthy
|
||||
return (True, None)
|
||||
return (False, "ssb-server whoami returned no id")
|
||||
return (False, f"ssb-server whoami failed: {result.stderr}")
|
||||
except subprocess.TimeoutExpired:
|
||||
return (False, "ssb-server whoami timed out")
|
||||
except json.JSONDecodeError:
|
||||
return (False, "Failed to parse ssb-server response")
|
||||
except FileNotFoundError:
|
||||
return (False, "ssb-server command not found")
|
||||
except Exception as e:
|
||||
return (False, str(e))
|
||||
|
||||
|
||||
def restart_ssb_service() -> tuple[bool, str | None]:
|
||||
"""
|
||||
Restart ssb-server.service using systemctl --user.
|
||||
Returns: (True, None) on success, (False, error_message) on failure.
|
||||
"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['systemctl', '--user', 'restart', 'ssb-server.service'],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return (True, None)
|
||||
return (False, f"Service restart failed: {result.stderr}")
|
||||
except subprocess.TimeoutExpired:
|
||||
return (False, "Service restart timed out")
|
||||
except Exception as e:
|
||||
return (False, str(e))
|
||||
|
||||
|
||||
def sanitize_for_ssb(text):
|
||||
"""Sanitize text for safe passage through shell and JSON."""
|
||||
# Order matters: escape backslashes first
|
||||
@ -85,8 +136,6 @@ def sanitize_for_ssb(text):
|
||||
text = text.replace('$', '\\$')
|
||||
# Escape backticks to prevent shell command substitution
|
||||
text = text.replace('`', '\\`')
|
||||
# Escape newlines for JSON
|
||||
text = text.replace('\n', '\\n')
|
||||
# Escape carriage returns
|
||||
text = text.replace('\r', '')
|
||||
# Escape tabs
|
||||
|
||||
@ -106,10 +106,15 @@ class DrawingMixin:
|
||||
|
||||
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
|
||||
# Crop if larger than canvas (top-left aligned crop)
|
||||
if img.size[0] > self.original_width or img.size[1] > self.original_height:
|
||||
img = img.crop((0, 0, min(img.size[0], self.original_width),
|
||||
min(img.size[1], self.original_height)))
|
||||
|
||||
# Paste onto blank canvas (preserves pixel accuracy, smaller images get white border)
|
||||
self.drawing = Image.new('1', (self.original_width, self.original_height), 1)
|
||||
self.drawing.paste(img, (0, 0)) # Top-left aligned
|
||||
self.draw = ImageDraw.Draw(self.drawing)
|
||||
|
||||
# Display the image at scaled size
|
||||
|
||||
@ -25,9 +25,13 @@ class RoundedLabel(tk.Canvas):
|
||||
# Draw rounded rectangle
|
||||
self._draw_rounded_rect(0, 0, width, height, radius, bg)
|
||||
|
||||
# Add text
|
||||
self.create_text(width // 2, height // 2, text=text, font=font, fill=fg,
|
||||
width=wraplength, justify=justify)
|
||||
# Add text (with tag for later updates)
|
||||
self.text_id = self.create_text(width // 2, height // 2, text=text, font=font, fill=fg,
|
||||
width=wraplength, justify=justify, tags="label_text")
|
||||
|
||||
def set_text(self, text):
|
||||
"""Update the text displayed in the label."""
|
||||
self.itemconfig(self.text_id, text=text)
|
||||
|
||||
def _draw_rounded_rect(self, x1, y1, x2, y2, radius, fill):
|
||||
"""Draw a rounded rectangle on the canvas."""
|
||||
|
||||
0
no-cut.mp4
Normal file
0
no-cut.mp4
Normal file
BIN
qr.png
BIN
qr.png
Binary file not shown.
|
Before Width: | Height: | Size: 709 B After Width: | Height: | Size: 1012 B |
BIN
ribbon-cut.mp4
BIN
ribbon-cut.mp4
Binary file not shown.
7
screensaver/90-backlight.rules
Normal file
7
screensaver/90-backlight.rules
Normal file
@ -0,0 +1,7 @@
|
||||
# Udev rule to allow video group write access to backlight brightness
|
||||
# This enables the screensaver to turn off the display without root privileges
|
||||
# Install to: /etc/udev/rules.d/90-backlight.rules
|
||||
# Then reload: sudo udevadm control --reload-rules && sudo udevadm trigger
|
||||
|
||||
ACTION=="add", SUBSYSTEM=="backlight", RUN+="/bin/chgrp video /sys/class/backlight/%k/brightness"
|
||||
ACTION=="add", SUBSYSTEM=="backlight", RUN+="/bin/chmod g+w /sys/class/backlight/%k/brightness"
|
||||
188
screensaver/custo-display.py
Executable file
188
screensaver/custo-display.py
Executable file
@ -0,0 +1,188 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Fullscreen text display for screensaver with animations and screen blanking"""
|
||||
|
||||
import gi
|
||||
gi.require_version('Gtk', '4.0')
|
||||
from gi.repository import Gtk, Gdk, GLib
|
||||
import math
|
||||
import sys
|
||||
import argparse
|
||||
import os
|
||||
|
||||
TEXT = "custo"
|
||||
|
||||
# Backlight control paths
|
||||
BACKLIGHT_PATH = "/sys/class/backlight/intel_backlight/brightness"
|
||||
MAX_BRIGHTNESS_PATH = "/sys/class/backlight/intel_backlight/max_brightness"
|
||||
|
||||
|
||||
def get_max_brightness():
|
||||
"""Read the maximum brightness value"""
|
||||
try:
|
||||
with open(MAX_BRIGHTNESS_PATH) as f:
|
||||
return f.read().strip()
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def set_brightness(value):
|
||||
"""Set the backlight brightness"""
|
||||
try:
|
||||
with open(BACKLIGHT_PATH, 'w') as f:
|
||||
f.write(str(value))
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"custo-display: Could not set brightness: {e}")
|
||||
return False
|
||||
|
||||
|
||||
class ScreensaverWindow(Gtk.ApplicationWindow):
|
||||
def __init__(self, blank_after_minutes=20, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.blank_after_minutes = blank_after_minutes
|
||||
self.is_blanked = False
|
||||
self.original_brightness = None
|
||||
|
||||
self.set_css_classes(['screensaver-window'])
|
||||
|
||||
# Create label with large white text
|
||||
self.label = Gtk.Label(label=TEXT)
|
||||
self.label.set_css_classes(['screensaver-text'])
|
||||
|
||||
# Center the label
|
||||
self.set_child(self.label)
|
||||
|
||||
# Animation state
|
||||
self.time = 0.0
|
||||
self.elapsed_seconds = 0
|
||||
|
||||
# Key press controller - close on any key
|
||||
key_controller = Gtk.EventControllerKey()
|
||||
key_controller.connect('key-pressed', self.on_input)
|
||||
self.add_controller(key_controller)
|
||||
|
||||
# Click/touch controller - close on click or tap
|
||||
click_controller = Gtk.GestureClick()
|
||||
click_controller.connect('pressed', self.on_input)
|
||||
self.add_controller(click_controller)
|
||||
|
||||
# Motion controller - close on mouse movement
|
||||
motion_controller = Gtk.EventControllerMotion()
|
||||
motion_controller.connect('motion', self.on_motion)
|
||||
self.add_controller(motion_controller)
|
||||
|
||||
# Track initial position to ignore tiny movements
|
||||
self.initial_x = None
|
||||
self.initial_y = None
|
||||
self.motion_threshold = 50 # pixels of movement before closing
|
||||
|
||||
# Go fullscreen
|
||||
self.fullscreen()
|
||||
|
||||
# Start animation (100ms tick = 10fps, gentle on CPU)
|
||||
GLib.timeout_add(100, self.animate)
|
||||
|
||||
# Store max brightness for restoration
|
||||
self.original_brightness = get_max_brightness()
|
||||
|
||||
print(f"custo-display: Started. Screen will blank after {blank_after_minutes} minutes.")
|
||||
|
||||
def on_input(self, *args):
|
||||
self.restore_and_close()
|
||||
return True
|
||||
|
||||
def on_motion(self, controller, x, y):
|
||||
if self.initial_x is None:
|
||||
self.initial_x = x
|
||||
self.initial_y = y
|
||||
return
|
||||
|
||||
# Only close if mouse moved significantly
|
||||
dx = abs(x - self.initial_x)
|
||||
dy = abs(y - self.initial_y)
|
||||
if dx > self.motion_threshold or dy > self.motion_threshold:
|
||||
self.restore_and_close()
|
||||
|
||||
def restore_and_close(self):
|
||||
"""Restore brightness and close the window"""
|
||||
if self.is_blanked and self.original_brightness:
|
||||
print("custo-display: Restoring brightness")
|
||||
set_brightness(self.original_brightness)
|
||||
self.close()
|
||||
|
||||
def blank_screen(self):
|
||||
"""Turn off the backlight"""
|
||||
if not self.is_blanked:
|
||||
print("custo-display: Blanking screen (backlight off)")
|
||||
if set_brightness(0):
|
||||
self.is_blanked = True
|
||||
# Hide the label since screen is blank
|
||||
self.label.set_visible(False)
|
||||
|
||||
def animate(self):
|
||||
self.time += 0.025 # Slower time progression
|
||||
self.elapsed_seconds += 0.1 # 100ms per tick
|
||||
|
||||
# Check if it's time to blank the screen
|
||||
elapsed_minutes = self.elapsed_seconds / 60
|
||||
if elapsed_minutes >= self.blank_after_minutes and not self.is_blanked:
|
||||
self.blank_screen()
|
||||
|
||||
# Skip animation updates if screen is blanked
|
||||
if self.is_blanked:
|
||||
return True
|
||||
|
||||
# Slow fade: oscillates between 0.3 and 1.0 over ~12 seconds
|
||||
fade = 0.65 + 0.35 * math.sin(self.time * 0.25)
|
||||
|
||||
# Wavy vertical offset using sine (increased amplitude for more visible movement)
|
||||
wave_offset = 60 * math.sin(self.time * 0.4)
|
||||
|
||||
# Apply opacity and transform
|
||||
self.label.set_opacity(fade)
|
||||
self.label.set_margin_top(int(100 + wave_offset))
|
||||
self.label.set_margin_bottom(int(100 - wave_offset))
|
||||
|
||||
return True # Continue animation
|
||||
|
||||
|
||||
class ScreensaverApp(Gtk.Application):
|
||||
def __init__(self, blank_after_minutes=20):
|
||||
super().__init__(application_id='com.custo.screensaver')
|
||||
self.blank_after_minutes = blank_after_minutes
|
||||
|
||||
def do_activate(self):
|
||||
# Load CSS
|
||||
css_provider = Gtk.CssProvider()
|
||||
css_provider.load_from_string('''
|
||||
.screensaver-window {
|
||||
background-color: black;
|
||||
}
|
||||
.screensaver-text {
|
||||
color: white;
|
||||
font-size: 120px;
|
||||
font-weight: bold;
|
||||
}
|
||||
''')
|
||||
Gtk.StyleContext.add_provider_for_display(
|
||||
Gdk.Display.get_default(),
|
||||
css_provider,
|
||||
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
|
||||
)
|
||||
|
||||
win = ScreensaverWindow(
|
||||
blank_after_minutes=self.blank_after_minutes,
|
||||
application=self
|
||||
)
|
||||
win.present()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(description='Custo screensaver display')
|
||||
parser.add_argument('--blank-after', type=int, default=20,
|
||||
help='Minutes until screen blanks (default: 20)')
|
||||
args = parser.parse_args()
|
||||
|
||||
app = ScreensaverApp(blank_after_minutes=args.blank_after)
|
||||
app.run(None)
|
||||
7
screensaver/custo-screensaver.desktop
Normal file
7
screensaver/custo-screensaver.desktop
Normal file
@ -0,0 +1,7 @@
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=Custo Screensaver
|
||||
Exec=/home/trav/.local/bin/custo-screensaver.sh
|
||||
Hidden=false
|
||||
NoDisplay=false
|
||||
X-GNOME-Autostart-enabled=true
|
||||
72
screensaver/custo-screensaver.sh
Executable file
72
screensaver/custo-screensaver.sh
Executable file
@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# custo-screensaver.sh: Display "custo" after idle timeout on GNOME/Wayland
|
||||
# Two-stage behavior:
|
||||
# Stage 1: After start_after minutes, show "custo" animation
|
||||
# Stage 2: After blank_after more minutes, tell display to blank screen
|
||||
|
||||
start_after=10 # minutes until screensaver appears
|
||||
blank_after=20 # additional minutes until screen blanks (total: 30 min)
|
||||
|
||||
# Kill any existing instances of this script (prevent duplicates)
|
||||
for pid in $(pgrep -f 'custo-screensaver.sh'); do
|
||||
if [ "$pid" != "$$" ]; then
|
||||
echo "custo-screensaver: Killing existing instance (PID $pid)"
|
||||
kill "$pid" 2>/dev/null
|
||||
fi
|
||||
done
|
||||
|
||||
# Also kill any orphaned display processes
|
||||
pkill -f 'custo-display.py' 2>/dev/null
|
||||
|
||||
cmd="python3 $HOME/.local/bin/custo-display.py"
|
||||
|
||||
get_idle_minutes() {
|
||||
idle_time=$(dbus-send --print-reply --dest=org.gnome.Mutter.IdleMonitor \
|
||||
/org/gnome/Mutter/IdleMonitor/Core org.gnome.Mutter.IdleMonitor.GetIdletime 2>/dev/null | grep -Po '(?<=uint64\s)\d+')
|
||||
|
||||
if [ -n "$idle_time" ]; then
|
||||
echo "$idle_time / 60000" | bc
|
||||
else
|
||||
echo "0"
|
||||
fi
|
||||
}
|
||||
|
||||
get_inhibitors() {
|
||||
for i in $(dbus-send --print-reply --dest=org.gnome.SessionManager \
|
||||
/org/gnome/SessionManager org.gnome.SessionManager.GetInhibitors 2>/dev/null | grep -Po 'Inhibitor\d+'); do
|
||||
|
||||
if dbus-send --print-reply --dest=org.gnome.SessionManager \
|
||||
/org/gnome/SessionManager/"$i" org.gnome.SessionManager.Inhibitor.GetFlags 2>/dev/null | grep -q 'uint32\s8'; then
|
||||
true; return
|
||||
fi
|
||||
done
|
||||
false; return
|
||||
}
|
||||
|
||||
echo "custo-screensaver: Started (PID $$). Will activate after $start_after minutes of idle."
|
||||
echo "custo-screensaver: Screen will blank after $blank_after additional minutes."
|
||||
|
||||
screensaver_pid=""
|
||||
screensaver_start_time=""
|
||||
|
||||
while :; do
|
||||
idle_mins=$(get_idle_minutes)
|
||||
|
||||
if [ "$idle_mins" -ge $start_after ] && [ -z "$screensaver_pid" ]; then
|
||||
if ! get_inhibitors; then
|
||||
echo "custo-screensaver: Activating (idle: ${idle_mins}m)"
|
||||
$cmd --blank-after=$blank_after &
|
||||
screensaver_pid=$!
|
||||
screensaver_start_time=$(date +%s)
|
||||
fi
|
||||
elif [ "$idle_mins" -lt $start_after ] && [ -n "$screensaver_pid" ]; then
|
||||
echo "custo-screensaver: Deactivating (user activity detected)"
|
||||
pkill -P $screensaver_pid 2>/dev/null
|
||||
kill $screensaver_pid 2>/dev/null
|
||||
screensaver_pid=""
|
||||
screensaver_start_time=""
|
||||
fi
|
||||
|
||||
sleep 1
|
||||
done
|
||||
Reference in New Issue
Block a user