tuanchris 4 miesięcy temu
rodzic
commit
cbcd06d339
37 zmienionych plików z 6250 dodań i 0 usunięć
  1. 7 0
      dune-weaver-touch/.gitattributes
  2. 387 0
      dune-weaver-touch/RASPBERRY_PI_SETUP.md
  3. 128 0
      dune-weaver-touch/README.md
  4. 914 0
      dune-weaver-touch/backend.py
  5. 170 0
      dune-weaver-touch/configure-boot.sh
  6. 9 0
      dune-weaver-touch/dune-weaver-touch.desktop
  7. 19 0
      dune-weaver-touch/dune-weaver-touch.service
  8. 46 0
      dune-weaver-touch/install-service.sh
  9. 195 0
      dune-weaver-touch/install.sh
  10. 43 0
      dune-weaver-touch/main.py
  11. 101 0
      dune-weaver-touch/models/pattern_model.py
  12. 85 0
      dune-weaver-touch/models/playlist_model.py
  13. 108 0
      dune-weaver-touch/qml/components/BottomNavTab.qml
  14. 66 0
      dune-weaver-touch/qml/components/BottomNavigation.qml
  15. 61 0
      dune-weaver-touch/qml/components/ConnectionStatus.qml
  16. 66 0
      dune-weaver-touch/qml/components/KeyboardHelper.qml
  17. 103 0
      dune-weaver-touch/qml/components/ModernControlButton.qml
  18. 152 0
      dune-weaver-touch/qml/components/ModernPatternCard.qml
  19. 53 0
      dune-weaver-touch/qml/components/PatternCard.qml
  20. 53 0
      dune-weaver-touch/qml/components/VirtualKeyboardLoader.qml
  21. 205 0
      dune-weaver-touch/qml/main.qml
  22. 540 0
      dune-weaver-touch/qml/pages/ExecutionPage.qml
  23. 284 0
      dune-weaver-touch/qml/pages/ModernPatternListPage.qml
  24. 678 0
      dune-weaver-touch/qml/pages/ModernPlaylistPage.qml
  25. 256 0
      dune-weaver-touch/qml/pages/PatternDetailPage.qml
  26. 68 0
      dune-weaver-touch/qml/pages/PatternListPage.qml
  27. 93 0
      dune-weaver-touch/qml/pages/PlaylistPage.qml
  28. 543 0
      dune-weaver-touch/qml/pages/TableControlPage.qml
  29. 3 0
      dune-weaver-touch/requirements.txt
  30. 53 0
      dune-weaver-touch/run.sh
  31. 59 0
      dune-weaver-touch/scripts/install-scripts.sh
  32. 6 0
      dune-weaver-touch/scripts/screen-off
  33. 12 0
      dune-weaver-touch/scripts/screen-on
  34. 37 0
      dune-weaver-touch/scripts/touch-monitor
  35. 58 0
      dune-weaver-touch/setup-autologin.sh
  36. 148 0
      dune-weaver-touch/setup-autostart.sh
  37. 441 0
      dune-weaver-touch/setup_kiosk.sh

+ 7 - 0
dune-weaver-touch/.gitattributes

@@ -0,0 +1,7 @@
+# Ensure shell scripts always use LF line endings
+*.sh text eol=lf
+install.sh text eol=lf
+setup-autostart.sh text eol=lf
+
+# Scripts should also use LF
+scripts/* text eol=lf

+ 387 - 0
dune-weaver-touch/RASPBERRY_PI_SETUP.md

@@ -0,0 +1,387 @@
+# Raspberry Pi MIPI DSI Touchscreen Setup for Dune Weaver Touch
+
+## Prerequisites
+Make sure your Raspberry Pi is running a recent version of Raspberry Pi OS (Bullseye or newer).
+
+**MIPI DSI Display Notes**: This guide is specifically configured for the **Freenove 5" MIPI DSI Touchscreen (800x480)** which uses driver-free configuration on modern Raspberry Pi OS.
+
+## Install System Dependencies
+
+```bash
+# Update system packages
+sudo apt update && sudo apt upgrade -y
+
+# Install Qt6 system packages (includes eglfs platform plugin)
+sudo apt install -y qt6-base-dev qt6-qml-dev qt6-quick-dev
+
+# Install graphics and display dependencies
+sudo apt install -y \
+    libgl1-mesa-dev \
+    libgles2-mesa-dev \
+    libegl1-mesa-dev \
+    libdrm2 \
+    libxkbcommon0 \
+    libinput10 \
+    libudev1
+
+# Install Python and pip
+sudo apt install -y python3-dev python3-pip
+
+# Install PySide6 from system packages (recommended for embedded)
+sudo apt install -y python3-pyside6.qtcore python3-pyside6.qtgui python3-pyside6.qtqml python3-pyside6.qtquick python3-pyside6.qtwebsockets
+
+# Alternative: Install PySide6 via pip (if system packages don't work)
+# pip3 install PySide6
+```
+
+## Configure Freenove 5" MIPI DSI Display
+
+### Step 1: Physical Connection
+1. Connect the display ribbon cable to the MIPI DSI port (next to camera connector)
+2. Connect the USB cable for touch (to any USB port)
+3. Power on the Raspberry Pi
+
+### Step 2: Configure Boot Settings
+
+The Freenove display is driver-free on newer Pi OS, but needs proper configuration:
+
+```bash
+sudo nano /boot/config.txt
+
+# Add these lines for Freenove 5" MIPI DSI display
+dtoverlay=vc4-kms-v3d
+max_framebuffers=2
+
+# GPU memory for graphics acceleration  
+gpu_mem=128
+
+# Enable DSI display (driver-free detection)
+display_auto_detect=1
+
+# Force 800x480 resolution if auto-detect fails
+# hdmi_group=2
+# hdmi_mode=87
+# hdmi_cvt=800 480 60 6 0 0 0
+
+# Disable overscan for exact pixel mapping
+disable_overscan=1
+
+# Optional: Disable rainbow splash screen
+disable_splash=1
+
+# Optional: Rotate display if mounted upside down
+# display_rotate=2
+```
+
+### Step 3: Configure Touch Input
+
+The Freenove display uses USB for 5-point capacitive touch (driver-free):
+
+```bash
+# Verify touch is detected via USB
+lsusb | grep -i touch
+ls /dev/input/event*
+
+# Check touch events
+sudo evtest
+# Select the touch device (usually event0 or event1)
+
+# Touch should work automatically - no additional drivers needed
+```
+
+### Step 4: Reboot and Verify Display
+```bash
+sudo reboot
+
+# After reboot, verify display is working
+# Check framebuffer resolution
+fbset -fb /dev/fb0
+
+# Verify display resolution
+xrandr
+# Should show 800x480 resolution
+
+# Test display with simple graphics
+sudo apt install -y fbi
+sudo fbi -T 1 /usr/share/pixmaps/debian-logo.png
+```
+
+## Configure Display for Application
+
+### Option 1: Direct Framebuffer (Kiosk Mode - Recommended)
+For a dedicated touchscreen kiosk:
+
+```bash
+# Add to ~/.bashrc or create a startup script
+export QT_QPA_PLATFORM=eglfs
+export QT_QPA_EGLFS_WIDTH=800
+export QT_QPA_EGLFS_HEIGHT=480
+export QT_QPA_EGLFS_PHYSICAL_WIDTH=154
+export QT_QPA_EGLFS_PHYSICAL_HEIGHT=86
+export QT_QPA_GENERIC_PLUGINS=evdevtouch
+```
+
+### Option 2: X11 with Touchscreen Support
+If you need window management:
+
+```bash
+# Install X11 and touchscreen support
+sudo apt install -y xinput-calibrator xserver-xorg-input-evdev
+
+# Configure X11 for touchscreen
+export DISPLAY=:0
+export QT_QPA_PLATFORM=xcb
+```
+
+### Option 3: Wayland (Modern Alternative)
+For newer systems:
+
+```bash
+sudo apt install -y qt6-wayland
+export QT_QPA_PLATFORM=wayland
+export WAYLAND_DISPLAY=wayland-1
+```
+
+## Running the Application
+
+### Method 1: Direct Framebuffer (Fullscreen Kiosk)
+```bash
+cd /path/to/dune-weaver-touch
+QT_QPA_PLATFORM=eglfs QT_QPA_EGLFS_WIDTH=800 QT_QPA_EGLFS_HEIGHT=480 python3 main.py
+```
+
+### Method 2: X11 Window
+```bash
+cd /path/to/dune-weaver-touch
+DISPLAY=:0 QT_QPA_PLATFORM=xcb python3 main.py
+```
+
+### Method 3: Auto-detect Platform
+```bash
+cd /path/to/dune-weaver-touch
+python3 main.py
+```
+
+## Touchscreen Calibration
+
+If touch input doesn't align with display:
+
+```bash
+# Install calibration tool
+sudo apt install -y xinput-calibrator
+
+# Run calibration (follow on-screen instructions)
+sudo xinput_calibrator
+
+# Save the output to X11 configuration
+sudo tee /etc/X11/xorg.conf.d/99-calibration.conf << EOF
+Section "InputClass"
+    Identifier "calibration"
+    MatchProduct "your_touchscreen_name"
+    Option "Calibration" "min_x max_x min_y max_y"
+    Option "SwapAxes" "0"
+EndSection
+EOF
+```
+
+## Auto-start on Boot (Kiosk Mode)
+
+### Automated Setup Script
+
+We provide a comprehensive setup script that automatically configures your Raspberry Pi for kiosk mode:
+
+```bash
+# Navigate to the application directory
+cd /home/pi/dune-weaver/dune-weaver-touch
+
+# Make the setup script executable
+chmod +x setup_kiosk.sh
+
+# Run the setup script
+./setup_kiosk.sh
+
+# The script will:
+# - Detect your Pi model (3/4/5)
+# - Configure boot settings for DSI display
+# - Install required dependencies
+# - Create systemd service for auto-start
+# - Set up proper Qt platform (EGLFS/LinuxFB)
+# - Create uninstall script for easy removal
+```
+
+### Script Commands
+
+```bash
+# Install kiosk mode (default)
+./setup_kiosk.sh
+
+# Check service status
+./setup_kiosk.sh status
+
+# Test display configuration
+./setup_kiosk.sh test
+
+# Uninstall kiosk mode
+./setup_kiosk.sh uninstall
+# Or use the generated script:
+./uninstall_kiosk.sh
+```
+
+### Manual Service Control
+
+After installation, you can control the kiosk service:
+
+```bash
+# Start the service
+sudo systemctl start dune-weaver-kiosk
+
+# Stop the service
+sudo systemctl stop dune-weaver-kiosk
+
+# Check service status
+sudo systemctl status dune-weaver-kiosk
+
+# View live logs
+journalctl -u dune-weaver-kiosk -f
+
+# Disable auto-start
+sudo systemctl disable dune-weaver-kiosk
+
+# Re-enable auto-start
+sudo systemctl enable dune-weaver-kiosk
+```
+
+### Manual Setup (Alternative)
+
+If you prefer manual setup, create a systemd service:
+
+```bash
+sudo tee /etc/systemd/system/dune-weaver-touch.service << EOF
+[Unit]
+Description=Dune Weaver Touch Interface
+After=multi-user.target graphical.target
+Wants=network-online.target
+
+[Service]
+Type=simple
+User=pi
+Environment=QT_QPA_PLATFORM=eglfs
+Environment=QT_QPA_EGLFS_WIDTH=800
+Environment=QT_QPA_EGLFS_HEIGHT=480
+Environment=QT_QPA_EGLFS_INTEGRATION=eglfs_kms
+Environment=QT_QPA_EGLFS_HIDECURSOR=1
+WorkingDirectory=/home/pi/dune-weaver/dune-weaver-touch
+ExecStart=/home/pi/dune-weaver/dune-weaver-touch/bin/python main.py
+Restart=always
+RestartSec=10
+
+[Install]
+WantedBy=multi-user.target
+EOF
+
+# Enable the service
+sudo systemctl enable dune-weaver-touch.service
+sudo systemctl start dune-weaver-touch.service
+```
+
+## Troubleshooting
+
+### "Could not find Qt platform plugin"
+```bash
+# Check available plugins
+python3 -c "from PySide6.QtGui import QGuiApplication; import sys; app=QGuiApplication(sys.argv); print(app.platformName())"
+
+# If eglfs missing, install system Qt packages
+sudo apt install -y qt6-qpa-plugins
+```
+
+### "Cannot open display"
+```bash
+# Make sure X11 is running or use eglfs
+export DISPLAY=:0
+xhost +local:
+```
+
+### Touch not working
+```bash
+# Check input devices
+ls /dev/input/
+cat /proc/bus/input/devices
+
+# Test touch events
+sudo apt install -y evtest
+sudo evtest
+```
+
+### Screen rotation
+Add to boot config (`/boot/config.txt`):
+```
+# Rotate display 180 degrees
+display_rotate=2
+
+# Or rotate 90 degrees
+display_rotate=1
+```
+
+## Performance Tips
+
+1. **GPU Memory Split**: Increase GPU memory in `/boot/config.txt`:
+   ```
+   gpu_mem=128
+   ```
+
+2. **Disable unnecessary services**:
+   ```bash
+   sudo systemctl disable bluetooth
+   sudo systemctl disable cups
+   ```
+
+3. **Use hardware acceleration**: Ensure Mesa drivers are properly installed
+
+## Screen Power Management (Kiosk Mode)
+
+The touch application includes built-in screen power management with touch-to-wake functionality.
+
+### Automatic Screen Timeout
+The screen will automatically turn off after 5 minutes (300 seconds) of inactivity and can be woken by touching the screen.
+
+### Manual Screen Control
+You can also control the screen manually from the command line:
+
+```bash
+# Turn screen OFF
+sudo vcgencmd display_power 0
+
+# Turn screen ON
+sudo vcgencmd display_power 1
+
+# Alternative: Control backlight
+sudo sh -c 'echo "0" > /sys/class/backlight/*/brightness'  # OFF
+sudo sh -c 'echo "255" > /sys/class/backlight/*/brightness'  # ON
+```
+
+### Configure Screen Timeout
+The default timeout is 5 minutes, but you can modify it in your application by calling:
+
+```python
+# In your backend or QML code
+backend.setScreenTimeout(600)  # 10 minutes
+```
+
+### Permissions for Screen Control
+The application needs sudo permissions for screen control. Add these to sudoers:
+
+```bash
+sudo visudo
+# Add these lines for the pi user:
+pi ALL=(ALL) NOPASSWD: /usr/bin/vcgencmd
+pi ALL=(ALL) NOPASSWD: /bin/sh -c echo * > /sys/class/backlight/*/brightness
+pi ALL=(ALL) NOPASSWD: /bin/cat /dev/input/event*
+```
+
+### Features
+- **Automatic timeout**: Screen turns off after configured inactivity period
+- **Touch-to-wake**: Any touch on the screen will wake it up immediately  
+- **Activity tracking**: All mouse/touch interactions reset the timeout timer
+- **Fallback methods**: Uses vcgencmd first, falls back to backlight control
+- **Background monitoring**: Monitors touch input devices when screen is off

+ 128 - 0
dune-weaver-touch/README.md

@@ -0,0 +1,128 @@
+# Dune Weaver Touch Interface
+
+A PySide6/QML touch interface for the Dune Weaver sand table system that works alongside the existing FastAPI web server.
+
+## Features
+
+- **Modern SwipeView Navigation**: Swipe between Patterns, Playlists, and Control pages
+- **Pattern Browsing**: Beautiful grid view with search and thumbnail previews
+- **Pattern Execution**: Touch-optimized controls with pre-execution options
+- **Table Control**: Dedicated control page with status monitoring and quick actions
+- **Real-time Status**: WebSocket integration for live progress updates
+- **Modern UI**: Material Design inspired interface with animations and shadows
+- **Touch Optimized**: Large buttons, smooth animations, and intuitive gestures
+
+## Architecture
+
+- **Pattern Browsing**: Direct file system access for instant loading
+- **Execution Control**: REST API calls to existing FastAPI endpoints  
+- **Status Monitoring**: WebSocket connection for real-time updates
+- **Navigation**: StackView-based page navigation
+
+## Quick Installation (Auto-Start Setup)
+
+**One command to set up everything for kiosk/production use:**
+
+```bash
+sudo ./install.sh
+```
+
+This will:
+- ✅ Create Python virtual environment with dependencies (`venv/`)
+- ✅ Install system scripts for screen control (`/usr/local/bin/screen-on`, `screen-off`, `touch-monitor`)
+- ✅ Set up systemd service for auto-start on boot  
+- ✅ Configure kiosk optimizations (clean boot, auto-login)
+- ✅ Enable automatic startup
+
+### Service Management
+```bash
+# Control the service
+sudo systemctl start dune-weaver-touch    # Start now
+sudo systemctl stop dune-weaver-touch     # Stop service
+sudo systemctl status dune-weaver-touch   # Check status
+sudo journalctl -u dune-weaver-touch -f   # View logs
+
+# Disable auto-start
+sudo systemctl disable dune-weaver-touch
+```
+
+## Manual Installation (Development)
+
+1. Create virtual environment and install dependencies:
+   ```bash
+   python3 -m venv venv
+   source venv/bin/activate  # or: . venv/bin/activate
+   pip install -r requirements.txt
+   ```
+
+2. Ensure the main Dune Weaver FastAPI server is running:
+   ```bash
+   cd ../  # Go to main dune-weaver directory
+   python main.py
+   ```
+
+3. Run the touch interface:
+   ```bash
+   ./run.sh              # Uses virtual environment automatically
+   # OR manually:
+   ./venv/bin/python main.py
+   ```
+
+## Advanced Setup Options
+
+For custom installations:
+```bash
+sudo ./setup-autostart.sh
+```
+
+Choose from multiple setup options including systemd service, desktop autostart, or kiosk optimizations.
+
+## Project Structure
+
+```
+dune-weaver-touch/
+├── main.py                     # Application entry point
+├── backend.py                  # Backend controller with API/WebSocket integration
+├── models/
+│   ├── pattern_model.py        # Pattern list model with file system access
+│   └── playlist_model.py       # Playlist model reading from JSON
+├── qml/
+│   ├── main.qml               # Main window with StackView navigation
+│   ├── pages/
+│   │   ├── PatternListPage.qml    # Grid view of patterns with search
+│   │   ├── PatternDetailPage.qml  # Pattern details with execution controls
+│   │   ├── PlaylistPage.qml       # Playlist selection and execution
+│   │   └── ExecutionPage.qml      # Current execution status display
+│   └── components/
+│       └── PatternCard.qml        # Pattern thumbnail card
+├── requirements.txt
+└── README.md
+```
+
+## Usage
+
+### Navigation
+- **Swipe left/right** to navigate between the three main pages:
+  - **Patterns**: Browse and search through all available patterns
+  - **Playlists**: View and manage pattern playlists
+  - **Control**: Monitor table status and quick control actions
+
+### Pattern Management
+1. **Browse Patterns**: Swipe to Patterns page to see grid view with thumbnail previews
+2. **Search**: Use the search field to filter patterns by name
+3. **Select Pattern**: Tap a pattern card to view details and execution options
+4. **Execute**: Choose pre-execution action and tap "Play Pattern"
+
+### Table Control
+1. **Monitor Status**: Swipe to Control page to see current pattern and progress
+2. **Control Execution**: Use Pause/Resume and Stop buttons
+3. **Quick Actions**: Use Clear In, Clear Out, or Circle pattern shortcuts
+4. **Connection Status**: View WebSocket connection status
+
+## Notes
+
+- The touch interface runs independently from the web UI
+- Both interfaces can be used simultaneously
+- Pattern browsing works even if the FastAPI server is offline
+- Execution requires the FastAPI server to be running
+- Paths are relative to the main dune-weaver directory

+ 914 - 0
dune-weaver-touch/backend.py

@@ -0,0 +1,914 @@
+from PySide6.QtCore import QObject, Signal, Property, Slot, QTimer
+from PySide6.QtQml import QmlElement
+from PySide6.QtWebSockets import QWebSocket
+import aiohttp
+import asyncio
+import json
+import subprocess
+import threading
+import time
+from pathlib import Path
+
+QML_IMPORT_NAME = "DuneWeaver"
+QML_IMPORT_MAJOR_VERSION = 1
+
+@QmlElement
+class Backend(QObject):
+    """Backend controller for API and WebSocket communication"""
+    
+    # Signals
+    statusChanged = Signal()
+    progressChanged = Signal()
+    connectionChanged = Signal()
+    executionStarted = Signal(str, str)  # patternName, patternPreview
+    executionStopped = Signal()
+    errorOccurred = Signal(str)
+    serialPortsUpdated = Signal(list)
+    serialConnectionChanged = Signal(bool)
+    currentPortChanged = Signal(str)
+    speedChanged = Signal(int)
+    settingsLoaded = Signal()
+    screenStateChanged = Signal(bool)  # True = on, False = off
+    
+    def __init__(self):
+        super().__init__()
+        self.base_url = "http://localhost:8080"
+        
+        # WebSocket for status
+        self.ws = QWebSocket()
+        self.ws.connected.connect(self._on_ws_connected)
+        self.ws.textMessageReceived.connect(self._on_ws_message)
+        self.ws.open("ws://localhost:8080/ws/status")
+        
+        # Status properties
+        self._current_file = ""
+        self._progress = 0
+        self._is_running = False
+        self._is_connected = False
+        self._serial_ports = []
+        self._serial_connected = False
+        self._current_port = ""
+        self._current_speed = 130
+        self._auto_play_on_boot = False
+        
+        # Screen management
+        self._screen_on = True
+        self._screen_timeout = 30  # 30 seconds for testing (change back to 300 for production)
+        self._last_activity = time.time()
+        self._touch_monitor_thread = None
+        self._screen_transition_lock = threading.Lock()  # Prevent rapid state changes
+        self._last_screen_change = 0  # Track last state change time
+        self._use_touch_script = False  # Disable external touch-monitor script (too sensitive)
+        self._screen_timer = QTimer()
+        self._screen_timer.timeout.connect(self._check_screen_timeout)
+        self._screen_timer.start(1000)  # Check every second
+        print(f"🖥️ Screen management initialized: timeout={self._screen_timeout}s, timer started")
+        
+        # HTTP session - initialize lazily
+        self.session = None
+        self._session_initialized = False
+        
+        # Use QTimer to defer session initialization until event loop is running
+        QTimer.singleShot(100, self._delayed_init)
+    
+    @Slot()
+    def _delayed_init(self):
+        """Initialize session after Qt event loop is running"""
+        if not self._session_initialized:
+            try:
+                loop = asyncio.get_event_loop()
+                if loop.is_running():
+                    asyncio.create_task(self._init_session())
+                else:
+                    # If no loop is running, try again later
+                    QTimer.singleShot(500, self._delayed_init)
+            except RuntimeError:
+                # No event loop yet, try again
+                QTimer.singleShot(500, self._delayed_init)
+    
+    async def _init_session(self):
+        """Initialize aiohttp session"""
+        if not self._session_initialized:
+            self.session = aiohttp.ClientSession()
+            self._session_initialized = True
+    
+    # Properties
+    @Property(str, notify=statusChanged)
+    def currentFile(self):
+        return self._current_file
+    
+    @Property(float, notify=progressChanged)
+    def progress(self):
+        return self._progress
+    
+    @Property(bool, notify=statusChanged)
+    def isRunning(self):
+        return self._is_running
+    
+    @Property(bool, notify=connectionChanged)
+    def isConnected(self):
+        return self._is_connected
+    
+    @Property(list, notify=serialPortsUpdated)
+    def serialPorts(self):
+        return self._serial_ports
+    
+    @Property(bool, notify=serialConnectionChanged)
+    def serialConnected(self):
+        return self._serial_connected
+    
+    @Property(str, notify=currentPortChanged)
+    def currentPort(self):
+        return self._current_port
+    
+    @Property(int, notify=speedChanged)
+    def currentSpeed(self):
+        return self._current_speed
+    
+    @Property(bool, notify=settingsLoaded)
+    def autoPlayOnBoot(self):
+        return self._auto_play_on_boot
+    
+    # WebSocket handlers
+    @Slot()
+    def _on_ws_connected(self):
+        print("WebSocket connected")
+        self._is_connected = True
+        self.connectionChanged.emit()
+    
+    @Slot(str)
+    def _on_ws_message(self, message):
+        try:
+            data = json.loads(message)
+            if data.get("type") == "status_update":
+                status = data.get("data", {})
+                self._current_file = status.get("current_file", "")
+                self._is_running = status.get("is_running", False)
+                
+                # Handle serial connection status from WebSocket
+                ws_connection_status = status.get("connection_status", False)
+                if ws_connection_status != self._serial_connected:
+                    print(f"🔌 WebSocket serial connection status changed: {ws_connection_status}")
+                    self._serial_connected = ws_connection_status
+                    self.serialConnectionChanged.emit(ws_connection_status)
+                    
+                    # If we're connected, we need to get the current port
+                    if ws_connection_status:
+                        # We'll need to fetch the current port via HTTP since WS doesn't include port info
+                        asyncio.create_task(self._get_current_port())
+                    else:
+                        self._current_port = ""
+                        self.currentPortChanged.emit("")
+                
+                # Handle speed updates from WebSocket
+                ws_speed = status.get("speed", None)
+                if ws_speed and ws_speed != self._current_speed:
+                    print(f"⚡ WebSocket speed changed: {ws_speed}")
+                    self._current_speed = ws_speed
+                    self.speedChanged.emit(ws_speed)
+                
+                if status.get("progress"):
+                    self._progress = status["progress"].get("percentage", 0)
+                
+                self.statusChanged.emit()
+                self.progressChanged.emit()
+        except json.JSONDecodeError:
+            pass
+    
+    async def _get_current_port(self):
+        """Fetch the current port when we detect a connection via WebSocket"""
+        if not self.session:
+            return
+        
+        try:
+            async with self.session.get(f"{self.base_url}/serial_status") as resp:
+                if resp.status == 200:
+                    data = await resp.json()
+                    current_port = data.get("port", "")
+                    if current_port:
+                        self._current_port = current_port
+                        self.currentPortChanged.emit(current_port)
+                        print(f"🔌 Updated current port from WebSocket trigger: {current_port}")
+        except Exception as e:
+            print(f"💥 Exception getting current port: {e}")
+    
+    # API Methods
+    @Slot(str, str)
+    def executePattern(self, fileName, preExecution="adaptive"):
+        print(f"🎯 ExecutePattern called: fileName='{fileName}', preExecution='{preExecution}'")
+        asyncio.create_task(self._execute_pattern(fileName, preExecution))
+    
+    async def _execute_pattern(self, fileName, preExecution):
+        if not self.session:
+            print("❌ Backend session not ready")
+            self.errorOccurred.emit("Backend not ready, please try again")
+            return
+        
+        try:
+            request_data = {"file_name": fileName, "pre_execution": preExecution}
+            print(f"🔄 Making HTTP POST to: {self.base_url}/run_theta_rho")
+            print(f"📝 Request payload: {request_data}")
+            
+            async with self.session.post(
+                f"{self.base_url}/run_theta_rho",
+                json=request_data
+            ) as resp:
+                print(f"📡 Response status: {resp.status}")
+                print(f"📋 Response headers: {dict(resp.headers)}")
+                
+                response_text = await resp.text()
+                print(f"📄 Response body: {response_text}")
+                
+                if resp.status == 200:
+                    print("✅ Pattern execution request successful")
+                    # Find preview image for the pattern
+                    preview_path = self._find_pattern_preview(fileName)
+                    print(f"🖼️ Pattern preview path: {preview_path}")
+                    print(f"📡 About to emit executionStarted signal with: fileName='{fileName}', preview='{preview_path}'")
+                    try:
+                        self.executionStarted.emit(fileName, preview_path)
+                        print("✅ ExecutionStarted signal emitted successfully")
+                    except Exception as e:
+                        print(f"❌ Error emitting executionStarted signal: {e}")
+                else:
+                    print(f"❌ Pattern execution failed with status {resp.status}")
+                    self.errorOccurred.emit(f"Failed to execute: {resp.status} - {response_text}")
+        except Exception as e:
+            print(f"💥 Exception in _execute_pattern: {e}")
+            self.errorOccurred.emit(str(e))
+    
+    def _find_pattern_preview(self, fileName):
+        """Find the preview image for a pattern"""
+        try:
+            # Extract just the filename from the path (remove any directory prefixes)
+            clean_filename = fileName.split('/')[-1]  # Get last part of path
+            print(f"🔍 Original fileName: {fileName}, clean filename: {clean_filename}")
+            
+            # Check multiple possible locations for patterns directory
+            # Use relative paths that work across different environments
+            possible_dirs = [
+                Path("../patterns"),  # One level up (for when running from touch subdirectory)
+                Path("patterns"),     # Same level (for when running from main directory)
+                Path(__file__).parent.parent / "patterns"  # Dynamic path relative to backend.py
+            ]
+            
+            for patterns_dir in possible_dirs:
+                cache_dir = patterns_dir / "cached_images"
+                if cache_dir.exists():
+                    print(f"🔍 Checking preview cache directory: {cache_dir}")
+                    # Try different preview image extensions - PNG first for kiosk
+                    # First try with .thr suffix (e.g., pattern.thr.png)
+                    for ext in [".png", ".webp", ".jpg", ".jpeg"]:
+                        preview_file = cache_dir / (clean_filename + ext)
+                        print(f"🔍 Looking for preview: {preview_file}")
+                        if preview_file.exists():
+                            print(f"✅ Found preview: {preview_file}")
+                            return str(preview_file.absolute())
+                    
+                    # Then try without .thr suffix (e.g., pattern.png)
+                    base_name = clean_filename.replace(".thr", "")
+                    for ext in [".png", ".webp", ".jpg", ".jpeg"]:
+                        preview_file = cache_dir / (base_name + ext)
+                        print(f"🔍 Looking for preview (no .thr): {preview_file}")
+                        if preview_file.exists():
+                            print(f"✅ Found preview: {preview_file}")
+                            return str(preview_file.absolute())
+            
+            print("❌ No preview image found")
+            return ""
+        except Exception as e:
+            print(f"💥 Exception finding preview: {e}")
+            return ""
+    
+    @Slot()
+    def stopExecution(self):
+        asyncio.create_task(self._stop_execution())
+    
+    async def _stop_execution(self):
+        if not self.session:
+            self.errorOccurred.emit("Backend not ready")
+            return
+        
+        try:
+            print("🛑 Calling stop_execution endpoint...")
+            # Add timeout to prevent hanging
+            timeout = aiohttp.ClientTimeout(total=10)  # 10 second timeout
+            async with self.session.post(f"{self.base_url}/stop_execution", timeout=timeout) as resp:
+                print(f"🛑 Stop execution response status: {resp.status}")
+                if resp.status == 200:
+                    response_data = await resp.json()
+                    print(f"🛑 Stop execution response: {response_data}")
+                    self.executionStopped.emit()
+                else:
+                    print(f"❌ Stop execution failed with status: {resp.status}")
+                    response_text = await resp.text()
+                    self.errorOccurred.emit(f"Stop failed: {resp.status} - {response_text}")
+        except asyncio.TimeoutError:
+            print("⏰ Stop execution request timed out")
+            self.errorOccurred.emit("Stop execution request timed out")
+        except Exception as e:
+            print(f"💥 Exception in _stop_execution: {e}")
+            self.errorOccurred.emit(str(e))
+    
+    @Slot()
+    def pauseExecution(self):
+        print("⏸️ Pausing execution...")
+        asyncio.create_task(self._api_call("/pause_execution"))
+    
+    @Slot()
+    def resumeExecution(self):
+        print("▶️ Resuming execution...")
+        asyncio.create_task(self._api_call("/resume_execution"))
+    
+    @Slot()
+    def skipPattern(self):
+        print("⏭️ Skipping pattern...")
+        asyncio.create_task(self._api_call("/skip_pattern"))
+    
+    @Slot(str, float, str, str, bool)
+    def executePlaylist(self, playlistName, pauseTime=0.0, clearPattern="adaptive", runMode="single", shuffle=False):
+        print(f"🎵 ExecutePlaylist called: playlist='{playlistName}', pauseTime={pauseTime}, clearPattern='{clearPattern}', runMode='{runMode}', shuffle={shuffle}")
+        asyncio.create_task(self._execute_playlist(playlistName, pauseTime, clearPattern, runMode, shuffle))
+    
+    async def _execute_playlist(self, playlistName, pauseTime, clearPattern, runMode, shuffle):
+        if not self.session:
+            print("❌ Backend session not ready")
+            self.errorOccurred.emit("Backend not ready, please try again")
+            return
+        
+        try:
+            request_data = {
+                "playlist_name": playlistName,
+                "pause_time": pauseTime,
+                "clear_pattern": clearPattern,
+                "run_mode": runMode,
+                "shuffle": shuffle
+            }
+            print(f"🔄 Making HTTP POST to: {self.base_url}/run_playlist")
+            print(f"📝 Request payload: {request_data}")
+            
+            async with self.session.post(
+                f"{self.base_url}/run_playlist",
+                json=request_data
+            ) as resp:
+                print(f"📡 Response status: {resp.status}")
+                
+                response_text = await resp.text()
+                print(f"📄 Response body: {response_text}")
+                
+                if resp.status == 200:
+                    print(f"✅ Playlist execution request successful: {playlistName}")
+                    # The playlist will start executing patterns automatically
+                    # Status updates will come through WebSocket
+                else:
+                    print(f"❌ Playlist execution failed with status {resp.status}")
+                    self.errorOccurred.emit(f"Failed to execute playlist: {resp.status} - {response_text}")
+        except Exception as e:
+            print(f"💥 Exception in _execute_playlist: {e}")
+            self.errorOccurred.emit(str(e))
+    
+    async def _api_call(self, endpoint):
+        if not self.session:
+            self.errorOccurred.emit("Backend not ready")
+            return
+        
+        try:
+            print(f"📡 Calling API endpoint: {endpoint}")
+            # Add timeout to prevent hanging
+            timeout = aiohttp.ClientTimeout(total=10)  # 10 second timeout
+            async with self.session.post(f"{self.base_url}{endpoint}", timeout=timeout) as resp:
+                print(f"📡 API response status for {endpoint}: {resp.status}")
+                if resp.status == 200:
+                    response_data = await resp.json()
+                    print(f"📡 API response for {endpoint}: {response_data}")
+                else:
+                    print(f"❌ API call {endpoint} failed with status: {resp.status}")
+                    response_text = await resp.text()
+                    self.errorOccurred.emit(f"API call failed: {endpoint} - {resp.status} - {response_text}")
+        except asyncio.TimeoutError:
+            print(f"⏰ API call {endpoint} timed out")
+            self.errorOccurred.emit(f"API call {endpoint} timed out")
+        except Exception as e:
+            print(f"💥 Exception in API call {endpoint}: {e}")
+            self.errorOccurred.emit(str(e))
+    
+    # Serial Port Management
+    @Slot()
+    def refreshSerialPorts(self):
+        print("🔌 Refreshing serial ports...")
+        asyncio.create_task(self._refresh_serial_ports())
+    
+    async def _refresh_serial_ports(self):
+        if not self.session:
+            self.errorOccurred.emit("Backend not ready")
+            return
+        
+        try:
+            async with self.session.get(f"{self.base_url}/list_serial_ports") as resp:
+                if resp.status == 200:
+                    # The endpoint returns a list directly, not a dictionary
+                    ports = await resp.json()
+                    self._serial_ports = ports if isinstance(ports, list) else []
+                    print(f"📡 Found serial ports: {self._serial_ports}")
+                    self.serialPortsUpdated.emit(self._serial_ports)
+                else:
+                    print(f"❌ Failed to get serial ports: {resp.status}")
+        except Exception as e:
+            print(f"💥 Exception refreshing serial ports: {e}")
+            self.errorOccurred.emit(str(e))
+    
+    @Slot(str)
+    def connectSerial(self, port):
+        print(f"🔗 Connecting to serial port: {port}")
+        asyncio.create_task(self._connect_serial(port))
+    
+    async def _connect_serial(self, port):
+        if not self.session:
+            self.errorOccurred.emit("Backend not ready")
+            return
+        
+        try:
+            async with self.session.post(f"{self.base_url}/connect", json={"port": port}) as resp:
+                if resp.status == 200:
+                    print(f"✅ Connected to {port}")
+                    self._serial_connected = True
+                    self._current_port = port
+                    self.serialConnectionChanged.emit(True)
+                    self.currentPortChanged.emit(port)
+                else:
+                    response_text = await resp.text()
+                    print(f"❌ Failed to connect to {port}: {resp.status} - {response_text}")
+                    self.errorOccurred.emit(f"Failed to connect: {response_text}")
+        except Exception as e:
+            print(f"💥 Exception connecting to serial: {e}")
+            self.errorOccurred.emit(str(e))
+    
+    @Slot()
+    def disconnectSerial(self):
+        print("🔌 Disconnecting serial...")
+        asyncio.create_task(self._disconnect_serial())
+    
+    async def _disconnect_serial(self):
+        if not self.session:
+            self.errorOccurred.emit("Backend not ready")
+            return
+        
+        try:
+            async with self.session.post(f"{self.base_url}/disconnect") as resp:
+                if resp.status == 200:
+                    print("✅ Disconnected from serial")
+                    self._serial_connected = False
+                    self._current_port = ""
+                    self.serialConnectionChanged.emit(False)
+                    self.currentPortChanged.emit("")
+                else:
+                    response_text = await resp.text()
+                    print(f"❌ Failed to disconnect: {resp.status} - {response_text}")
+        except Exception as e:
+            print(f"💥 Exception disconnecting serial: {e}")
+            self.errorOccurred.emit(str(e))
+    
+    # Hardware Movement Controls
+    @Slot()
+    def sendHome(self):
+        print("🏠 Sending home command...")
+        asyncio.create_task(self._api_call("/send_home"))
+    
+    @Slot()
+    def moveToCenter(self):
+        print("🎯 Moving to center...")
+        asyncio.create_task(self._api_call("/move_to_center"))
+    
+    @Slot()
+    def moveToPerimeter(self):
+        print("⭕ Moving to perimeter...")
+        asyncio.create_task(self._api_call("/move_to_perimeter"))
+    
+    # Speed Control
+    @Slot(int)
+    def setSpeed(self, speed):
+        print(f"⚡ Setting speed to: {speed}")
+        asyncio.create_task(self._set_speed(speed))
+    
+    async def _set_speed(self, speed):
+        if not self.session:
+            self.errorOccurred.emit("Backend not ready")
+            return
+        
+        try:
+            async with self.session.post(f"{self.base_url}/set_speed", json={"speed": speed}) as resp:
+                if resp.status == 200:
+                    print(f"✅ Speed set to {speed}")
+                    self._current_speed = speed
+                    self.speedChanged.emit(speed)
+                else:
+                    response_text = await resp.text()
+                    print(f"❌ Failed to set speed: {resp.status} - {response_text}")
+        except Exception as e:
+            print(f"💥 Exception setting speed: {e}")
+            self.errorOccurred.emit(str(e))
+    
+    # Auto Play on Boot Setting
+    @Slot(bool)
+    def setAutoPlayOnBoot(self, enabled):
+        print(f"🚀 Setting auto play on boot: {enabled}")
+        asyncio.create_task(self._set_auto_play_on_boot(enabled))
+    
+    async def _set_auto_play_on_boot(self, enabled):
+        if not self.session:
+            self.errorOccurred.emit("Backend not ready")
+            return
+        
+        try:
+            # Use the kiosk mode API endpoint for auto-play on boot
+            async with self.session.post(f"{self.base_url}/api/kiosk-mode", json={"enabled": enabled}) as resp:
+                if resp.status == 200:
+                    print(f"✅ Auto play on boot set to {enabled}")
+                    self._auto_play_on_boot = enabled
+                else:
+                    response_text = await resp.text()
+                    print(f"❌ Failed to set auto play: {resp.status} - {response_text}")
+        except Exception as e:
+            print(f"💥 Exception setting auto play: {e}")
+            self.errorOccurred.emit(str(e))
+    
+    async def _save_screen_timeout_setting(self, timeout_seconds):
+        if not self.session:
+            self.errorOccurred.emit("Backend not ready")
+            return
+        
+        try:
+            # Convert seconds to minutes for the main application API
+            timeout_minutes = timeout_seconds // 60
+            # Use the kiosk mode API endpoint to save screen timeout
+            async with self.session.post(f"{self.base_url}/api/kiosk-mode", json={
+                "enabled": self._auto_play_on_boot, 
+                "screen_timeout": timeout_minutes
+            }) as resp:
+                if resp.status == 200:
+                    print(f"✅ Screen timeout saved: {timeout_minutes} minutes")
+                else:
+                    response_text = await resp.text()
+                    print(f"❌ Failed to save screen timeout: {resp.status} - {response_text}")
+        except Exception as e:
+            print(f"💥 Exception saving screen timeout: {e}")
+            self.errorOccurred.emit(str(e))
+    
+    # Load Settings
+    @Slot()
+    def loadControlSettings(self):
+        print("📋 Loading control settings...")
+        asyncio.create_task(self._load_settings())
+    
+    async def _load_settings(self):
+        if not self.session:
+            self.errorOccurred.emit("Backend not ready")
+            return
+        
+        try:
+            # Load kiosk mode settings
+            async with self.session.get(f"{self.base_url}/api/kiosk-mode") as resp:
+                if resp.status == 200:
+                    data = await resp.json()
+                    self._auto_play_on_boot = data.get("enabled", False)
+                    # Load screen timeout from kiosk settings (convert minutes to seconds)
+                    screen_timeout_minutes = data.get("screen_timeout", 0)
+                    if screen_timeout_minutes > 0:
+                        self._screen_timeout = screen_timeout_minutes * 60
+                    print(f"🚀 Loaded auto play setting: {self._auto_play_on_boot}")
+                    print(f"🖥️ Loaded screen timeout: {screen_timeout_minutes} minutes ({self._screen_timeout} seconds)")
+            
+            # Serial status will be handled by WebSocket updates automatically
+            # But we still load the initial port info if connected
+            async with self.session.get(f"{self.base_url}/serial_status") as resp:
+                if resp.status == 200:
+                    data = await resp.json()
+                    initial_connected = data.get("connected", False)
+                    current_port = data.get("port", "")
+                    print(f"🔌 Initial serial status: connected={initial_connected}, port={current_port}")
+                    
+                    # Only update if WebSocket hasn't already set this
+                    if initial_connected and current_port and not self._current_port:
+                        self._current_port = current_port
+                        self.currentPortChanged.emit(current_port)
+                    
+                    # Set initial connection status (WebSocket will take over from here)
+                    if self._serial_connected != initial_connected:
+                        self._serial_connected = initial_connected
+                        self.serialConnectionChanged.emit(initial_connected)
+            
+            print("✅ Settings loaded - WebSocket will handle real-time updates")
+            self.settingsLoaded.emit()
+            
+        except Exception as e:
+            print(f"💥 Exception loading settings: {e}")
+            self.errorOccurred.emit(str(e))
+    
+    # Screen Management Properties
+    @Property(bool, notify=screenStateChanged)
+    def screenOn(self):
+        return self._screen_on
+    
+    @Property(int)
+    def screenTimeout(self):
+        return self._screen_timeout
+    
+    @screenTimeout.setter
+    def setScreenTimeout(self, timeout):
+        if self._screen_timeout != timeout:
+            self._screen_timeout = timeout
+            print(f"🖥️ Screen timeout set to {timeout} seconds")
+            # Save to main application's kiosk settings
+            asyncio.create_task(self._save_screen_timeout_setting(timeout))
+    
+    # Screen Control Methods
+    @Slot()
+    def turnScreenOn(self):
+        """Turn the screen on and reset activity timer"""
+        if not self._screen_on:
+            self._turn_screen_on()
+        self._reset_activity_timer()
+    
+    @Slot()
+    def turnScreenOff(self):
+        """Turn the screen off"""
+        self._turn_screen_off()
+        # Start touch monitoring after manual screen off
+        QTimer.singleShot(1000, self._start_touch_monitoring)  # 1 second delay
+    
+    @Slot()
+    def resetActivityTimer(self):
+        """Reset the activity timer (call on user interaction)"""
+        self._reset_activity_timer()
+        if not self._screen_on:
+            self._turn_screen_on()
+    
+    def _turn_screen_on(self):
+        """Internal method to turn screen on"""
+        with self._screen_transition_lock:
+            # Debounce: Don't turn on if we just changed state
+            time_since_change = time.time() - self._last_screen_change
+            if time_since_change < 2.0:  # 2 second debounce
+                print(f"🖥️ Screen state change blocked (debounce: {time_since_change:.1f}s < 2s)")
+                return
+            
+            if self._screen_on:
+                print("🖥️ Screen already ON, skipping")
+                return
+            
+            try:
+                # Use the working screen-on script if available
+                screen_on_script = Path('/usr/local/bin/screen-on')
+                if screen_on_script.exists():
+                    result = subprocess.run(['sudo', '/usr/local/bin/screen-on'], 
+                                          capture_output=True, text=True, timeout=5)
+                    if result.returncode == 0:
+                        print("🖥️ Screen turned ON (screen-on script)")
+                    else:
+                        print(f"⚠️ screen-on script failed: {result.stderr}")
+                else:
+                    # Fallback: Manual control matching the script
+                    # Unblank framebuffer and restore backlight
+                    max_brightness = 255
+                    try:
+                        result = subprocess.run(['cat', '/sys/class/backlight/*/max_brightness'], 
+                                              shell=True, capture_output=True, text=True, timeout=2)
+                        if result.returncode == 0 and result.stdout.strip():
+                            max_brightness = int(result.stdout.strip())
+                    except:
+                        pass
+                    
+                    subprocess.run(['sudo', 'sh', '-c', 
+                                  f'echo 0 > /sys/class/graphics/fb0/blank && echo {max_brightness} > /sys/class/backlight/*/brightness'], 
+                                 check=False, timeout=5)
+                    print(f"🖥️ Screen turned ON (manual, brightness: {max_brightness})")
+                
+                self._screen_on = True
+                self._last_screen_change = time.time()
+                self.screenStateChanged.emit(True)
+            
+            except Exception as e:
+                print(f"❌ Failed to turn screen on: {e}")
+    
+    def _turn_screen_off(self):
+        """Internal method to turn screen off"""
+        print("🖥️ _turn_screen_off() called")
+        with self._screen_transition_lock:
+            # Debounce: Don't turn off if we just changed state
+            time_since_change = time.time() - self._last_screen_change
+            if time_since_change < 2.0:  # 2 second debounce
+                print(f"🖥️ Screen state change blocked (debounce: {time_since_change:.1f}s < 2s)")
+                return
+            
+            if not self._screen_on:
+                print("🖥️ Screen already OFF, skipping")
+                return
+        
+        try:
+            # Use the working screen-off script if available
+            screen_off_script = Path('/usr/local/bin/screen-off')
+            print(f"🖥️ Checking for screen-off script at: {screen_off_script}")
+            print(f"🖥️ Script exists: {screen_off_script.exists()}")
+            
+            if screen_off_script.exists():
+                print("🖥️ Executing screen-off script...")
+                result = subprocess.run(['sudo', '/usr/local/bin/screen-off'], 
+                                      capture_output=True, text=True, timeout=10)
+                print(f"🖥️ Script return code: {result.returncode}")
+                if result.stdout:
+                    print(f"🖥️ Script stdout: {result.stdout}")
+                if result.stderr:
+                    print(f"🖥️ Script stderr: {result.stderr}")
+                    
+                if result.returncode == 0:
+                    print("✅ Screen turned OFF (screen-off script)")
+                else:
+                    print(f"⚠️ screen-off script failed: return code {result.returncode}")
+            else:
+                print("🖥️ Using manual screen control...")
+                # Fallback: Manual control matching the script
+                # Blank framebuffer and turn off backlight
+                subprocess.run(['sudo', 'sh', '-c', 
+                              'echo 0 > /sys/class/backlight/*/brightness && echo 1 > /sys/class/graphics/fb0/blank'], 
+                             check=False, timeout=5)
+                print("🖥️ Screen turned OFF (manual)")
+            
+            self._screen_on = False
+            self._last_screen_change = time.time()
+            self.screenStateChanged.emit(False)
+            print("🖥️ Screen state set to OFF, signal emitted")
+            
+        except Exception as e:
+            print(f"❌ Failed to turn screen off: {e}")
+            import traceback
+            traceback.print_exc()
+    
+    def _reset_activity_timer(self):
+        """Reset the last activity timestamp"""
+        old_time = self._last_activity
+        self._last_activity = time.time()
+        time_since_last = self._last_activity - old_time
+        if time_since_last > 1:  # Only log if it's been more than 1 second
+            print(f"🖥️ Activity detected - timer reset (was idle for {time_since_last:.1f}s)")
+    
+    def _check_screen_timeout(self):
+        """Check if screen should be turned off due to inactivity"""
+        if self._screen_on:
+            idle_time = time.time() - self._last_activity
+            # Log every 10 seconds when getting close to timeout
+            if idle_time > self._screen_timeout - 10 and idle_time % 10 < 1:
+                print(f"🖥️ Screen idle for {idle_time:.0f}s (timeout at {self._screen_timeout}s)")
+            
+            if idle_time > self._screen_timeout:
+                print(f"🖥️ Screen timeout reached! Idle for {idle_time:.0f}s (timeout: {self._screen_timeout}s)")
+                self._turn_screen_off()
+                # Add delay before starting touch monitoring to avoid catching residual events
+                QTimer.singleShot(1000, self._start_touch_monitoring)  # 1 second delay
+    
+    def _start_touch_monitoring(self):
+        """Start monitoring touch input for wake-up"""
+        if self._touch_monitor_thread is None or not self._touch_monitor_thread.is_alive():
+            self._touch_monitor_thread = threading.Thread(target=self._monitor_touch_input, daemon=True)
+            self._touch_monitor_thread.start()
+    
+    def _monitor_touch_input(self):
+        """Monitor touch input to wake up the screen"""
+        print("👆 Starting touch monitoring for wake-up")
+        # Add delay to let any residual touch events clear
+        time.sleep(2)
+        
+        # Flush touch device to clear any buffered events
+        try:
+            # Find and flush touch device
+            for i in range(5):
+                device = f'/dev/input/event{i}'
+                if Path(device).exists():
+                    try:
+                        # Read and discard any pending events
+                        with open(device, 'rb') as f:
+                            import fcntl
+                            import os
+                            fcntl.fcntl(f.fileno(), fcntl.F_SETFL, os.O_NONBLOCK)
+                            while True:
+                                try:
+                                    f.read(24)  # Standard input_event size
+                                except:
+                                    break
+                        print(f"👆 Flushed touch device: {device}")
+                        break
+                    except:
+                        continue
+        except Exception as e:
+            print(f"👆 Could not flush touch device: {e}")
+        
+        print("👆 Touch monitoring active")
+        try:
+            # Use external touch monitor script if available - but only if not too sensitive
+            touch_monitor_script = Path('/usr/local/bin/touch-monitor')
+            use_script = touch_monitor_script.exists() and hasattr(self, '_use_touch_script') and self._use_touch_script
+            
+            if use_script:
+                print("👆 Using touch-monitor script")
+                # Add extra delay for script-based monitoring since it's more sensitive
+                time.sleep(3)
+                print("👆 Starting touch-monitor script after flush delay")
+                process = subprocess.Popen(['sudo', '/usr/local/bin/touch-monitor'], 
+                                         stdout=subprocess.PIPE, 
+                                         stderr=subprocess.PIPE)
+                
+                # Wait for script to detect touch and wake screen
+                while not self._screen_on:
+                    if process.poll() is not None:  # Script exited (touch detected)
+                        print("👆 Touch detected by monitor script")
+                        self._turn_screen_on()
+                        self._reset_activity_timer()
+                        break
+                    time.sleep(0.1)
+                
+                if process.poll() is None:
+                    process.terminate()
+            else:
+                # Fallback: Direct monitoring
+                # Find touch input device
+                touch_device = None
+                for i in range(5):  # Check event0 through event4
+                    device = f'/dev/input/event{i}'
+                    if Path(device).exists():
+                        # Check if it's a touch device
+                        try:
+                            info = subprocess.run(['udevadm', 'info', '--query=all', f'--name={device}'], 
+                                                capture_output=True, text=True, timeout=2)
+                            if 'touch' in info.stdout.lower() or 'ft5406' in info.stdout.lower():
+                                touch_device = device
+                                break
+                        except:
+                            pass
+                
+                if not touch_device:
+                    touch_device = '/dev/input/event0'  # Default fallback
+                
+                print(f"👆 Monitoring touch device: {touch_device}")
+                
+                # Try evtest first (more responsive to single taps)
+                evtest_available = subprocess.run(['which', 'evtest'], 
+                                                 capture_output=True).returncode == 0
+                
+                if evtest_available:
+                    # Use evtest which is more sensitive to single touches
+                    print("👆 Using evtest for touch detection")
+                    process = subprocess.Popen(['sudo', 'evtest', touch_device], 
+                                             stdout=subprocess.PIPE, 
+                                             stderr=subprocess.DEVNULL,
+                                             text=True)
+                    
+                    # Wait for any event line
+                    while not self._screen_on:
+                        try:
+                            line = process.stdout.readline()
+                            if line and 'Event:' in line:
+                                print("👆 Touch detected via evtest - waking screen")
+                                process.terminate()
+                                self._turn_screen_on()
+                                self._reset_activity_timer()
+                                break
+                        except:
+                            pass
+                        
+                        if process.poll() is not None:
+                            break
+                        time.sleep(0.01)  # Small sleep to prevent CPU spinning
+                else:
+                    # Fallback: Use cat with single byte read (more responsive)
+                    print("👆 Using cat for touch detection")
+                    process = subprocess.Popen(['sudo', 'cat', touch_device], 
+                                             stdout=subprocess.PIPE, 
+                                             stderr=subprocess.DEVNULL)
+                    
+                    # Wait for any data (even 1 byte indicates touch)
+                    while not self._screen_on:
+                        try:
+                            # Non-blocking check for data
+                            import select
+                            ready, _, _ = select.select([process.stdout], [], [], 0.1)
+                            if ready:
+                                data = process.stdout.read(1)  # Read just 1 byte
+                                if data:
+                                    print("👆 Touch detected - waking screen")
+                                    process.terminate()
+                                    self._turn_screen_on()
+                                    self._reset_activity_timer()
+                                    break
+                        except:
+                            pass
+                        
+                        # Check if screen was turned on by other means
+                        if self._screen_on:
+                            process.terminate()
+                            break
+                        
+                        time.sleep(0.1)
+                
+        except Exception as e:
+            print(f"❌ Error monitoring touch input: {e}")
+        
+        print("👆 Touch monitoring stopped")

+ 170 - 0
dune-weaver-touch/configure-boot.sh

@@ -0,0 +1,170 @@
+#!/bin/bash
+# Configure boot and session options for Dune Weaver Touch
+
+set -e
+
+if [ "$EUID" -ne 0 ]; then
+    echo "❌ This script must be run as root (use sudo)"
+    exit 1
+fi
+
+ACTUAL_USER="${SUDO_USER:-$USER}"
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+
+echo "🔧 Dune Weaver Touch - Boot Configuration"
+echo "=========================================="
+echo "Current user: $ACTUAL_USER"
+echo ""
+
+show_current_config() {
+    echo "📊 Current Configuration:"
+    
+    # Check auto-login status
+    if [ -f /etc/lightdm/lightdm.conf.d/60-autologin.conf ]; then
+        echo "   ✅ Auto-login: Enabled (lightdm)"
+    elif grep -q "autologin-user=" /etc/lightdm/lightdm.conf 2>/dev/null; then
+        echo "   ✅ Auto-login: Enabled (lightdm main config)"
+    elif [ -f /etc/systemd/system/getty@tty1.service.d/override.conf ]; then
+        echo "   ✅ Auto-login: Enabled (systemd)"
+    else
+        echo "   ❌ Auto-login: Disabled"
+    fi
+    
+    # Check service status
+    if systemctl is-enabled dune-weaver-touch >/dev/null 2>&1; then
+        echo "   ✅ Service: Enabled (starts on boot)"
+    else
+        echo "   ❌ Service: Disabled"
+    fi
+    
+    # Check kiosk session
+    if [ -f /usr/share/xsessions/dune-weaver-kiosk.session ]; then
+        echo "   ✅ Kiosk Session: Available"
+    else
+        echo "   ❌ Kiosk Session: Not installed"
+    fi
+    
+    echo ""
+}
+
+enable_auto_login() {
+    echo "🔑 Enabling auto-login..."
+    
+    if [ -d /etc/lightdm ]; then
+        mkdir -p /etc/lightdm/lightdm.conf.d
+        cat > /etc/lightdm/lightdm.conf.d/60-autologin.conf << EOF
+[Seat:*]
+autologin-user=$ACTUAL_USER
+autologin-user-timeout=0
+user-session=LXDE-pi
+EOF
+        echo "   ✅ lightdm auto-login enabled"
+    else
+        # Fallback to systemd
+        mkdir -p /etc/systemd/system/getty@tty1.service.d
+        cat > /etc/systemd/system/getty@tty1.service.d/override.conf << EOF
+[Service]
+ExecStart=
+ExecStart=-/sbin/agetty --autologin $ACTUAL_USER --noclear %I \$TERM
+EOF
+        systemctl daemon-reload
+        echo "   ✅ systemd auto-login enabled"
+    fi
+}
+
+disable_auto_login() {
+    echo "🔑 Disabling auto-login..."
+    
+    # Remove lightdm auto-login configs
+    rm -f /etc/lightdm/lightdm.conf.d/60-autologin.conf
+    
+    if [ -f /etc/lightdm/lightdm.conf ]; then
+        sed -i "s/^autologin-user=.*/#autologin-user=/" /etc/lightdm/lightdm.conf
+    fi
+    
+    # Remove systemd auto-login
+    rm -rf /etc/systemd/system/getty@tty1.service.d
+    systemctl daemon-reload
+    
+    echo "   ✅ Auto-login disabled"
+}
+
+enable_service() {
+    echo "🚀 Enabling Dune Weaver service..."
+    systemctl enable dune-weaver-touch.service
+    echo "   ✅ Service will start on boot"
+}
+
+disable_service() {
+    echo "🚀 Disabling Dune Weaver service..."
+    systemctl disable dune-weaver-touch.service
+    systemctl stop dune-weaver-touch.service 2>/dev/null || true
+    echo "   ✅ Service disabled"
+}
+
+# Show current status
+show_current_config
+
+# Main menu
+echo "Choose configuration:"
+echo "1) Full Kiosk Mode (auto-login + service enabled)"
+echo "2) Service Only (manual login, auto-start service)"  
+echo "3) Manual Mode (manual login, manual start)"
+echo "4) Toggle auto-login only"
+echo "5) Toggle service only"
+echo "6) Show current status"
+echo "7) Exit"
+echo ""
+read -p "Enter your choice (1-7): " choice
+
+case $choice in
+    1)
+        enable_auto_login
+        enable_service
+        echo ""
+        echo "🎯 Full kiosk mode enabled!"
+        echo "   Reboot to see the changes"
+        ;;
+    2)
+        disable_auto_login
+        enable_service
+        echo ""
+        echo "🔧 Service-only mode enabled!"
+        echo "   Manual login required, but service starts automatically"
+        ;;
+    3)
+        disable_auto_login
+        disable_service
+        echo ""
+        echo "🛠️  Manual mode enabled!"
+        echo "   Manual login and manual service start required"
+        ;;
+    4)
+        if [ -f /etc/lightdm/lightdm.conf.d/60-autologin.conf ] || [ -f /etc/systemd/system/getty@tty1.service.d/override.conf ]; then
+            disable_auto_login
+        else
+            enable_auto_login
+        fi
+        ;;
+    5)
+        if systemctl is-enabled dune-weaver-touch >/dev/null 2>&1; then
+            disable_service
+        else
+            enable_service
+        fi
+        ;;
+    6)
+        show_current_config
+        ;;
+    7)
+        echo "👋 Exiting..."
+        exit 0
+        ;;
+    *)
+        echo "❌ Invalid choice"
+        exit 1
+        ;;
+esac
+
+echo ""
+echo "✅ Configuration updated!"

+ 9 - 0
dune-weaver-touch/dune-weaver-touch.desktop

@@ -0,0 +1,9 @@
+[Desktop Entry]
+Type=Application
+Name=Dune Weaver Touch
+Comment=Dune Weaver Touch Interface
+Exec=python3 /home/pi/dune-weaver-touch/main.py
+Path=/home/pi/dune-weaver-touch
+Terminal=false
+StartupNotify=true
+X-GNOME-Autostart-enabled=true

+ 19 - 0
dune-weaver-touch/dune-weaver-touch.service

@@ -0,0 +1,19 @@
+[Unit]
+Description=Dune Weaver Touch Interface
+After=graphical.target
+Wants=graphical.target
+
+[Service]
+Type=simple
+User=pi
+Group=pi
+WorkingDirectory=/home/pi/dune-weaver-touch
+Environment=DISPLAY=:0
+Environment=QT_QPA_PLATFORM=eglfs
+Environment=QT_QPA_EGLFS_ALWAYS_SET_MODE=1
+ExecStart=/home/pi/dune-weaver-touch/venv/bin/python /home/pi/dune-weaver-touch/main.py
+Restart=always
+RestartSec=5
+
+[Install]
+WantedBy=graphical.target

+ 46 - 0
dune-weaver-touch/install-service.sh

@@ -0,0 +1,46 @@
+#!/bin/bash
+
+# Install Dune Weaver Touch as a systemd service
+
+# Check if running as root
+if [ "$EUID" -ne 0 ]; then
+    echo "Please run as root (use sudo)"
+    exit 1
+fi
+
+# Get the current directory
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+APP_DIR="$SCRIPT_DIR"
+
+# Get the current user (the one who called sudo)
+ACTUAL_USER="${SUDO_USER:-$USER}"
+USER_HOME=$(eval echo ~$ACTUAL_USER)
+
+echo "Installing Dune Weaver Touch service..."
+echo "App directory: $APP_DIR"
+echo "User: $ACTUAL_USER"
+echo "User home: $USER_HOME"
+
+# Update paths in the service file
+sed "s|/home/pi/dune-weaver-touch|$APP_DIR|g" "$SCRIPT_DIR/dune-weaver-touch.service" > /tmp/dune-weaver-touch.service
+sed -i "s|User=pi|User=$ACTUAL_USER|g" /tmp/dune-weaver-touch.service
+sed -i "s|Group=pi|Group=$ACTUAL_USER|g" /tmp/dune-weaver-touch.service
+
+# Copy service file to systemd directory
+cp /tmp/dune-weaver-touch.service /etc/systemd/system/
+
+# Reload systemd
+systemctl daemon-reload
+
+# Enable the service
+systemctl enable dune-weaver-touch.service
+
+echo "✅ Service installed and enabled!"
+echo ""
+echo "Commands to manage the service:"
+echo "  Start:   sudo systemctl start dune-weaver-touch"
+echo "  Stop:    sudo systemctl stop dune-weaver-touch"  
+echo "  Status:  sudo systemctl status dune-weaver-touch"
+echo "  Logs:    sudo journalctl -u dune-weaver-touch -f"
+echo ""
+echo "The app will now start automatically on boot."

+ 195 - 0
dune-weaver-touch/install.sh

@@ -0,0 +1,195 @@
+#!/bin/bash
+# Dune Weaver Touch - One-Command Installer
+# This script sets up everything needed to run Dune Weaver Touch on boot
+
+set -e
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+ACTUAL_USER="${SUDO_USER:-$USER}"
+USER_HOME=$(eval echo ~$ACTUAL_USER)
+
+echo "🎯 Dune Weaver Touch - Complete Installation"
+echo "============================================="
+echo "App directory: $SCRIPT_DIR"
+echo "User: $ACTUAL_USER"
+echo ""
+
+# Check if running as root
+if [ "$EUID" -ne 0 ]; then
+    echo "❌ This installer must be run with sudo privileges"
+    echo ""
+    echo "Usage: sudo ./install.sh"
+    echo ""
+    exit 1
+fi
+
+echo "🔧 Installing system components..."
+echo ""
+
+# Function to install system scripts
+install_scripts() {
+    echo "📄 Installing system scripts..."
+    
+    local scripts=("screen-on" "screen-off" "touch-monitor")
+    
+    for script in "${scripts[@]}"; do
+        local source_path="$SCRIPT_DIR/scripts/$script"
+        local target_path="/usr/local/bin/$script"
+        
+        if [ -f "$source_path" ]; then
+            cp "$source_path" "$target_path"
+            chmod 755 "$target_path"
+            chown root:root "$target_path"
+            echo "   ✅ $script → $target_path"
+        else
+            echo "   ⚠️  $script not found at $source_path"
+        fi
+    done
+    
+    echo "   📄 System scripts installed"
+}
+
+# Function to setup systemd service
+setup_systemd() {
+    echo "🚀 Setting up systemd service..."
+    
+    # Update paths in the service file
+    sed "s|/home/pi/dune-weaver-touch|$SCRIPT_DIR|g" "$SCRIPT_DIR/dune-weaver-touch.service" > /tmp/dune-weaver-touch.service
+    sed -i "s|User=pi|User=$ACTUAL_USER|g" /tmp/dune-weaver-touch.service
+    sed -i "s|Group=pi|Group=$ACTUAL_USER|g" /tmp/dune-weaver-touch.service
+    
+    # Ensure the ExecStart uses the venv python
+    sed -i "s|ExecStart=.*python.*|ExecStart=$SCRIPT_DIR/venv/bin/python $SCRIPT_DIR/main.py|g" /tmp/dune-weaver-touch.service
+    
+    # Copy service file
+    cp /tmp/dune-weaver-touch.service /etc/systemd/system/
+    
+    # Enable service
+    systemctl daemon-reload
+    systemctl enable dune-weaver-touch.service
+    
+    echo "   🚀 Systemd service installed and enabled"
+}
+
+# Function to setup kiosk optimizations
+setup_kiosk_optimizations() {
+    echo "🖥️  Setting up kiosk optimizations..."
+    
+    # Disable boot messages for cleaner boot
+    if ! grep -q "quiet splash" /boot/cmdline.txt 2>/dev/null; then
+        if [ -f /boot/cmdline.txt ]; then
+            cp /boot/cmdline.txt /boot/cmdline.txt.backup
+            sed -i 's/$/ quiet splash/' /boot/cmdline.txt
+            echo "   ✅ Boot splash enabled"
+        fi
+    else
+        echo "   ℹ️  Boot splash already enabled"
+    fi
+    
+    # Disable rainbow splash
+    if ! grep -q "disable_splash=1" /boot/config.txt 2>/dev/null; then
+        if [ -f /boot/config.txt ]; then
+            echo "disable_splash=1" >> /boot/config.txt
+            echo "   ✅ Rainbow splash disabled"
+        fi
+    else
+        echo "   ℹ️  Rainbow splash already disabled"
+    fi
+    
+    # Note about auto-login - let user configure manually
+    echo "   ℹ️  Auto-login configuration skipped (manual setup recommended)"
+    
+    echo "   🖥️  Kiosk optimizations applied"
+}
+
+# Function to setup Python virtual environment and install dependencies
+setup_python_environment() {
+    echo "🐍 Setting up Python virtual environment..."
+    
+    # Create virtual environment if it doesn't exist
+    if [ ! -d "$SCRIPT_DIR/venv" ]; then
+        echo "   📦 Creating virtual environment..."
+        python3 -m venv "$SCRIPT_DIR/venv" || {
+            echo "   ⚠️  Could not create virtual environment. Installing python3-venv..."
+            apt update && apt install -y python3-venv python3-full
+            python3 -m venv "$SCRIPT_DIR/venv"
+        }
+    else
+        echo "   ℹ️  Virtual environment already exists"
+    fi
+    
+    # Activate virtual environment and install dependencies
+    echo "   📦 Installing Python dependencies in virtual environment..."
+    "$SCRIPT_DIR/venv/bin/python" -m pip install --upgrade pip
+    
+    # Install from requirements.txt if it exists, otherwise install manually
+    if [ -f "$SCRIPT_DIR/requirements.txt" ]; then
+        "$SCRIPT_DIR/venv/bin/pip" install -r "$SCRIPT_DIR/requirements.txt"
+    else
+        "$SCRIPT_DIR/venv/bin/pip" install PySide6 requests
+    fi
+    
+    # Change ownership to the actual user (not root)
+    chown -R "$ACTUAL_USER:$ACTUAL_USER" "$SCRIPT_DIR/venv"
+    
+    echo "   🐍 Python virtual environment ready"
+}
+
+# Main installation process
+echo "Starting complete installation..."
+echo ""
+
+# Install everything
+setup_python_environment
+install_scripts
+setup_systemd
+setup_kiosk_optimizations
+
+echo ""
+echo "🎉 Installation Complete!"
+echo "========================"
+echo ""
+echo "✅ Python virtual environment created at: $SCRIPT_DIR/venv"
+echo "✅ System scripts installed in /usr/local/bin/"
+echo "✅ Systemd service configured for auto-start"
+echo "✅ Kiosk optimizations applied"
+echo ""
+echo "🔧 Service Management:"
+echo "   Start now:  sudo systemctl start dune-weaver-touch"
+echo "   Stop:       sudo systemctl stop dune-weaver-touch"
+echo "   Status:     sudo systemctl status dune-weaver-touch"
+echo "   Logs:       sudo journalctl -u dune-weaver-touch -f"
+echo ""
+echo "🚀 Next Steps:"
+echo "   1. Configure auto-login (recommended for kiosk mode):"
+echo "      sudo ./setup-autologin.sh    (automated setup)"
+echo "      OR manually: sudo raspi-config → System Options → Boot/Auto Login → Desktop Autologin"
+echo "   2. Reboot your system to see the full kiosk experience"
+echo "   3. The app will start automatically on boot via systemd service"
+echo "   4. Check the logs if you encounter any issues"
+echo ""
+echo "💡 To start the service now without rebooting:"
+echo "   sudo systemctl start dune-weaver-touch"
+echo ""
+echo "🛠️  For development/testing (run manually):"
+echo "   cd $SCRIPT_DIR"
+echo "   ./run.sh"
+echo ""
+echo "⚙️  To change boot/login settings later:"
+echo "   sudo ./configure-boot.sh"
+echo ""
+
+# Ask if user wants to start now
+read -p "🤔 Would you like to start the service now? (y/N): " -n 1 -r
+echo
+if [[ $REPLY =~ ^[Yy]$ ]]; then
+    echo "🚀 Starting Dune Weaver Touch service..."
+    systemctl start dune-weaver-touch
+    sleep 2
+    systemctl status dune-weaver-touch --no-pager -l
+    echo ""
+    echo "✅ Service started! Check the status above."
+fi
+
+echo ""
+echo "🎯 Installation completed successfully!"

+ 43 - 0
dune-weaver-touch/main.py

@@ -0,0 +1,43 @@
+import sys
+import os
+import asyncio
+from pathlib import Path
+from PySide6.QtCore import QUrl
+from PySide6.QtGui import QGuiApplication
+from PySide6.QtQml import QQmlApplicationEngine, qmlRegisterType
+from qasync import QEventLoop
+
+from backend import Backend
+from models.pattern_model import PatternModel
+from models.playlist_model import PlaylistModel
+
+def main():
+    # Enable virtual keyboard
+    os.environ['QT_IM_MODULE'] = 'qtvirtualkeyboard'
+    
+    app = QGuiApplication(sys.argv)
+    
+    # Setup async event loop
+    loop = QEventLoop(app)
+    asyncio.set_event_loop(loop)
+    
+    # Register types
+    qmlRegisterType(Backend, "DuneWeaver", 1, 0, "Backend")
+    qmlRegisterType(PatternModel, "DuneWeaver", 1, 0, "PatternModel")
+    qmlRegisterType(PlaylistModel, "DuneWeaver", 1, 0, "PlaylistModel")
+    
+    # Load QML
+    engine = QQmlApplicationEngine()
+    qml_file = Path(__file__).parent / "qml" / "main.qml"
+    engine.load(QUrl.fromLocalFile(str(qml_file)))
+    
+    if not engine.rootObjects():
+        return -1
+    
+    with loop:
+        loop.run_forever()
+    
+    return 0
+
+if __name__ == "__main__":
+    sys.exit(main())

+ 101 - 0
dune-weaver-touch/models/pattern_model.py

@@ -0,0 +1,101 @@
+from PySide6.QtCore import QAbstractListModel, Qt, Slot, Signal
+from PySide6.QtQml import QmlElement
+from pathlib import Path
+
+QML_IMPORT_NAME = "DuneWeaver"
+QML_IMPORT_MAJOR_VERSION = 1
+
+@QmlElement
+class PatternModel(QAbstractListModel):
+    """Model for pattern list with direct file system access"""
+    
+    NameRole = Qt.UserRole + 1
+    PathRole = Qt.UserRole + 2
+    PreviewRole = Qt.UserRole + 3
+    
+    def __init__(self):
+        super().__init__()
+        self._patterns = []
+        self._filtered_patterns = []
+        # Look for patterns in the parent directory (main dune-weaver folder)
+        self.patterns_dir = Path("../patterns")
+        self.cache_dir = Path("../patterns/cached_images")
+        self.refresh()
+    
+    def roleNames(self):
+        return {
+            self.NameRole: b"name",
+            self.PathRole: b"path", 
+            self.PreviewRole: b"preview"
+        }
+    
+    def rowCount(self, parent=None):
+        return len(self._filtered_patterns)
+    
+    def data(self, index, role):
+        if not index.isValid() or index.row() >= len(self._filtered_patterns):
+            return None
+        
+        pattern = self._filtered_patterns[index.row()]
+        
+        if role == self.NameRole:
+            return pattern["name"]
+        elif role == self.PathRole:
+            return pattern["path"]
+        elif role == self.PreviewRole:
+            # For patterns in subdirectories, check both flattened and hierarchical cache structures
+            pattern_name = pattern["name"]
+            
+            # Try PNG format for kiosk compatibility
+            # First try hierarchical structure (preserving subdirectories)
+            preview_path_hierarchical = self.cache_dir / f"{pattern_name}.png"
+            if preview_path_hierarchical.exists():
+                return str(preview_path_hierarchical.absolute())
+            
+            # Then try flattened structure (replace / with _)
+            preview_name_flat = pattern_name.replace("/", "_").replace("\\", "_")
+            preview_path_flat = self.cache_dir / f"{preview_name_flat}.png"
+            if preview_path_flat.exists():
+                return str(preview_path_flat.absolute())
+            
+            # Fallback to WebP if PNG not found (for existing caches)
+            preview_path_hierarchical_webp = self.cache_dir / f"{pattern_name}.webp"
+            if preview_path_hierarchical_webp.exists():
+                return str(preview_path_hierarchical_webp.absolute())
+            
+            preview_path_flat_webp = self.cache_dir / f"{preview_name_flat}.webp"
+            if preview_path_flat_webp.exists():
+                return str(preview_path_flat_webp.absolute())
+            
+            return ""
+        
+        return None
+    
+    @Slot()
+    def refresh(self):
+        print(f"Loading patterns from: {self.patterns_dir.absolute()}")
+        self.beginResetModel()
+        patterns = []
+        for file_path in self.patterns_dir.rglob("*.thr"):
+            relative = file_path.relative_to(self.patterns_dir)
+            patterns.append({
+                "name": str(relative),
+                "path": str(file_path)
+            })
+        self._patterns = sorted(patterns, key=lambda x: x["name"])
+        self._filtered_patterns = self._patterns.copy()
+        print(f"Loaded {len(self._patterns)} patterns")
+        self.endResetModel()
+    
+    @Slot(str)
+    def filter(self, search_text):
+        self.beginResetModel()
+        if not search_text:
+            self._filtered_patterns = self._patterns.copy()
+        else:
+            search_lower = search_text.lower()
+            self._filtered_patterns = [
+                p for p in self._patterns 
+                if search_lower in p["name"].lower()
+            ]
+        self.endResetModel()

+ 85 - 0
dune-weaver-touch/models/playlist_model.py

@@ -0,0 +1,85 @@
+from PySide6.QtCore import QAbstractListModel, Qt, Slot, Signal
+from PySide6.QtQml import QmlElement
+from pathlib import Path
+import json
+
+QML_IMPORT_NAME = "DuneWeaver"
+QML_IMPORT_MAJOR_VERSION = 1
+
+@QmlElement
+class PlaylistModel(QAbstractListModel):
+    """Model for playlist list with direct JSON file access"""
+    
+    NameRole = Qt.UserRole + 1
+    ItemCountRole = Qt.UserRole + 2
+    
+    def __init__(self):
+        super().__init__()
+        self._playlists = []
+        # Look for playlists in the parent directory (main dune-weaver folder)
+        self.playlists_file = Path("../playlists.json")
+        self.refresh()
+    
+    def roleNames(self):
+        return {
+            self.NameRole: b"name",
+            self.ItemCountRole: b"itemCount"
+        }
+    
+    def rowCount(self, parent=None):
+        return len(self._playlists)
+    
+    def data(self, index, role):
+        if not index.isValid() or index.row() >= len(self._playlists):
+            return None
+        
+        playlist = self._playlists[index.row()]
+        
+        if role == self.NameRole:
+            return playlist["name"]
+        elif role == self.ItemCountRole:
+            return playlist["itemCount"]
+        
+        return None
+    
+    @Slot()
+    def refresh(self):
+        self.beginResetModel()
+        playlists = []
+        
+        if self.playlists_file.exists():
+            try:
+                with open(self.playlists_file, 'r') as f:
+                    self._playlist_data = json.load(f)
+                    for name, playlist_patterns in self._playlist_data.items():
+                        # playlist_patterns is a list of pattern filenames
+                        playlists.append({
+                            "name": name,
+                            "itemCount": len(playlist_patterns) if isinstance(playlist_patterns, list) else 0
+                        })
+            except (json.JSONDecodeError, KeyError, AttributeError):
+                self._playlist_data = {}
+        else:
+            self._playlist_data = {}
+        
+        self._playlists = sorted(playlists, key=lambda x: x["name"])
+        self.endResetModel()
+    
+    @Slot(str, result=list)
+    def getPatternsForPlaylist(self, playlistName):
+        """Get the list of patterns for a given playlist"""
+        if hasattr(self, '_playlist_data') and playlistName in self._playlist_data:
+            patterns = self._playlist_data[playlistName]
+            if isinstance(patterns, list):
+                # Clean up pattern names for display
+                cleaned_patterns = []
+                for pattern in patterns:
+                    # Remove path and .thr extension for display
+                    clean_name = pattern
+                    if '/' in clean_name:
+                        clean_name = clean_name.split('/')[-1]
+                    if clean_name.endswith('.thr'):
+                        clean_name = clean_name[:-4]
+                    cleaned_patterns.append(clean_name)
+                return cleaned_patterns
+        return []

+ 108 - 0
dune-weaver-touch/qml/components/BottomNavTab.qml

@@ -0,0 +1,108 @@
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+
+Rectangle {
+    property string icon: ""
+    property string text: ""
+    property bool active: false
+    
+    signal clicked()
+    
+    color: "transparent"
+    
+    // Active indicator (blue bottom border)
+    Rectangle {
+        anchors.bottom: parent.bottom
+        width: parent.width
+        height: 3
+        color: active ? "#2563eb" : "transparent"
+        
+        Behavior on color {
+            ColorAnimation { duration: 200 }
+        }
+    }
+    
+    Column {
+        anchors.centerIn: parent
+        spacing: 4
+        
+        // Icon (using emoji for cross-platform compatibility)
+        Text {
+            property string iconValue: parent.parent.icon
+            text: {
+                // Debug log the icon value
+                console.log("BottomNavTab icon value:", iconValue)
+                
+                // Map icon names to emoji equivalents directly
+                switch(iconValue) {
+                    case "search": return "🔍"
+                    case "list_alt": return "📋" 
+                    case "table_chart": return "⚙️"
+                    case "play_arrow": return "▶️"
+                    default: {
+                        console.log("Unknown icon:", iconValue, "- using default")
+                        return "📄"  // Default icon if mapping fails
+                    }
+                }
+            }
+            font.pixelSize: 20
+            font.family: "sans-serif"  // Use system sans-serif font
+            color: parent.parent.active ? "#2563eb" : "#6b7280"
+            anchors.horizontalCenter: parent.horizontalCenter
+            
+            Behavior on color {
+                ColorAnimation { duration: 200 }
+            }
+        }
+        
+        // Label
+        Label {
+            text: parent.parent.text
+            font.pixelSize: 11
+            font.weight: Font.Medium
+            color: parent.parent.active ? "#2563eb" : "#6b7280"
+            anchors.horizontalCenter: parent.horizontalCenter
+            
+            Behavior on color {
+                ColorAnimation { duration: 200 }
+            }
+        }
+    }
+    
+    // Touch feedback
+    Rectangle {
+        id: touchFeedback
+        anchors.fill: parent
+        color: "#f3f4f6"
+        opacity: 0
+        radius: 0
+        
+        NumberAnimation {
+            id: touchAnimation
+            target: touchFeedback
+            property: "opacity"
+            from: 0.3
+            to: 0
+            duration: 200
+            easing.type: Easing.OutQuad
+        }
+    }
+    
+    MouseArea {
+        anchors.fill: parent
+        onClicked: parent.clicked()
+        
+        onPressed: {
+            touchAnimation.stop()
+            touchFeedback.opacity = 0.3
+        }
+        
+        onReleased: {
+            touchAnimation.start()
+        }
+        
+        onCanceled: {
+            touchAnimation.start()
+        }
+    }
+}

+ 66 - 0
dune-weaver-touch/qml/components/BottomNavigation.qml

@@ -0,0 +1,66 @@
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+import QtQuick.Layouts 1.15
+
+Rectangle {
+    id: bottomNav
+    
+    property int currentIndex: 0
+    signal tabClicked(int index)
+    
+    height: 55
+    color: "#ffffff"
+    
+    // Top border to match web UI
+    Rectangle {
+        anchors.top: parent.top
+        width: parent.width
+        height: 1
+        color: "#e5e7eb"
+    }
+    
+    RowLayout {
+        anchors.fill: parent
+        spacing: 0
+        
+        // Browse Tab
+        BottomNavTab {
+            Layout.fillWidth: true
+            Layout.fillHeight: true
+            icon: "search"
+            text: "Browse"
+            active: bottomNav.currentIndex === 0
+            onClicked: bottomNav.tabClicked(0)
+        }
+        
+        // Playlists Tab
+        BottomNavTab {
+            Layout.fillWidth: true
+            Layout.fillHeight: true
+            icon: "list_alt"
+            text: "Playlists"
+            active: bottomNav.currentIndex === 1
+            onClicked: bottomNav.tabClicked(1)
+        }
+        
+        // Table Control Tab
+        BottomNavTab {
+            Layout.fillWidth: true
+            Layout.fillHeight: true
+            icon: "table_chart"
+            text: "Control"
+            active: bottomNav.currentIndex === 2
+            onClicked: bottomNav.tabClicked(2)
+        }
+        
+        // Execution Tab
+        BottomNavTab {
+            Layout.fillWidth: true
+            Layout.fillHeight: true
+            icon: "play_arrow"
+            text: "Execution"
+            active: bottomNav.currentIndex === 3
+            onClicked: bottomNav.tabClicked(3)
+        }
+    }
+}

+ 61 - 0
dune-weaver-touch/qml/components/ConnectionStatus.qml

@@ -0,0 +1,61 @@
+import QtQuick 2.15
+
+Rectangle {
+    id: connectionDot
+    
+    property var backend: null
+    
+    width: 12
+    height: 12
+    radius: 6
+    
+    // Direct property binding to backend.serialConnected
+    color: {
+        if (!backend) {
+            console.log("ConnectionStatus: No backend available")
+            return "#FF5722"  // Red if no backend
+        }
+        
+        var connected = backend.serialConnected
+        console.log("ConnectionStatus: backend.serialConnected =", connected)
+        
+        if (connected === true) {
+            return "#4CAF50"  // Green if connected
+        } else {
+            return "#FF5722"  // Red if not connected
+        }
+    }
+    
+    // Listen for changes to trigger color update
+    Connections {
+        target: backend
+        
+        function onSerialConnectionChanged(connected) {
+            console.log("ConnectionStatus: serialConnectionChanged signal received:", connected)
+            // The color binding will automatically update
+        }
+    }
+    
+    // Debug logging
+    Component.onCompleted: {
+        console.log("ConnectionStatus: Component completed, backend =", backend)
+        if (backend) {
+            console.log("ConnectionStatus: initial serialConnected =", backend.serialConnected)
+        }
+    }
+    
+    onBackendChanged: {
+        console.log("ConnectionStatus: backend changed to", backend)
+        if (backend) {
+            console.log("ConnectionStatus: new backend serialConnected =", backend.serialConnected)
+        }
+    }
+    
+    // Animate color changes
+    Behavior on color {
+        ColorAnimation {
+            duration: 300
+            easing.type: Easing.OutQuart
+        }
+    }
+}

+ 66 - 0
dune-weaver-touch/qml/components/KeyboardHelper.qml

@@ -0,0 +1,66 @@
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+
+// Helper component to ensure virtual keyboard works properly
+Item {
+    id: keyboardHelper
+    
+    // Force show keyboard for a specific TextField
+    function showKeyboardFor(textField) {
+        textField.forceActiveFocus()
+        Qt.inputMethod.show()
+    }
+    
+    // Hide keyboard
+    function hideKeyboard() {
+        Qt.inputMethod.hide()
+    }
+    
+    // Enhanced TextField with proper keyboard support
+    component EnhancedTextField: TextField {
+        activeFocusOnPress: true
+        selectByMouse: true
+        
+        // Default input hints
+        inputMethodHints: Qt.ImhNone
+        
+        MouseArea {
+            anchors.fill: parent
+            onPressed: {
+                parent.forceActiveFocus()
+                Qt.inputMethod.show()
+                mouse.accepted = false
+            }
+        }
+        
+        onActiveFocusChanged: {
+            if (activeFocus) {
+                Qt.inputMethod.show()
+            }
+        }
+        
+        Keys.onReturnPressed: {
+            Qt.inputMethod.hide()
+            focus = false
+        }
+        
+        Keys.onEscapePressed: {
+            Qt.inputMethod.hide()
+            focus = false
+        }
+    }
+    
+    // Numeric-only TextField
+    component NumericTextField: EnhancedTextField {
+        inputMethodHints: Qt.ImhDigitsOnly | Qt.ImhNoPredictiveText
+        validator: IntValidator { bottom: 0; top: 9999 }
+        
+        // Only allow numeric input
+        onTextChanged: {
+            var numeric = text.replace(/[^0-9]/g, '')
+            if (text !== numeric) {
+                text = numeric
+            }
+        }
+    }
+}

+ 103 - 0
dune-weaver-touch/qml/components/ModernControlButton.qml

@@ -0,0 +1,103 @@
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+import QtQuick.Layouts 1.15
+import QtQuick.Effects
+
+Rectangle {
+    property alias text: buttonLabel.text
+    property string icon: ""
+    property color buttonColor: "#2196F3"
+    property bool enabled: true
+    property int fontSize: 16
+    
+    signal clicked()
+    
+    radius: 12
+    color: enabled ? buttonColor : "#E0E0E0"
+    opacity: enabled ? 1.0 : 0.6
+    
+    // Gradient effect
+    gradient: Gradient {
+        GradientStop { position: 0; color: Qt.lighter(buttonColor, 1.1) }
+        GradientStop { position: 1; color: buttonColor }
+    }
+    
+    // Press animation
+    scale: mouseArea.pressed ? 0.95 : (mouseArea.containsMouse ? 1.02 : 1.0)
+    
+    Behavior on scale {
+        NumberAnimation { duration: 150; easing.type: Easing.OutQuad }
+    }
+    
+    Behavior on color {
+        ColorAnimation { duration: 200 }
+    }
+    
+    // Shadow effect
+    layer.enabled: true
+    layer.effect: MultiEffect {
+        shadowEnabled: true
+        shadowColor: "#25000000"
+        shadowBlur: 0.8
+        shadowVerticalOffset: 2
+    }
+    
+    RowLayout {
+        anchors.centerIn: parent
+        spacing: 8
+        
+        Text {
+            text: parent.parent.icon
+            font.pixelSize: parent.parent.fontSize + 2
+            visible: parent.parent.icon !== ""
+        }
+        
+        Label {
+            id: buttonLabel
+            color: "white"
+            font.pixelSize: parent.parent.fontSize
+            font.bold: true
+        }
+    }
+    
+    MouseArea {
+        id: mouseArea
+        anchors.fill: parent
+        hoverEnabled: true
+        enabled: parent.enabled
+        onClicked: parent.clicked()
+        
+        // Ripple effect
+        Rectangle {
+            id: ripple
+            width: 0
+            height: 0
+            radius: width / 2
+            color: "#40FFFFFF"
+            anchors.centerIn: parent
+            
+            NumberAnimation {
+                id: rippleAnimation
+                target: ripple
+                property: "width"
+                from: 0
+                to: parent.width * 1.2
+                duration: 400
+                easing.type: Easing.OutQuad
+                
+                onFinished: {
+                    ripple.width = 0
+                    ripple.height = 0
+                }
+            }
+            
+            Connections {
+                target: mouseArea
+                function onPressed() {
+                    ripple.height = ripple.width
+                    rippleAnimation.start()
+                }
+            }
+        }
+    }
+}

+ 152 - 0
dune-weaver-touch/qml/components/ModernPatternCard.qml

@@ -0,0 +1,152 @@
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+import QtQuick.Effects
+
+Rectangle {
+    property string name: ""
+    property alias preview: previewImage.source
+    
+    // Clean up the pattern name for display
+    property string cleanName: {
+        var cleanedName = name
+        // Remove path (get everything after the last slash)
+        var parts = cleanedName.split('/')
+        cleanedName = parts[parts.length - 1]
+        // Remove .thr extension
+        cleanedName = cleanedName.replace('.thr', '')
+        return cleanedName
+    }
+    
+    signal clicked()
+    
+    color: "white"
+    radius: 12
+    
+    // Drop shadow effect
+    layer.enabled: true
+    layer.effect: MultiEffect {
+        shadowEnabled: true
+        shadowColor: "#20000000"
+        shadowBlur: 0.8
+        shadowVerticalOffset: 2
+        shadowHorizontalOffset: 0
+    }
+    
+    // Hover/press animation
+    scale: mouseArea.pressed ? 0.95 : (mouseArea.containsMouse ? 1.02 : 1.0)
+    
+    Behavior on scale {
+        NumberAnimation { duration: 150; easing.type: Easing.OutQuad }
+    }
+    
+    Column {
+        anchors.fill: parent
+        anchors.margins: 8
+        spacing: 6
+        
+        // Preview image container
+        Rectangle {
+            width: parent.width
+            height: parent.height - nameLabel.height - 12
+            radius: 8
+            color: "#f8f8f8"
+            clip: true
+            
+            Image {
+                id: previewImage
+                anchors.fill: parent
+                fillMode: Image.PreserveAspectFit
+                source: preview ? "file:///" + preview : ""
+                smooth: true
+                
+                // Loading animation
+                opacity: status === Image.Ready ? 1 : 0
+                Behavior on opacity {
+                    NumberAnimation { duration: 200 }
+                }
+            }
+            
+            // Placeholder when no preview
+            Rectangle {
+                anchors.fill: parent
+                color: "#f0f0f0"
+                visible: previewImage.status === Image.Error || previewImage.source == ""
+                radius: 8
+                
+                Column {
+                    anchors.centerIn: parent
+                    spacing: 8
+                    
+                    Text {
+                        text: "🎨"
+                        font.pixelSize: 32
+                        anchors.horizontalCenter: parent.horizontalCenter
+                        color: "#ddd"
+                    }
+                    
+                    Text {
+                        text: "No Preview"
+                        anchors.horizontalCenter: parent.horizontalCenter
+                        color: "#999"
+                        font.pixelSize: 12
+                    }
+                }
+            }
+        }
+        
+        // Pattern name
+        Label {
+            id: nameLabel
+            text: cleanName
+            width: parent.width
+            elide: Label.ElideRight
+            horizontalAlignment: Label.AlignHCenter
+            font.pixelSize: 13
+            font.weight: Font.Medium
+            color: "#333"
+            wrapMode: Text.Wrap
+            maximumLineCount: 2
+        }
+    }
+    
+    // Click area
+    MouseArea {
+        id: mouseArea
+        anchors.fill: parent
+        hoverEnabled: true
+        onClicked: parent.clicked()
+        
+        // Ripple effect on click
+        Rectangle {
+            id: ripple
+            width: 0
+            height: 0
+            radius: width / 2
+            color: "#20000000"
+            anchors.centerIn: parent
+            
+            NumberAnimation {
+                id: rippleAnimation
+                target: ripple
+                property: "width"
+                from: 0
+                to: mouseArea.width * 1.5
+                duration: 300
+                easing.type: Easing.OutQuad
+                
+                onFinished: {
+                    ripple.width = 0
+                    ripple.height = 0
+                }
+            }
+            
+            Connections {
+                target: mouseArea
+                function onPressed() {
+                    ripple.height = ripple.width
+                    rippleAnimation.start()
+                }
+            }
+        }
+    }
+}

+ 53 - 0
dune-weaver-touch/qml/components/PatternCard.qml

@@ -0,0 +1,53 @@
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+
+Rectangle {
+    property alias name: nameLabel.text
+    property alias preview: previewImage.source
+    
+    signal clicked()
+    
+    color: mouseArea.pressed ? "#e0e0e0" : "#f5f5f5"
+    radius: 8
+    border.color: "#d0d0d0"
+    
+    Column {
+        anchors.fill: parent
+        anchors.margins: 10
+        spacing: 10
+        
+        Image {
+            id: previewImage
+            width: parent.width
+            height: parent.height - nameLabel.height - 10
+            fillMode: Image.PreserveAspectFit
+            source: preview ? "file:///" + preview : ""
+            
+            Rectangle {
+                anchors.fill: parent
+                color: "#f0f0f0"
+                visible: previewImage.status === Image.Error || previewImage.source == ""
+                
+                Text {
+                    anchors.centerIn: parent
+                    text: "No Preview"
+                    color: "#999"
+                }
+            }
+        }
+        
+        Label {
+            id: nameLabel
+            width: parent.width
+            elide: Label.ElideRight
+            horizontalAlignment: Label.AlignHCenter
+            font.pixelSize: 12
+        }
+    }
+    
+    MouseArea {
+        id: mouseArea
+        anchors.fill: parent
+        onClicked: parent.clicked()
+    }
+}

+ 53 - 0
dune-weaver-touch/qml/components/VirtualKeyboardLoader.qml

@@ -0,0 +1,53 @@
+import QtQuick 2.15
+import QtQuick.VirtualKeyboard 2.15
+import QtQuick.VirtualKeyboard.Settings 2.15
+
+Item {
+    id: keyboardLoader
+    
+    // Configure keyboard settings
+    Component.onCompleted: {
+        // Set keyboard style (can be "default", "retro", etc.)
+        VirtualKeyboardSettings.styleName = "default"
+        
+        // Set available locales (languages)
+        VirtualKeyboardSettings.activeLocales = ["en_US"]
+        
+        // Enable word candidate list
+        VirtualKeyboardSettings.wordCandidateList.enabled = true
+        
+        // Set keyboard height (as percentage of screen)
+        VirtualKeyboardSettings.fullScreenMode = false
+    }
+    
+    InputPanel {
+        id: inputPanel
+        z: 99999
+        y: window.height
+        anchors.left: parent.left
+        anchors.right: parent.right
+        
+        states: State {
+            name: "visible"
+            when: inputPanel.active
+            PropertyChanges {
+                target: inputPanel
+                y: window.height - inputPanel.height
+            }
+        }
+        
+        transitions: Transition {
+            from: ""
+            to: "visible"
+            reversible: true
+            ParallelAnimation {
+                NumberAnimation {
+                    target: inputPanel
+                    property: "y"
+                    duration: 250
+                    easing.type: Easing.InOutQuad
+                }
+            }
+        }
+    }
+}

+ 205 - 0
dune-weaver-touch/qml/main.qml

@@ -0,0 +1,205 @@
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+import QtQuick.Layouts 1.15
+import QtQuick.Dialogs
+import QtQuick.VirtualKeyboard 2.15
+import DuneWeaver 1.0
+import "components"
+
+ApplicationWindow {
+    id: window
+    visible: true
+    width: 800
+    height: 480
+    title: "Dune Weaver Touch"
+    
+    property int currentPageIndex: 0
+    property alias stackView: stackView
+    property alias backend: backend
+    property bool shouldNavigateToExecution: false
+    
+    onCurrentPageIndexChanged: {
+        console.log("📱 currentPageIndex changed to:", currentPageIndex)
+    }
+    
+    onShouldNavigateToExecutionChanged: {
+        if (shouldNavigateToExecution) {
+            console.log("🎯 Navigating to execution page")
+            console.log("🎯 Current stack depth:", stackView.depth)
+            
+            // If we're in a sub-page (like PatternDetailPage), pop back to main view first
+            if (stackView.depth > 1) {
+                console.log("🎯 Popping back to main view first")
+                stackView.pop()
+            }
+            
+            // Then navigate to ExecutionPage tab
+            console.log("🎯 Setting currentPageIndex to 3")
+            currentPageIndex = 3
+            shouldNavigateToExecution = false
+        }
+    }
+    
+    Backend {
+        id: backend
+        
+        onExecutionStarted: function(patternName, patternPreview) {
+            console.log("🎯 QML: ExecutionStarted signal received! patternName='" + patternName + "', preview='" + patternPreview + "'")
+            console.log("🎯 Setting shouldNavigateToExecution = true")
+            // Navigate to Execution tab (index 3) instead of pushing page
+            shouldNavigateToExecution = true
+            console.log("🎯 shouldNavigateToExecution set to:", shouldNavigateToExecution)
+        }
+        
+        onErrorOccurred: function(error) {
+            errorDialog.text = error
+            errorDialog.open()
+        }
+        
+        onScreenStateChanged: function(isOn) {
+            console.log("🖥️ Screen state changed:", isOn ? "ON" : "OFF")
+        }
+    }
+    
+    // Global touch/mouse handler for activity tracking
+    MouseArea {
+        anchors.fill: parent
+        acceptedButtons: Qt.NoButton  // Don't interfere with other mouse areas
+        hoverEnabled: true
+        propagateComposedEvents: true
+        
+        onPressed: {
+            console.log("🖥️ QML: Touch/press detected - resetting activity timer")
+            backend.resetActivityTimer()
+        }
+        
+        onPositionChanged: {
+            console.log("🖥️ QML: Mouse movement detected - resetting activity timer")
+            backend.resetActivityTimer()
+        }
+        
+        onClicked: {
+            console.log("🖥️ QML: Click detected - resetting activity timer")
+            backend.resetActivityTimer()
+        }
+    }
+    
+    PatternModel {
+        id: patternModel
+    }
+    
+    StackView {
+        id: stackView
+        anchors.fill: parent
+        initialItem: mainSwipeView
+        
+        Component {
+            id: mainSwipeView
+            
+            Item {
+                // Main content area
+                StackLayout {
+                    id: stackLayout
+                    anchors.top: parent.top
+                    anchors.left: parent.left
+                    anchors.right: parent.right
+                    anchors.bottom: bottomNav.top
+                    currentIndex: window.currentPageIndex
+                    
+                    Component.onCompleted: {
+                        console.log("📱 StackLayout created with currentIndex:", currentIndex, "bound to window.currentPageIndex:", window.currentPageIndex)
+                    }
+                    
+                    // Patterns Page
+                    Loader {
+                        source: "pages/ModernPatternListPage.qml"
+                        onLoaded: {
+                            item.patternModel = patternModel
+                            item.backend = backend
+                            item.stackView = stackView
+                        }
+                    }
+                    
+                    // Playlists Page  
+                    Loader {
+                        source: "pages/ModernPlaylistPage.qml"
+                        onLoaded: {
+                            item.backend = backend
+                            item.stackView = stackView
+                            item.mainWindow = window
+                        }
+                    }
+                    
+                    // Control Page
+                    Loader {
+                        source: "pages/TableControlPage.qml"
+                        onLoaded: {
+                            item.backend = backend
+                        }
+                    }
+                    
+                    // Execution Page
+                    Loader {
+                        source: "pages/ExecutionPage.qml"
+                        onLoaded: {
+                            item.backend = backend
+                            item.stackView = stackView
+                        }
+                    }
+                }
+                
+                // Bottom Navigation
+                BottomNavigation {
+                    id: bottomNav
+                    anchors.bottom: parent.bottom
+                    anchors.left: parent.left
+                    anchors.right: parent.right
+                    currentIndex: window.currentPageIndex
+                    
+                    onTabClicked: function(index) {
+                        console.log("📱 Tab clicked:", index)
+                        window.currentPageIndex = index
+                    }
+                }
+            }
+        }
+    }
+    
+    MessageDialog {
+        id: errorDialog
+        title: "Error"
+        buttons: MessageDialog.Ok
+    }
+    
+    // Virtual Keyboard Support
+    InputPanel {
+        id: inputPanel
+        z: 99999
+        y: window.height
+        anchors.left: parent.left
+        anchors.right: parent.right
+        
+        states: State {
+            name: "visible"
+            when: inputPanel.active
+            PropertyChanges {
+                target: inputPanel
+                y: window.height - inputPanel.height
+            }
+        }
+        
+        transitions: Transition {
+            from: ""
+            to: "visible"
+            reversible: true
+            ParallelAnimation {
+                NumberAnimation {
+                    target: inputPanel
+                    property: "y"
+                    duration: 250
+                    easing.type: Easing.InOutQuad
+                }
+            }
+        }
+    }
+}

+ 540 - 0
dune-weaver-touch/qml/pages/ExecutionPage.qml

@@ -0,0 +1,540 @@
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+import QtQuick.Layouts 1.15
+import Qt.labs.folderlistmodel 2.15
+import "../components"
+
+Page {
+    id: page
+    property var backend: null
+    property var stackView: null
+    property string patternName: ""
+    property string patternPreview: ""
+    
+    // Get current pattern info from backend
+    property string currentPattern: backend ? backend.currentFile : ""
+    property string currentPreviewPath: ""
+    property var allPossiblePaths: []
+    property int currentPathIndex: 0
+    property string activeImageSource: ""  // Separate property to avoid binding loop
+    property string repoRoot: ""  // Will hold the absolute path to repository root
+    property bool imageRetryInProgress: false  // Prevent multiple retry attempts
+    
+    // Debug backend connection
+    onBackendChanged: {
+        console.log("ExecutionPage: backend changed to", backend)
+        if (backend) {
+            console.log("ExecutionPage: backend.serialConnected =", backend.serialConnected)
+            console.log("ExecutionPage: backend.isConnected =", backend.isConnected)
+        }
+    }
+    
+    Component.onCompleted: {
+        console.log("ExecutionPage: Component completed, backend =", backend)
+        if (backend) {
+            console.log("ExecutionPage: initial serialConnected =", backend.serialConnected)
+        }
+        
+        // Find repository root directory
+        findRepoRoot()
+    }
+    
+    // Direct connection to backend signals
+    Connections {
+        target: backend
+        
+        function onSerialConnectionChanged(connected) {
+            console.log("ExecutionPage: received serialConnectionChanged signal:", connected)
+        }
+        
+        function onConnectionChanged() {
+            console.log("ExecutionPage: received connectionChanged signal")
+            if (backend) {
+                console.log("ExecutionPage: after connectionChanged, serialConnected =", backend.serialConnected)
+            }
+        }
+    }
+    
+    onCurrentPatternChanged: {
+        if (currentPattern) {
+            // Generate preview path from current pattern
+            updatePreviewPath()
+        }
+    }
+    
+    function updatePreviewPath() {
+        if (!currentPattern) {
+            console.log("🔍 No current pattern, clearing preview path")
+            currentPreviewPath = ""
+            return
+        }
+        
+        console.log("🔍 Updating preview for pattern:", currentPattern)
+        
+        // Extract just the filename from the path
+        var fileName = currentPattern.split('/').pop()  // Get last part of path
+        var baseName = fileName.replace(".thr", "")
+        console.log("🔍 File name:", fileName, "Base name:", baseName)
+        
+        // Use absolute paths based on discovered repository root
+        var possibleBasePaths = []
+        
+        if (repoRoot) {
+            // Use the discovered repository root
+            possibleBasePaths = [
+                "file://" + repoRoot + "/patterns/cached_images/"
+            ]
+            console.log("🎯 Using repository root for paths:", repoRoot)
+        } else {
+            console.log("⚠️ Repository root not found, using fallback relative paths")
+            // Fallback to relative paths if repo root discovery failed
+            possibleBasePaths = [
+                "../../../patterns/cached_images/",  // Three levels up from QML file location
+                "../../patterns/cached_images/",     // Two levels up (backup)
+                "../../../../patterns/cached_images/"  // Four levels up (backup)
+            ]
+        }
+        
+        var possiblePaths = []
+        
+        // Build paths using all possible base paths
+        // Prioritize PNG format since WebP is not supported on this system
+        for (var i = 0; i < possibleBasePaths.length; i++) {
+            var basePath = possibleBasePaths[i]
+            // First try with .thr suffix (e.g., pattern.thr.png) - PNG first since WebP failed
+            possiblePaths.push(basePath + fileName + ".png")
+            possiblePaths.push(basePath + fileName + ".jpg")
+            possiblePaths.push(basePath + fileName + ".jpeg")
+            // Then try without .thr suffix (e.g., pattern.png) 
+            possiblePaths.push(basePath + baseName + ".png")
+            possiblePaths.push(basePath + baseName + ".jpg")
+            possiblePaths.push(basePath + baseName + ".jpeg")
+        }
+        
+        console.log("🔍 Possible preview paths:", JSON.stringify(possiblePaths))
+        
+        // Store all possible paths for fallback mechanism
+        allPossiblePaths = possiblePaths
+        currentPathIndex = 0
+        
+        // Set the active image source to avoid binding loops
+        if (possiblePaths.length > 0) {
+            currentPreviewPath = possiblePaths[0]
+            activeImageSource = possiblePaths[0]
+            console.log("🎯 Setting preview path to:", currentPreviewPath)
+            console.log("🎯 Setting active image source to:", activeImageSource)
+        } else {
+            console.log("❌ No possible paths found")
+            currentPreviewPath = ""
+            activeImageSource = ""
+        }
+    }
+    
+    function tryNextPreviewPath() {
+        if (allPossiblePaths.length === 0) {
+            console.log("❌ No more paths to try")
+            return false
+        }
+        
+        currentPathIndex++
+        if (currentPathIndex >= allPossiblePaths.length) {
+            console.log("❌ All paths exhausted")
+            return false
+        }
+        
+        currentPreviewPath = allPossiblePaths[currentPathIndex]
+        activeImageSource = allPossiblePaths[currentPathIndex]
+        console.log("🔄 Trying next preview path:", currentPreviewPath)
+        console.log("🔄 Setting active image source to:", activeImageSource)
+        return true
+    }
+    
+    function findRepoRoot() {
+        // Start from the current QML file location and work our way up
+        var currentPath = Qt.resolvedUrl(".").toString()
+        console.log("🔍 Starting search from QML file location:", currentPath)
+        
+        // Remove file:// prefix and get directory parts
+        if (currentPath.startsWith("file://")) {
+            currentPath = currentPath.substring(7)
+        }
+        
+        var pathParts = currentPath.split("/")
+        console.log("🔍 Path parts:", JSON.stringify(pathParts))
+        
+        // Look for the dune-weaver directory by going up the path
+        for (var i = pathParts.length - 1; i >= 0; i--) {
+            if (pathParts[i] === "dune-weaver" || pathParts[i] === "dune-weaver-touch") {
+                // Found it! Build the repo root path
+                var rootPath = "/" + pathParts.slice(1, i + (pathParts[i] === "dune-weaver" ? 1 : 0)).join("/")
+                if (pathParts[i] === "dune-weaver-touch") {
+                    // We need to go up one more level to get to dune-weaver
+                    rootPath = "/" + pathParts.slice(1, i).join("/")
+                }
+                repoRoot = rootPath
+                console.log("🎯 Found repository root:", repoRoot)
+                return
+            }
+        }
+        
+        console.log("❌ Could not find repository root")
+    }
+    
+    // Timer to handle image retry without causing binding loops
+    Timer {
+        id: imageRetryTimer
+        interval: 100  // Small delay to break the binding cycle
+        onTriggered: {
+            if (tryNextPreviewPath()) {
+                console.log("🔄 Retrying with new path after timer...")
+            }
+            imageRetryInProgress = false
+        }
+    }
+    
+    Rectangle {
+        anchors.fill: parent
+        color: "#f5f5f5"
+    }
+    
+    ColumnLayout {
+        anchors.fill: parent
+        spacing: 0
+        
+        // Header (consistent with other pages)
+        Rectangle {
+            Layout.fillWidth: true
+            Layout.preferredHeight: 50
+            color: "white"
+            
+            // Bottom border
+            Rectangle {
+                anchors.bottom: parent.bottom
+                width: parent.width
+                height: 1
+                color: "#e5e7eb"
+            }
+            
+            RowLayout {
+                anchors.fill: parent
+                anchors.leftMargin: 15
+                anchors.rightMargin: 10
+                
+                ConnectionStatus {
+                    backend: page.backend
+                    Layout.rightMargin: 8
+                }
+                
+                Label {
+                    text: "Pattern Execution"
+                    font.pixelSize: 18
+                    font.bold: true
+                    color: "#333"
+                }
+                
+                Item { 
+                    Layout.fillWidth: true 
+                }
+            }
+        }
+        
+        // Content - Side by side layout
+        Item {
+            Layout.fillWidth: true
+            Layout.fillHeight: true
+            
+            Row {
+                anchors.fill: parent
+                spacing: 0
+                
+                // Left side - Pattern Preview (60% of width)
+                Rectangle {
+                    width: parent.width * 0.6
+                    height: parent.height
+                    color: "#ffffff"
+                    
+                    Image {
+                        anchors.fill: parent
+                        anchors.margins: 10
+                        source: {
+                            var finalSource = ""
+                            
+                            // Try different sources in priority order
+                            if (patternPreview) {
+                                finalSource = "file:///" + patternPreview
+                                console.log("🖼️ Using patternPreview:", finalSource)
+                            } else if (activeImageSource) {
+                                // Use the activeImageSource to avoid binding loops
+                                finalSource = activeImageSource
+                                console.log("🖼️ Using activeImageSource:", finalSource)
+                            } else {
+                                console.log("🖼️ No preview source available")
+                            }
+                            
+                            return finalSource
+                        }
+                        fillMode: Image.PreserveAspectFit
+                        
+                        onStatusChanged: {
+                            console.log("📷 Image status:", status, "for source:", source)
+                            if (status === Image.Error) {
+                                console.log("❌ Image failed to load:", source)
+                                // Use timer to avoid binding loop
+                                if (!imageRetryInProgress) {
+                                    imageRetryInProgress = true
+                                    imageRetryTimer.start()
+                                }
+                            } else if (status === Image.Ready) {
+                                console.log("✅ Image loaded successfully:", source)
+                                imageRetryInProgress = false  // Reset on successful load
+                            } else if (status === Image.Loading) {
+                                console.log("🔄 Image loading:", source)
+                            }
+                        }
+                        
+                        onSourceChanged: {
+                            console.log("🔄 Image source changed to:", source)
+                        }
+                        
+                        Rectangle {
+                            anchors.fill: parent
+                            color: "#f0f0f0"
+                            visible: parent.status === Image.Error || parent.source == ""
+                            
+                            Column {
+                                anchors.centerIn: parent
+                                spacing: 10
+                                
+                                Text {
+                                    text: "⚙️"
+                                    font.pixelSize: 48
+                                    color: "#ccc"
+                                    anchors.horizontalCenter: parent.horizontalCenter
+                                }
+                                
+                                Text {
+                                    text: "Pattern Preview"
+                                    color: "#999"
+                                    font.pixelSize: 14
+                                    anchors.horizontalCenter: parent.horizontalCenter
+                                }
+                            }
+                        }
+                    }
+                }
+                
+                // Divider
+                Rectangle {
+                    width: 1
+                    height: parent.height
+                    color: "#e5e7eb"
+                }
+                
+                // Right side - Controls (40% of width)
+                Rectangle {
+                    width: parent.width * 0.4 - 1
+                    height: parent.height
+                    color: "white"
+                    
+                    Column {
+                        anchors.left: parent.left
+                        anchors.right: parent.right
+                        anchors.top: parent.top
+                        anchors.margins: 10
+                        spacing: 15
+                        
+                        // Pattern Name
+                        Rectangle {
+                            width: parent.width
+                            height: 50
+                            radius: 8
+                            color: "#f8f9fa"
+                            border.color: "#e5e7eb"
+                            border.width: 1
+                            
+                            Column {
+                                anchors.centerIn: parent
+                                spacing: 4
+                                
+                                Label {
+                                    text: "Current Pattern"
+                                    font.pixelSize: 10
+                                    color: "#666"
+                                    anchors.horizontalCenter: parent.horizontalCenter
+                                }
+                                
+                                Label {
+                                    text: {
+                                        // Use WebSocket current pattern first, then fallback to passed parameter
+                                        var displayName = ""
+                                        if (backend && backend.currentFile) displayName = backend.currentFile
+                                        else if (patternName) displayName = patternName
+                                        else return "No pattern running"
+                                        
+                                        // Clean up the name for display
+                                        var parts = displayName.split('/')
+                                        displayName = parts[parts.length - 1]
+                                        displayName = displayName.replace('.thr', '')
+                                        return displayName
+                                    }
+                                    font.pixelSize: 12
+                                    font.bold: true
+                                    color: "#333"
+                                    anchors.horizontalCenter: parent.horizontalCenter
+                                    width: parent.parent.width - 20
+                                    elide: Text.ElideMiddle
+                                    horizontalAlignment: Text.AlignHCenter
+                                }
+                            }
+                        }
+                        
+                        // Progress
+                        Rectangle {
+                            width: parent.width
+                            height: 70
+                            radius: 8
+                            color: "#f8f9fa"
+                            border.color: "#e5e7eb"
+                            border.width: 1
+                            
+                            Column {
+                                anchors.fill: parent
+                                anchors.margins: 10
+                                spacing: 8
+                                
+                                Label {
+                                    text: "Progress"
+                                    font.pixelSize: 12
+                                    font.bold: true
+                                    color: "#333"
+                                }
+                                
+                                ProgressBar {
+                                    width: parent.width
+                                    height: 8
+                                    value: backend ? backend.progress / 100 : 0
+                                }
+                                
+                                Label {
+                                    text: backend ? Math.round(backend.progress) + "%" : "0%"
+                                    anchors.horizontalCenter: parent.horizontalCenter
+                                    font.pixelSize: 14
+                                    font.bold: true
+                                    color: "#333"
+                                }
+                            }
+                        }
+                        
+                        // Control Buttons
+                        Rectangle {
+                            width: parent.width
+                            height: 180
+                            radius: 8
+                            color: "#f8f9fa"
+                            border.color: "#e5e7eb"
+                            border.width: 1
+                            
+                            Column {
+                                anchors.fill: parent
+                                anchors.margins: 10
+                                spacing: 10
+                                
+                                Label {
+                                    text: "Controls"
+                                    font.pixelSize: 12
+                                    font.bold: true
+                                    color: "#333"
+                                }
+                                
+                                // Pause/Resume button
+                                Rectangle {
+                                    width: parent.width
+                                    height: 35
+                                    radius: 6
+                                    color: pauseMouseArea.pressed ? "#1e40af" : (backend && backend.currentFile !== "" ? "#2563eb" : "#9ca3af")
+                                    
+                                    Text {
+                                        anchors.centerIn: parent
+                                        text: (backend && backend.isRunning) ? "⏸ Pause" : "▶ Resume"
+                                        color: "white"
+                                        font.pixelSize: 12
+                                        font.bold: true
+                                    }
+                                    
+                                    MouseArea {
+                                        id: pauseMouseArea
+                                        anchors.fill: parent
+                                        enabled: backend && backend.currentFile !== ""
+                                        onClicked: {
+                                            if (backend) {
+                                                if (backend.isRunning) {
+                                                    backend.pauseExecution()
+                                                } else {
+                                                    backend.resumeExecution()
+                                                }
+                                            }
+                                        }
+                                    }
+                                }
+                                
+                                // Stop button
+                                Rectangle {
+                                    width: parent.width
+                                    height: 35
+                                    radius: 6
+                                    color: stopMouseArea.pressed ? "#b91c1c" : (backend && backend.currentFile !== "" ? "#dc2626" : "#9ca3af")
+                                    
+                                    Text {
+                                        anchors.centerIn: parent
+                                        text: "⏹ Stop"
+                                        color: "white"
+                                        font.pixelSize: 12
+                                        font.bold: true
+                                    }
+                                    
+                                    MouseArea {
+                                        id: stopMouseArea
+                                        anchors.fill: parent
+                                        enabled: backend && backend.currentFile !== ""
+                                        onClicked: {
+                                            if (backend) {
+                                                backend.stopExecution()
+                                            }
+                                        }
+                                    }
+                                }
+                                
+                                // Skip button
+                                Rectangle {
+                                    width: parent.width
+                                    height: 35
+                                    radius: 6
+                                    color: skipMouseArea.pressed ? "#525252" : (backend && backend.currentFile !== "" ? "#6b7280" : "#9ca3af")
+                                    
+                                    Text {
+                                        anchors.centerIn: parent
+                                        text: "⏭ Skip"
+                                        color: "white"
+                                        font.pixelSize: 12
+                                        font.bold: true
+                                    }
+                                    
+                                    MouseArea {
+                                        id: skipMouseArea
+                                        anchors.fill: parent
+                                        enabled: backend && backend.currentFile !== ""
+                                        onClicked: {
+                                            if (backend) {
+                                                backend.skipPattern()
+                                            }
+                                        }
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+}

+ 284 - 0
dune-weaver-touch/qml/pages/ModernPatternListPage.qml

@@ -0,0 +1,284 @@
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+import QtQuick.Layouts 1.15
+import "../components"
+
+Page {
+    id: page
+    
+    property var patternModel
+    property var backend
+    property var stackView
+    property bool searchExpanded: false
+    
+    Rectangle {
+        anchors.fill: parent
+        color: "#f5f5f5"
+    }
+    
+    ColumnLayout {
+        anchors.fill: parent
+        spacing: 0
+        
+        // Header with integrated search
+        Rectangle {
+            Layout.fillWidth: true
+            Layout.preferredHeight: 50
+            color: "white"
+            
+            // Bottom border
+            Rectangle {
+                anchors.bottom: parent.bottom
+                width: parent.width
+                height: 1
+                color: "#e5e7eb"
+            }
+            
+            RowLayout {
+                anchors.fill: parent
+                anchors.leftMargin: 15
+                anchors.rightMargin: 10
+                
+                ConnectionStatus {
+                    backend: page.backend
+                    Layout.rightMargin: 8
+                    visible: !searchExpanded
+                }
+                
+                Label {
+                    text: "Browse Patterns"
+                    font.pixelSize: 18
+                    font.bold: true
+                    color: "#333"
+                    visible: !searchExpanded
+                }
+                
+                // Pattern count
+                Label {
+                    text: patternModel.rowCount() + " patterns"
+                    font.pixelSize: 12
+                    color: "#999"
+                    visible: !searchExpanded
+                }
+                
+                Item { 
+                    Layout.fillWidth: true 
+                    visible: !searchExpanded
+                }
+                
+                // Expandable search
+                Rectangle {
+                    Layout.fillWidth: searchExpanded
+                    Layout.preferredWidth: searchExpanded ? parent.width - 60 : 120
+                    Layout.preferredHeight: 32
+                    radius: 16
+                    color: searchExpanded ? "white" : "#f5f5f5"
+                    border.color: searchExpanded ? "#2563eb" : "#e0e0e0"
+                    border.width: 1
+                    
+                    Behavior on Layout.preferredWidth {
+                        NumberAnimation { duration: 200 }
+                    }
+                    
+                    RowLayout {
+                        anchors.fill: parent
+                        anchors.leftMargin: 10
+                        anchors.rightMargin: 10
+                        spacing: 5
+                        
+                        Text {
+                            text: "🔍"
+                            font.pixelSize: 16
+                            font.family: "sans-serif"
+                            color: searchExpanded ? "#2563eb" : "#6b7280"
+                        }
+                        
+                        TextField {
+                            id: searchField
+                            Layout.fillWidth: true
+                            placeholderText: searchExpanded ? "Search patterns... (press Enter)" : "Search"
+                            font.pixelSize: 14
+                            visible: searchExpanded || text.length > 0
+                            
+                            property string lastSearchText: ""
+                            property bool hasUnappliedSearch: text !== lastSearchText && text.length > 0
+                            
+                            background: Rectangle { 
+                                color: "transparent"
+                                border.color: searchField.hasUnappliedSearch ? "#f59e0b" : "transparent"
+                                border.width: searchField.hasUnappliedSearch ? 1 : 0
+                                radius: 4
+                            }
+                            
+                            // Remove automatic filtering on text change
+                            // onTextChanged: patternModel.filter(text)
+                            
+                            // Only filter when user presses Enter or field loses focus
+                            onAccepted: {
+                                patternModel.filter(text)
+                                lastSearchText = text
+                                Qt.inputMethod.hide()
+                                focus = false
+                            }
+                            
+                            // Enable virtual keyboard
+                            activeFocusOnPress: true
+                            selectByMouse: true
+                            inputMethodHints: Qt.ImhNoPredictiveText
+                            
+                            // Direct MouseArea for touch events
+                            MouseArea {
+                                anchors.fill: parent
+                                onPressed: {
+                                    searchField.forceActiveFocus()
+                                    Qt.inputMethod.show()
+                                    mouse.accepted = false // Pass through to TextField
+                                }
+                            }
+                            
+                            onActiveFocusChanged: {
+                                if (activeFocus) {
+                                    searchExpanded = true
+                                    // Force virtual keyboard to show
+                                    Qt.inputMethod.show()
+                                } else {
+                                    // Apply search when focus is lost
+                                    if (text !== lastSearchText) {
+                                        patternModel.filter(text)
+                                        lastSearchText = text
+                                    }
+                                }
+                            }
+                            
+                            // Handle Enter key - triggers onAccepted
+                            Keys.onReturnPressed: {
+                                // onAccepted will be called automatically
+                                // Just hide keyboard and unfocus
+                                Qt.inputMethod.hide()
+                                focus = false
+                            }
+                            
+                            Keys.onEscapePressed: {
+                                // Clear search and hide keyboard
+                                text = ""
+                                lastSearchText = ""
+                                patternModel.filter("")
+                                Qt.inputMethod.hide()
+                                focus = false
+                            }
+                        }
+                        
+                        Text {
+                            text: searchExpanded || searchField.text.length > 0 ? "Search" : ""
+                            font.pixelSize: 12
+                            color: "#999"
+                            visible: !searchExpanded && searchField.text.length === 0
+                        }
+                    }
+                    
+                    MouseArea {
+                        anchors.fill: parent
+                        enabled: !searchExpanded
+                        onClicked: {
+                            searchExpanded = true
+                            searchField.forceActiveFocus()
+                            Qt.inputMethod.show()
+                        }
+                    }
+                }
+                
+                // Close button when expanded
+                Button {
+                    text: "✕"
+                    font.pixelSize: 18
+                    flat: true
+                    visible: searchExpanded
+                    Layout.preferredWidth: 32
+                    Layout.preferredHeight: 32
+                    onClicked: {
+                        searchExpanded = false
+                        searchField.text = ""
+                        searchField.lastSearchText = ""
+                        searchField.focus = false
+                        // Clear the filter when closing search
+                        patternModel.filter("")
+                    }
+                }
+            }
+        }
+        
+        // Content - Pattern Grid
+        GridView {
+            id: gridView
+            Layout.fillWidth: true
+            Layout.fillHeight: true
+            cellWidth: 200
+            cellHeight: 220
+            model: patternModel
+            clip: true
+            
+            // Add smooth scrolling
+            ScrollBar.vertical: ScrollBar {
+                active: true
+                policy: ScrollBar.AsNeeded
+            }
+            
+            delegate: ModernPatternCard {
+                width: gridView.cellWidth - 10
+                height: gridView.cellHeight - 10
+                name: model.name
+                preview: model.preview
+                
+                onClicked: {
+                    if (stackView && backend) {
+                        stackView.push("PatternDetailPage.qml", {
+                            patternName: model.name,
+                            patternPath: model.path,
+                            patternPreview: model.preview,
+                            backend: backend
+                        })
+                    }
+                }
+            }
+            
+            // Add scroll animations
+            add: Transition {
+                NumberAnimation { property: "opacity"; from: 0; to: 1; duration: 300 }
+                NumberAnimation { property: "scale"; from: 0.8; to: 1; duration: 300 }
+            }
+        }
+        
+        // Empty state
+        Item {
+            Layout.fillWidth: true
+            Layout.fillHeight: true
+            visible: patternModel.rowCount() === 0 && searchField.text !== ""
+            
+            Column {
+                anchors.centerIn: parent
+                spacing: 20
+                
+                Text {
+                    text: "🔍"
+                    font.pixelSize: 48
+                    anchors.horizontalCenter: parent.horizontalCenter
+                    color: "#ccc"
+                }
+                
+                Label {
+                    text: "No patterns found"
+                    anchors.horizontalCenter: parent.horizontalCenter
+                    color: "#999"
+                    font.pixelSize: 18
+                }
+                
+                Label {
+                    text: "Try a different search term"
+                    anchors.horizontalCenter: parent.horizontalCenter
+                    color: "#ccc"
+                    font.pixelSize: 14
+                }
+            }
+        }
+    }
+}

+ 678 - 0
dune-weaver-touch/qml/pages/ModernPlaylistPage.qml

@@ -0,0 +1,678 @@
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+import QtQuick.Layouts 1.15
+import DuneWeaver 1.0
+import "../components"
+
+Page {
+    id: page
+    
+    property var backend: null
+    property var stackView: null
+    property var mainWindow: null
+    
+    // State management for navigation
+    property bool showingPlaylistDetail: false
+    property string selectedPlaylist: ""
+    property var selectedPlaylistData: null
+    property var currentPlaylistPatterns: []
+    
+    // Playlist execution settings
+    property real pauseTime: 5.0
+    property string clearPattern: "adaptive"
+    property string runMode: "single"
+    property bool shuffle: false
+    
+    PlaylistModel {
+        id: playlistModel
+    }
+    
+    // Update patterns when playlist selection changes
+    onSelectedPlaylistChanged: {
+        if (selectedPlaylist) {
+            currentPlaylistPatterns = playlistModel.getPatternsForPlaylist(selectedPlaylist)
+            console.log("Loaded patterns for", selectedPlaylist + ":", currentPlaylistPatterns)
+        } else {
+            currentPlaylistPatterns = []
+        }
+    }
+    
+    // Debug playlist loading
+    Component.onCompleted: {
+        console.log("ModernPlaylistPage completed, playlist count:", playlistModel.rowCount())
+        console.log("showingPlaylistDetail:", showingPlaylistDetail)
+    }
+    
+    // Function to navigate to playlist detail
+    function showPlaylistDetail(playlistName, playlistData) {
+        selectedPlaylist = playlistName
+        selectedPlaylistData = playlistData
+        showingPlaylistDetail = true
+    }
+    
+    // Function to go back to playlist list
+    function showPlaylistList() {
+        showingPlaylistDetail = false
+        selectedPlaylist = ""
+        selectedPlaylistData = null
+    }
+    
+    Rectangle {
+        anchors.fill: parent
+        color: "#f5f5f5"
+    }
+    
+    // Playlist List View (shown by default)
+    Rectangle {
+        id: playlistListView
+        anchors.fill: parent
+        color: "#f5f5f5"
+        visible: !showingPlaylistDetail
+        
+        ColumnLayout {
+            anchors.fill: parent
+            spacing: 0
+            
+            // Header
+            Rectangle {
+                Layout.fillWidth: true
+                Layout.preferredHeight: 50
+                color: "white"
+                
+                // Bottom border
+                Rectangle {
+                    anchors.bottom: parent.bottom
+                    width: parent.width
+                    height: 1
+                    color: "#e5e7eb"
+                }
+                
+                RowLayout {
+                    anchors.fill: parent
+                    anchors.leftMargin: 15
+                    anchors.rightMargin: 10
+                    
+                    ConnectionStatus {
+                        backend: page.backend
+                        Layout.rightMargin: 8
+                    }
+                    
+                    Label {
+                        text: "Playlists"
+                        font.pixelSize: 18
+                        font.bold: true
+                        color: "#333"
+                    }
+                    
+                    Label {
+                        text: playlistModel.rowCount() + " playlists"
+                        font.pixelSize: 12
+                        color: "#999"
+                    }
+                    
+                    Item { 
+                        Layout.fillWidth: true 
+                    }
+                }
+            }
+            
+            // Playlist List
+            ListView {
+                Layout.fillWidth: true
+                Layout.fillHeight: true
+                Layout.margins: 15
+                model: playlistModel
+                spacing: 12
+                clip: true
+                
+                ScrollBar.vertical: ScrollBar {
+                    active: true
+                    policy: ScrollBar.AsNeeded
+                }
+                
+                delegate: Rectangle {
+                    width: ListView.view.width
+                    height: 80
+                    color: "white"
+                    radius: 12
+                    border.color: "#e5e7eb"
+                    border.width: 1
+                    
+                    // Press animation
+                    scale: mouseArea.pressed ? 0.98 : 1.0
+                    
+                    Behavior on scale {
+                        NumberAnimation { duration: 100; easing.type: Easing.OutQuad }
+                    }
+                    
+                    RowLayout {
+                        anchors.fill: parent
+                        anchors.margins: 20
+                        spacing: 15
+                        
+                        // Icon
+                        Rectangle {
+                            Layout.preferredWidth: 40
+                            Layout.preferredHeight: 40
+                            radius: 20
+                            color: "#e3f2fd"
+                            
+                            Text {
+                                anchors.centerIn: parent
+                                text: "♪"
+                                font.pixelSize: 18
+                                color: "#2196F3"
+                            }
+                        }
+                        
+                        // Playlist info
+                        Column {
+                            Layout.fillWidth: true
+                            spacing: 4
+                            
+                            Label {
+                                text: model.name
+                                font.pixelSize: 16
+                                font.bold: true
+                                color: "#333"
+                                elide: Text.ElideRight
+                                width: parent.width
+                            }
+                            
+                            Label {
+                                text: model.itemCount + " patterns"
+                                color: "#666"
+                                font.pixelSize: 12
+                            }
+                        }
+                        
+                        // Arrow
+                        Text {
+                            text: "▶"
+                            font.pixelSize: 16
+                            color: "#999"
+                        }
+                    }
+                    
+                    MouseArea {
+                        id: mouseArea
+                        anchors.fill: parent
+                        onClicked: {
+                            showPlaylistDetail(model.name, model)
+                        }
+                    }
+                }
+                
+                // Empty state
+                Rectangle {
+                    anchors.fill: parent
+                    color: "transparent"
+                    visible: playlistModel.rowCount() === 0
+                    
+                    Column {
+                        anchors.centerIn: parent
+                        spacing: 15
+                        
+                        Text {
+                            text: "♪"
+                            color: "#ccc"
+                            font.pixelSize: 64
+                            anchors.horizontalCenter: parent.horizontalCenter
+                        }
+                        
+                        Label {
+                            text: "No playlists found"
+                            anchors.horizontalCenter: parent.horizontalCenter
+                            color: "#999"
+                            font.pixelSize: 18
+                        }
+                        
+                        Label {
+                            text: "Create playlists to organize\\nyour pattern collections"
+                            anchors.horizontalCenter: parent.horizontalCenter
+                            color: "#ccc"
+                            font.pixelSize: 14
+                            horizontalAlignment: Text.AlignHCenter
+                        }
+                    }
+                }
+            }
+        }
+    }
+    
+    // Playlist Detail View (shown when a playlist is selected)
+    Rectangle {
+        id: playlistDetailView
+        anchors.fill: parent
+        color: "#f5f5f5"
+        visible: showingPlaylistDetail
+        
+        ColumnLayout {
+            anchors.fill: parent
+            spacing: 0
+            
+            // Header with back button
+            Rectangle {
+                Layout.fillWidth: true
+                Layout.preferredHeight: 50
+                color: "white"
+                
+                // Bottom border
+                Rectangle {
+                    anchors.bottom: parent.bottom
+                    width: parent.width
+                    height: 1
+                    color: "#e5e7eb"
+                }
+                
+                RowLayout {
+                    anchors.fill: parent
+                    anchors.leftMargin: 15
+                    anchors.rightMargin: 10
+                    spacing: 10
+                    
+                    ConnectionStatus {
+                        backend: page.backend
+                        Layout.rightMargin: 8
+                    }
+                    
+                    Button {
+                        text: "← Back"
+                        font.pixelSize: 14
+                        flat: true
+                        onClicked: showPlaylistList()
+                    }
+                    
+                    Label {
+                        text: selectedPlaylist
+                        font.pixelSize: 18
+                        font.bold: true
+                        color: "#333"
+                        Layout.fillWidth: true
+                        elide: Text.ElideRight
+                    }
+                    
+                    Label {
+                        text: currentPlaylistPatterns.length + " patterns"
+                        font.pixelSize: 12
+                        color: "#999"
+                    }
+                }
+            }
+            
+            // Content - Pattern list on left, controls on right
+            Item {
+                Layout.fillWidth: true
+                Layout.fillHeight: true
+                
+                Row {
+                    anchors.fill: parent
+                    spacing: 0
+                    
+                    // Left side - Pattern List (40% of width)
+                    Rectangle {
+                        width: parent.width * 0.4
+                        height: parent.height
+                        color: "white"
+                        
+                        ColumnLayout {
+                            anchors.fill: parent
+                            anchors.margins: 15
+                            spacing: 10
+                            
+                            Label {
+                                text: "Patterns"
+                                font.pixelSize: 14
+                                font.bold: true
+                                color: "#333"
+                            }
+                            
+                            ScrollView {
+                                Layout.fillWidth: true
+                                Layout.fillHeight: true
+                                clip: true
+                                
+                                ListView {
+                                    id: patternListView
+                                    width: parent.width
+                                    model: currentPlaylistPatterns
+                                    spacing: 6
+                                    
+                                    delegate: Rectangle {
+                                        width: patternListView.width
+                                        height: 35
+                                        color: index % 2 === 0 ? "#f8f9fa" : "#ffffff"
+                                        radius: 6
+                                        border.color: "#e5e7eb"
+                                        border.width: 1
+                                        
+                                        RowLayout {
+                                            anchors.fill: parent
+                                            anchors.margins: 10
+                                            spacing: 8
+                                            
+                                            Label {
+                                                text: (index + 1) + "."
+                                                font.pixelSize: 11
+                                                color: "#666"
+                                                Layout.preferredWidth: 25
+                                            }
+                                            
+                                            Label {
+                                                text: modelData
+                                                font.pixelSize: 11
+                                                color: "#333"
+                                                Layout.fillWidth: true
+                                                elide: Text.ElideRight
+                                            }
+                                        }
+                                    }
+                                }
+                            }
+                            
+                            // Empty playlist message
+                            Rectangle {
+                                Layout.fillWidth: true
+                                Layout.fillHeight: true
+                                color: "transparent"
+                                visible: currentPlaylistPatterns.length === 0
+                                
+                                Column {
+                                    anchors.centerIn: parent
+                                    spacing: 10
+                                    
+                                    Text {
+                                        text: "♪"
+                                        font.pixelSize: 32
+                                        color: "#ccc"
+                                        anchors.horizontalCenter: parent.horizontalCenter
+                                    }
+                                    
+                                    Label {
+                                        text: "Empty playlist"
+                                        anchors.horizontalCenter: parent.horizontalCenter
+                                        color: "#999"
+                                        font.pixelSize: 14
+                                    }
+                                }
+                            }
+                        }
+                    }
+                    
+                    // Divider
+                    Rectangle {
+                        width: 1
+                        height: parent.height
+                        color: "#e5e7eb"
+                    }
+                    
+                    // Right side - Full height controls (60% of width)
+                    Rectangle {
+                        width: parent.width * 0.6 - 1
+                        height: parent.height
+                        color: "white"
+                        
+                        ColumnLayout {
+                            anchors.fill: parent
+                            anchors.margins: 15
+                            spacing: 15
+                            
+                            Label {
+                                text: "Playlist Controls"
+                                font.pixelSize: 16
+                                font.bold: true
+                                color: "#333"
+                            }
+                            
+                            // Main execution buttons
+                            RowLayout {
+                                Layout.fillWidth: true
+                                spacing: 10
+                                
+                                // Play Playlist button
+                                Rectangle {
+                                    Layout.fillWidth: true
+                                    Layout.preferredHeight: 45
+                                    radius: 8
+                                    color: playMouseArea.pressed ? "#1e40af" : "#2563eb"
+                                    
+                                    Text {
+                                        anchors.centerIn: parent
+                                        text: "Play Playlist"
+                                        color: "white"
+                                        font.pixelSize: 14
+                                        font.bold: true
+                                    }
+                                    
+                                    MouseArea {
+                                        id: playMouseArea
+                                        anchors.fill: parent
+                                        onClicked: {
+                                            if (backend) {
+                                                console.log("Playing playlist:", selectedPlaylist, "with settings:", {
+                                                    pauseTime: pauseTime,
+                                                    clearPattern: clearPattern,
+                                                    runMode: runMode,
+                                                    shuffle: shuffle
+                                                })
+                                                backend.executePlaylist(selectedPlaylist, pauseTime, clearPattern, runMode, shuffle)
+                                                
+                                                // Navigate to execution page
+                                                console.log("🎵 Navigating to execution page after playlist start")
+                                                if (mainWindow) {
+                                                    console.log("🎵 Setting shouldNavigateToExecution = true")
+                                                    mainWindow.shouldNavigateToExecution = true
+                                                } else {
+                                                    console.log("🎵 ERROR: mainWindow is null, cannot navigate")
+                                                }
+                                            }
+                                        }
+                                    }
+                                }
+                                
+                                // Shuffle toggle button
+                                Rectangle {
+                                    Layout.preferredWidth: 60
+                                    Layout.preferredHeight: 45
+                                    radius: 8
+                                    color: shuffle ? "#2563eb" : "#6b7280"
+                                    
+                                    Text {
+                                        anchors.centerIn: parent
+                                        text: "🔀"
+                                        color: "white"
+                                        font.pixelSize: 16
+                                    }
+                                    
+                                    MouseArea {
+                                        id: shuffleMouseArea
+                                        anchors.fill: parent
+                                        onClicked: {
+                                            shuffle = !shuffle
+                                            console.log("Shuffle toggled:", shuffle)
+                                        }
+                                    }
+                                }
+                            }
+                            
+                            // Settings section
+                            Rectangle {
+                                Layout.fillWidth: true
+                                Layout.fillHeight: true
+                                radius: 10
+                                color: "#f8f9fa"
+                                border.color: "#e5e7eb"
+                                border.width: 1
+                                
+                                ColumnLayout {
+                                    anchors.fill: parent
+                                    anchors.margins: 15
+                                    spacing: 15
+                                    
+                                    Label {
+                                        text: "Settings"
+                                        font.pixelSize: 14
+                                        font.bold: true
+                                        color: "#333"
+                                    }
+                                    
+                                    // Run mode
+                                    Column {
+                                        Layout.fillWidth: true
+                                        spacing: 8
+                                        
+                                        Label {
+                                            text: "Run Mode:"
+                                            font.pixelSize: 12
+                                            color: "#666"
+                                            font.bold: true
+                                        }
+                                        
+                                        RowLayout {
+                                            width: parent.width
+                                            spacing: 15
+                                            
+                                            RadioButton {
+                                                id: singleModeRadio
+                                                text: "Single"
+                                                font.pixelSize: 11
+                                                checked: runMode === "single"
+                                                onCheckedChanged: {
+                                                    if (checked) runMode = "single"
+                                                }
+                                            }
+                                            
+                                            RadioButton {
+                                                id: loopModeRadio
+                                                text: "Loop"
+                                                font.pixelSize: 11
+                                                checked: runMode === "loop"
+                                                onCheckedChanged: {
+                                                    if (checked) runMode = "loop"
+                                                }
+                                            }
+                                        }
+                                    }
+                                    
+                                    // Pause time
+                                    Column {
+                                        Layout.fillWidth: true
+                                        spacing: 8
+                                        
+                                        Label {
+                                            text: "Pause Between Patterns:"
+                                            font.pixelSize: 12
+                                            color: "#666"
+                                            font.bold: true
+                                        }
+                                        
+                                        RowLayout {
+                                            width: parent.width
+                                            spacing: 10
+                                            
+                                            TextField {
+                                                Layout.preferredWidth: 140
+                                                Layout.preferredHeight: 20
+                                                text: Math.round(pauseTime).toString()
+                                                font.pixelSize: 12
+                                                horizontalAlignment: TextInput.AlignHCenter
+                                                maximumLength: 10
+                                                inputMethodHints: Qt.ImhDigitsOnly
+                                                validator: IntValidator {
+                                                    bottom: 0
+                                                    top: 99999
+                                                }
+                                                onTextChanged: {
+                                                    var newValue = parseInt(text)
+                                                    if (!isNaN(newValue) && newValue >= 0 && newValue <= 99999) {
+                                                        pauseTime = newValue
+                                                    }
+                                                }
+                                                background: Rectangle {
+                                                    color: "white"
+                                                    border.color: "#e5e7eb"
+                                                    border.width: 1
+                                                    radius: 6
+                                                }
+                                            }
+                                            
+                                            Label {
+                                                text: "seconds"
+                                                font.pixelSize: 11
+                                                color: "#666"
+                                            }
+                                            
+                                            Item { 
+                                                Layout.fillWidth: true 
+                                            }
+                                        }
+                                    }
+                                    
+                                    // Clear pattern
+                                    Column {
+                                        Layout.fillWidth: true
+                                        spacing: 8
+                                        
+                                        Label {
+                                            text: "Clear Pattern:"
+                                            font.pixelSize: 12
+                                            color: "#666"
+                                            font.bold: true
+                                        }
+                                        
+                                        GridLayout {
+                                            width: parent.width
+                                            columns: 2
+                                            columnSpacing: 10
+                                            rowSpacing: 5
+                                            
+                                            RadioButton {
+                                                text: "Adaptive"
+                                                font.pixelSize: 11
+                                                checked: clearPattern === "adaptive"
+                                                onCheckedChanged: {
+                                                    if (checked) clearPattern = "adaptive"
+                                                }
+                                            }
+                                            
+                                            RadioButton {
+                                                text: "Clear Center"
+                                                font.pixelSize: 11
+                                                checked: clearPattern === "clear_center"
+                                                onCheckedChanged: {
+                                                    if (checked) clearPattern = "clear_center"
+                                                }
+                                            }
+                                            
+                                            RadioButton {
+                                                text: "Clear Edge"
+                                                font.pixelSize: 11
+                                                checked: clearPattern === "clear_perimeter"
+                                                onCheckedChanged: {
+                                                    if (checked) clearPattern = "clear_perimeter"
+                                                }
+                                            }
+                                            
+                                            RadioButton {
+                                                text: "None"
+                                                font.pixelSize: 11
+                                                checked: clearPattern === "none"
+                                                onCheckedChanged: {
+                                                    if (checked) clearPattern = "none"
+                                                }
+                                            }
+                                        }
+                                    }
+                                    
+                                    Item {
+                                        Layout.fillHeight: true
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+}

+ 256 - 0
dune-weaver-touch/qml/pages/PatternDetailPage.qml

@@ -0,0 +1,256 @@
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+import QtQuick.Layouts 1.15
+import "../components"
+
+Page {
+    id: page
+    property string patternName: ""
+    property string patternPath: ""
+    property string patternPreview: ""
+    property var backend: null
+    
+    Rectangle {
+        anchors.fill: parent
+        color: "#f5f5f5"
+    }
+    
+    ColumnLayout {
+        anchors.fill: parent
+        spacing: 0
+        
+        // Header
+        Rectangle {
+            Layout.fillWidth: true
+            Layout.preferredHeight: 50
+            color: "white"
+            
+            // Bottom border
+            Rectangle {
+                anchors.bottom: parent.bottom
+                width: parent.width
+                height: 1
+                color: "#e5e7eb"
+            }
+            
+            RowLayout {
+                anchors.fill: parent
+                anchors.margins: 10
+                
+                ConnectionStatus {
+                    backend: page.backend
+                    Layout.rightMargin: 8
+                }
+                
+                Button {
+                    text: "← Back"
+                    font.pixelSize: 14
+                    flat: true
+                    onClicked: stackView.pop()
+                }
+                
+                Label {
+                    text: patternName
+                    Layout.fillWidth: true
+                    elide: Label.ElideRight
+                    font.pixelSize: 16
+                    font.bold: true
+                    color: "#333"
+                }
+            }
+        }
+        
+        // Content - Side by side layout
+        Item {
+            Layout.fillWidth: true
+            Layout.fillHeight: true
+            
+            Row {
+                anchors.fill: parent
+                spacing: 0
+                
+                // Left side - Pattern Preview (60% of width)
+                Rectangle {
+                    width: parent.width * 0.6
+                    height: parent.height
+                    color: "#ffffff"
+                
+                Image {
+                    anchors.fill: parent
+                    anchors.margins: 10
+                    source: patternPreview ? "file:///" + patternPreview : ""
+                    fillMode: Image.PreserveAspectFit
+                    
+                    Rectangle {
+                        anchors.fill: parent
+                        color: "#f0f0f0"
+                        visible: parent.status === Image.Error || parent.source == ""
+                        
+                        Column {
+                            anchors.centerIn: parent
+                            spacing: 10
+                            
+                            Text {
+                                text: "○"
+                                font.pixelSize: 48
+                                color: "#ccc"
+                                anchors.horizontalCenter: parent.horizontalCenter
+                            }
+                            
+                            Text {
+                                text: "No Preview Available"
+                                color: "#999"
+                                font.pixelSize: 14
+                                anchors.horizontalCenter: parent.horizontalCenter
+                            }
+                        }
+                    }
+                }
+            }
+                
+                // Divider
+                Rectangle {
+                    width: 1
+                    height: parent.height
+                    color: "#e5e7eb"
+                }
+                
+                // Right side - Controls (40% of width)
+                Rectangle {
+                    width: parent.width * 0.4 - 1
+                    height: parent.height
+                    color: "white"
+                
+                Column {
+                    anchors.fill: parent
+                    anchors.margins: 10
+                    spacing: 15
+                    
+                    // Play Button - FIRST AND PROMINENT
+                    Rectangle {
+                        width: parent.width
+                        height: 50
+                        radius: 8
+                        color: playMouseArea.pressed ? "#1e40af" : (backend ? "#2563eb" : "#9ca3af")
+                        
+                        Text {
+                            anchors.centerIn: parent
+                            text: "▶ Play Pattern"
+                            color: "white"
+                            font.pixelSize: 16
+                            font.bold: true
+                        }
+                        
+                        MouseArea {
+                            id: playMouseArea
+                            anchors.fill: parent
+                            enabled: backend !== null
+                            onClicked: {
+                                if (backend) {
+                                    var preExecution = "adaptive"
+                                    if (centerRadio.checked) preExecution = "clear_center"
+                                    else if (perimeterRadio.checked) preExecution = "clear_perimeter"
+                                    else if (noneRadio.checked) preExecution = "none"
+                                    
+                                    backend.executePattern(patternName, preExecution)
+                                }
+                            }
+                        }
+                    }
+                    
+                    // Pre-Execution Options
+                    Rectangle {
+                        width: parent.width
+                        height: 160  // Increased height to fit all options
+                        radius: 8
+                        color: "#f8f9fa"
+                        border.color: "#e5e7eb"
+                        border.width: 1
+                        
+                        Column {
+                            id: preExecColumn
+                            anchors.left: parent.left
+                            anchors.right: parent.right
+                            anchors.top: parent.top
+                            anchors.margins: 8  // Reduced margins to save space
+                            spacing: 6  // Reduced spacing
+                            
+                            Label {
+                                text: "Pre-Execution"
+                                font.pixelSize: 12
+                                font.bold: true
+                                color: "#333"
+                            }
+                            
+                            RadioButton {
+                                id: adaptiveRadio
+                                text: "Adaptive"
+                                checked: true
+                                font.pixelSize: 10
+                            }
+                            
+                            RadioButton {
+                                id: centerRadio
+                                text: "Clear Center"
+                                font.pixelSize: 10
+                            }
+                            
+                            RadioButton {
+                                id: perimeterRadio
+                                text: "Clear Edge"
+                                font.pixelSize: 10
+                            }
+                            
+                            RadioButton {
+                                id: noneRadio
+                                text: "None"
+                                font.pixelSize: 10
+                            }
+                        }
+                    }
+                    
+                    // Pattern Info
+                    Rectangle {
+                        width: parent.width
+                        height: 80
+                        radius: 8
+                        color: "#f8f9fa"
+                        border.color: "#e5e7eb"
+                        border.width: 1
+                        
+                        Column {
+                            id: infoColumn
+                            anchors.left: parent.left
+                            anchors.right: parent.right
+                            anchors.top: parent.top
+                            anchors.margins: 10
+                            spacing: 6
+                            
+                            Label {
+                                text: "Pattern Info"
+                                font.pixelSize: 14
+                                font.bold: true
+                                color: "#333"
+                            }
+                            
+                            Label {
+                                text: "Name: " + patternName
+                                font.pixelSize: 11
+                                color: "#666"
+                                elide: Text.ElideRight
+                                width: parent.width
+                            }
+                            
+                            Label {
+                                text: "Type: Sand Pattern"
+                                font.pixelSize: 11
+                                color: "#666"
+                            }
+                        }
+                    }
+                }
+            }
+            }
+        }
+    }
+}

+ 68 - 0
dune-weaver-touch/qml/pages/PatternListPage.qml

@@ -0,0 +1,68 @@
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+import QtQuick.Layouts 1.15
+import "../components"
+
+Page {
+    id: page
+    
+    header: ToolBar {
+        RowLayout {
+            anchors.fill: parent
+            anchors.margins: 10
+            
+            TextField {
+                id: searchField
+                Layout.fillWidth: true
+                placeholderText: "Search patterns..."
+                onTextChanged: patternModel.filter(text)
+                font.pixelSize: 16
+            }
+            
+            Button {
+                text: "Playlists"
+                Layout.preferredHeight: 50
+                font.pixelSize: 16
+                onClicked: stackView.push("PlaylistPage.qml")
+            }
+        }
+    }
+    
+    GridView {
+        id: gridView
+        anchors.fill: parent
+        anchors.margins: 10
+        cellWidth: 200
+        cellHeight: 250
+        model: patternModel
+        
+        delegate: PatternCard {
+            width: gridView.cellWidth - 10
+            height: gridView.cellHeight - 10
+            name: model.name
+            preview: model.preview
+            
+            onClicked: {
+                stackView.push("PatternDetailPage.qml", {
+                    patternName: model.name,
+                    patternPath: model.path,
+                    patternPreview: model.preview
+                })
+            }
+        }
+    }
+    
+    BusyIndicator {
+        anchors.centerIn: parent
+        running: patternModel.rowCount() === 0
+        visible: running
+    }
+    
+    Label {
+        anchors.centerIn: parent
+        text: "No patterns found"
+        visible: patternModel.rowCount() === 0 && searchField.text !== ""
+        color: "#999"
+        font.pixelSize: 18
+    }
+}

+ 93 - 0
dune-weaver-touch/qml/pages/PlaylistPage.qml

@@ -0,0 +1,93 @@
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+import QtQuick.Layouts 1.15
+import DuneWeaver 1.0
+
+Page {
+    header: ToolBar {
+        RowLayout {
+            anchors.fill: parent
+            anchors.margins: 10
+            
+            Button {
+                text: "← Back"
+                font.pixelSize: 14
+                flat: true
+                onClicked: stackView.pop()
+            }
+            
+            Label {
+                text: "Playlists"
+                Layout.fillWidth: true
+                font.pixelSize: 20
+                font.bold: true
+            }
+        }
+    }
+    
+    PlaylistModel {
+        id: playlistModel
+    }
+    
+    ListView {
+        anchors.fill: parent
+        anchors.margins: 20
+        model: playlistModel
+        spacing: 10
+        
+        delegate: Rectangle {
+            width: parent.width
+            height: 80
+            color: mouseArea.pressed ? "#e0e0e0" : "#f5f5f5"
+            radius: 8
+            border.color: "#d0d0d0"
+            
+            RowLayout {
+                anchors.fill: parent
+                anchors.margins: 15
+                spacing: 15
+                
+                Column {
+                    Layout.fillWidth: true
+                    spacing: 5
+                    
+                    Label {
+                        text: model.name
+                        font.pixelSize: 16
+                        font.bold: true
+                    }
+                    
+                    Label {
+                        text: model.itemCount + " patterns"
+                        color: "#666"
+                        font.pixelSize: 14
+                    }
+                }
+                
+                Button {
+                    text: "Play"
+                    Layout.preferredWidth: 80
+                    Layout.preferredHeight: 40
+                    font.pixelSize: 14
+                    enabled: false // TODO: Implement playlist execution
+                }
+            }
+            
+            MouseArea {
+                id: mouseArea
+                anchors.fill: parent
+                onClicked: {
+                    // TODO: Navigate to playlist detail page
+                }
+            }
+        }
+    }
+    
+    Label {
+        anchors.centerIn: parent
+        text: "No playlists found"
+        visible: playlistModel.rowCount() === 0
+        color: "#999"
+        font.pixelSize: 18
+    }
+}

+ 543 - 0
dune-weaver-touch/qml/pages/TableControlPage.qml

@@ -0,0 +1,543 @@
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+import QtQuick.Layouts 1.15
+import QtQuick.Effects
+import "../components"
+
+Page {
+    id: page
+    property var backend: null
+    property var serialPorts: []
+    property string selectedPort: ""
+    property bool isSerialConnected: false
+    property int currentSpeed: 130
+    property bool autoPlayOnBoot: false
+    property int screenTimeoutMinutes: 0
+    
+    // Backend signal connections
+    Connections {
+        target: backend
+        
+        function onSerialPortsUpdated(ports) {
+            console.log("Serial ports updated:", ports)
+            serialPorts = ports
+        }
+        
+        function onSerialConnectionChanged(connected) {
+            console.log("Serial connection changed:", connected)
+            isSerialConnected = connected
+        }
+        
+        function onCurrentPortChanged(port) {
+            console.log("Current port changed:", port)
+            if (port) {
+                selectedPort = port
+            }
+        }
+        
+        function onSpeedChanged(speed) {
+            console.log("Speed changed:", speed)
+            currentSpeed = speed
+        }
+        
+        function onSettingsLoaded() {
+            console.log("Settings loaded")
+            if (backend) {
+                autoPlayOnBoot = backend.autoPlayOnBoot
+                currentSpeed = backend.currentSpeed
+                isSerialConnected = backend.serialConnected
+                screenTimeoutMinutes = Math.round(backend.screenTimeout / 60)
+                if (backend.currentPort) {
+                    selectedPort = backend.currentPort
+                }
+            }
+        }
+    }
+    
+    // Refresh serial ports on page load
+    Component.onCompleted: {
+        refreshSerialPorts()
+        loadSettings()
+    }
+    
+    function refreshSerialPorts() {
+        if (backend) {
+            backend.refreshSerialPorts()
+        }
+    }
+    
+    function loadSettings() {
+        if (backend) {
+            backend.loadControlSettings()
+        }
+    }
+    
+    Rectangle {
+        anchors.fill: parent
+        color: "#f5f5f5"
+    }
+    
+    ColumnLayout {
+        anchors.fill: parent
+        spacing: 0
+        
+        // Header
+        Rectangle {
+            Layout.fillWidth: true
+            Layout.preferredHeight: 50
+            color: "white"
+            
+            Rectangle {
+                anchors.bottom: parent.bottom
+                width: parent.width
+                height: 1
+                color: "#e5e7eb"
+            }
+            
+            RowLayout {
+                anchors.fill: parent
+                anchors.leftMargin: 15
+                anchors.rightMargin: 10
+                
+                ConnectionStatus {
+                    backend: page.backend
+                    Layout.rightMargin: 8
+                }
+                
+                Label {
+                    text: "Table Control"
+                    font.pixelSize: 18
+                    font.bold: true
+                    color: "#333"
+                }
+                
+                Item { 
+                    Layout.fillWidth: true 
+                }
+            }
+        }
+        
+        // Main Content
+        ScrollView {
+            Layout.fillWidth: true
+            Layout.fillHeight: true
+            contentWidth: availableWidth
+            
+            ColumnLayout {
+                width: parent.width
+                anchors.margins: 10
+                spacing: 10
+                
+                // Serial Connection Section
+                Rectangle {
+                    Layout.fillWidth: true
+                    Layout.preferredHeight: 160
+                    Layout.margins: 10
+                    radius: 8
+                    color: "white"
+                    
+                    ColumnLayout {
+                        anchors.fill: parent
+                        anchors.margins: 15
+                        spacing: 10
+                        
+                        Label {
+                            text: "Serial Connection"
+                            font.pixelSize: 16
+                            font.bold: true
+                            color: "#333"
+                        }
+                        
+                        RowLayout {
+                            Layout.fillWidth: true
+                            spacing: 10
+                            
+                            Rectangle {
+                                Layout.fillWidth: true
+                                Layout.preferredHeight: 40
+                                radius: 6
+                                color: isSerialConnected ? "#e8f5e8" : "#f8f9fa"
+                                border.color: isSerialConnected ? "#4CAF50" : "#e5e7eb"
+                                border.width: 1
+                                
+                                RowLayout {
+                                    anchors.fill: parent
+                                    anchors.margins: 8
+                                    
+                                    Label {
+                                        text: isSerialConnected ? 
+                                              (selectedPort ? `Connected: ${selectedPort}` : "Connected") :
+                                              (selectedPort || "No port selected")
+                                        color: isSerialConnected ? "#2e7d32" : (selectedPort ? "#333" : "#999")
+                                        font.pixelSize: 12
+                                        font.bold: isSerialConnected
+                                        Layout.fillWidth: true
+                                    }
+                                    
+                                    Text {
+                                        text: "▼"
+                                        color: "#666"
+                                        font.pixelSize: 10
+                                        visible: !isSerialConnected
+                                    }
+                                }
+                                
+                                MouseArea {
+                                    anchors.fill: parent
+                                    enabled: !isSerialConnected
+                                    onClicked: portMenu.open()
+                                }
+                                
+                                Menu {
+                                    id: portMenu
+                                    y: parent.height
+                                    
+                                    Repeater {
+                                        model: serialPorts
+                                        MenuItem {
+                                            text: modelData
+                                            onTriggered: {
+                                                selectedPort = modelData
+                                            }
+                                        }
+                                    }
+                                    
+                                    MenuSeparator {}
+                                    
+                                    MenuItem {
+                                        text: "Refresh Ports"
+                                        onTriggered: refreshSerialPorts()
+                                    }
+                                }
+                            }
+                            
+                            ModernControlButton {
+                                Layout.preferredWidth: 150
+                                Layout.preferredHeight: 40
+                                text: isSerialConnected ? "Disconnect" : "Connect"
+                                icon: isSerialConnected ? "🔌" : "🔗"
+                                buttonColor: isSerialConnected ? "#dc2626" : "#059669"
+                                fontSize: 11
+                                enabled: isSerialConnected || selectedPort !== ""
+                                
+                                onClicked: {
+                                    if (backend) {
+                                        if (isSerialConnected) {
+                                            backend.disconnectSerial()
+                                        } else {
+                                            backend.connectSerial(selectedPort)
+                                        }
+                                    }
+                                }
+                            }
+                        }
+                        
+                        RowLayout {
+                            Layout.fillWidth: true
+                            spacing: 8
+                            visible: !isSerialConnected
+                            
+                            ModernControlButton {
+                                Layout.fillWidth: true
+                                Layout.preferredHeight: 35
+                                text: "Refresh Ports"
+                                icon: "🔄"
+                                buttonColor: "#6b7280"
+                                fontSize: 10
+                                
+                                onClicked: refreshSerialPorts()
+                            }
+                        }
+                    }
+                }
+                
+                // Hardware Movement Section
+                Rectangle {
+                    Layout.fillWidth: true
+                    Layout.preferredHeight: 180
+                    Layout.margins: 10
+                    radius: 8
+                    color: "white"
+                    
+                    ColumnLayout {
+                        anchors.fill: parent
+                        anchors.margins: 15
+                        spacing: 10
+                        
+                        Label {
+                            text: "Table Movement"
+                            font.pixelSize: 16
+                            font.bold: true
+                            color: "#333"
+                        }
+                        
+                        GridLayout {
+                            Layout.fillWidth: true
+                            columns: 3
+                            rowSpacing: 8
+                            columnSpacing: 8
+                            
+                            ModernControlButton {
+                                Layout.fillWidth: true
+                                Layout.preferredHeight: 45
+                                text: "Home"
+                                icon: "🏠"
+                                buttonColor: "#2563eb"
+                                fontSize: 12
+                                enabled: isSerialConnected
+                                
+                                onClicked: {
+                                    if (backend) backend.sendHome()
+                                }
+                            }
+                            
+                            ModernControlButton {
+                                Layout.fillWidth: true
+                                Layout.preferredHeight: 45
+                                text: "Center"
+                                icon: "🎯"
+                                buttonColor: "#2563eb"
+                                fontSize: 12
+                                enabled: isSerialConnected
+                                
+                                onClicked: {
+                                    if (backend) backend.moveToCenter()
+                                }
+                            }
+                            
+                            ModernControlButton {
+                                Layout.fillWidth: true
+                                Layout.preferredHeight: 45
+                                text: "Perimeter"
+                                icon: "⭕"
+                                buttonColor: "#2563eb"
+                                fontSize: 12
+                                enabled: isSerialConnected
+                                
+                                onClicked: {
+                                    if (backend) backend.moveToPerimeter()
+                                }
+                            }
+                        }
+                    }
+                }
+                
+                // Speed Control Section
+                Rectangle {
+                    Layout.fillWidth: true
+                    Layout.preferredHeight: 120
+                    Layout.margins: 10
+                    radius: 8
+                    color: "white"
+                    
+                    ColumnLayout {
+                        anchors.fill: parent
+                        anchors.margins: 15
+                        spacing: 10
+                        
+                        Label {
+                            text: "Speed Control"
+                            font.pixelSize: 16
+                            font.bold: true
+                            color: "#333"
+                        }
+                        
+                        RowLayout {
+                            Layout.fillWidth: true
+                            spacing: 10
+                            
+                            Label {
+                                text: "Speed:"
+                                font.pixelSize: 12
+                                color: "#666"
+                            }
+                            
+                            Slider {
+                                id: speedSlider
+                                Layout.fillWidth: true
+                                from: 10
+                                to: 500
+                                value: currentSpeed
+                                stepSize: 10
+                                
+                                onValueChanged: {
+                                    currentSpeed = Math.round(value)
+                                }
+                                
+                                onPressedChanged: {
+                                    if (!pressed && backend) {
+                                        backend.setSpeed(currentSpeed)
+                                    }
+                                }
+                            }
+                            
+                            Label {
+                                text: currentSpeed
+                                font.pixelSize: 12
+                                font.bold: true
+                                color: "#333"
+                                Layout.preferredWidth: 40
+                            }
+                        }
+                    }
+                }
+                
+                // Auto Play on Boot Section
+                Rectangle {
+                    Layout.fillWidth: true
+                    Layout.preferredHeight: 160
+                    Layout.margins: 10
+                    radius: 8
+                    color: "white"
+                    
+                    ColumnLayout {
+                        anchors.fill: parent
+                        anchors.margins: 15
+                        spacing: 10
+                        
+                        Label {
+                            text: "Auto Play Settings"
+                            font.pixelSize: 16
+                            font.bold: true
+                            color: "#333"
+                        }
+                        
+                        RowLayout {
+                            Layout.fillWidth: true
+                            spacing: 10
+                            
+                            Label {
+                                text: "Auto play on boot:"
+                                font.pixelSize: 12
+                                color: "#666"
+                                Layout.fillWidth: true
+                            }
+                            
+                            Switch {
+                                id: autoPlaySwitch
+                                checked: autoPlayOnBoot
+                                
+                                onToggled: {
+                                    autoPlayOnBoot = checked
+                                    if (backend) {
+                                        backend.setAutoPlayOnBoot(checked)
+                                    }
+                                }
+                            }
+                        }
+                        
+                        RowLayout {
+                            Layout.fillWidth: true
+                            spacing: 10
+                            
+                            Label {
+                                text: "Screen timeout:"
+                                font.pixelSize: 12
+                                color: "#666"
+                            }
+                            
+                            SpinBox {
+                                id: timeoutSpinBox
+                                Layout.preferredWidth: 120
+                                from: 0
+                                to: 120
+                                value: screenTimeoutMinutes
+                                stepSize: 5
+                                
+                                textFromValue: function(value, locale) {
+                                    return value === 0 ? "Never" : value + " min"
+                                }
+                                
+                                onValueModified: {
+                                    screenTimeoutMinutes = value
+                                    if (backend) {
+                                        // Convert minutes to seconds for backend
+                                        backend.screenTimeout = value * 60
+                                    }
+                                }
+                            }
+                            
+                            Item { Layout.fillWidth: true }
+                        }
+                    }
+                }
+                
+                // Debug Screen Control Section (remove this later)
+                Rectangle {
+                    Layout.fillWidth: true
+                    Layout.preferredHeight: 120
+                    Layout.margins: 10
+                    radius: 8
+                    color: "white"
+                    border.color: "#ff0000"
+                    border.width: 2
+                    
+                    ColumnLayout {
+                        anchors.fill: parent
+                        anchors.margins: 15
+                        spacing: 10
+                        
+                        Label {
+                            text: "DEBUG: Screen Control (Remove Later)"
+                            font.pixelSize: 12
+                            font.bold: true
+                            color: "#ff0000"
+                        }
+                        
+                        RowLayout {
+                            Layout.fillWidth: true
+                            spacing: 10
+                            
+                            ModernControlButton {
+                                Layout.fillWidth: true
+                                Layout.preferredHeight: 35
+                                text: "Screen OFF"
+                                icon: "💤"
+                                buttonColor: "#dc2626"
+                                fontSize: 10
+                                
+                                onClicked: {
+                                    console.log("DEBUG: Manual screen off clicked")
+                                    backend.turnScreenOff()
+                                }
+                            }
+                            
+                            ModernControlButton {
+                                Layout.fillWidth: true
+                                Layout.preferredHeight: 35
+                                text: "Screen ON"
+                                icon: "💡"
+                                buttonColor: "#059669"
+                                fontSize: 10
+                                
+                                onClicked: {
+                                    console.log("DEBUG: Manual screen on clicked")
+                                    backend.turnScreenOn()
+                                }
+                            }
+                            
+                            ModernControlButton {
+                                Layout.fillWidth: true
+                                Layout.preferredHeight: 35
+                                text: "Reset Timer"
+                                icon: "⏰"
+                                buttonColor: "#2563eb"
+                                fontSize: 10
+                                
+                                onClicked: {
+                                    console.log("DEBUG: Reset activity timer clicked")
+                                    backend.resetActivityTimer()
+                                }
+                            }
+                        }
+                    }
+                }
+                
+                // Add some bottom spacing for better scrolling
+                Item {
+                    Layout.preferredHeight: 20
+                }
+            }
+        }
+    }
+}

+ 3 - 0
dune-weaver-touch/requirements.txt

@@ -0,0 +1,3 @@
+PySide6>=6.5.0
+qasync>=0.27.0
+aiohttp>=3.9.0

+ 53 - 0
dune-weaver-touch/run.sh

@@ -0,0 +1,53 @@
+#!/bin/bash
+# Development runner - uses the virtual environment
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+
+# Check if virtual environment exists
+if [ ! -d "$SCRIPT_DIR/venv" ]; then
+    echo "❌ Virtual environment not found!"
+    echo "   Run: sudo ./install.sh"
+    echo "   Or manually create: python3 -m venv venv && venv/bin/pip install -r requirements.txt"
+    exit 1
+fi
+
+# Check if backend is running at localhost:8080
+echo "🔍 Checking backend availability at localhost:8080..."
+BACKEND_URL="http://localhost:8080"
+MAX_ATTEMPTS=30
+ATTEMPT=0
+
+while [ $ATTEMPT -lt $MAX_ATTEMPTS ]; do
+    if curl -s --connect-timeout 2 "$BACKEND_URL/serial_status" > /dev/null 2>&1; then
+        echo "✅ Backend is available at localhost:8080"
+        break
+    else
+        ATTEMPT=$((ATTEMPT + 1))
+        if [ $ATTEMPT -eq 1 ]; then
+            echo "⏳ Waiting for backend to become available..."
+            echo "   Make sure the main Dune Weaver application is running"
+            echo "   Attempting connection ($ATTEMPT/$MAX_ATTEMPTS)..."
+        elif [ $((ATTEMPT % 5)) -eq 0 ]; then
+            echo "   Still waiting... ($ATTEMPT/$MAX_ATTEMPTS)"
+        fi
+        sleep 1
+    fi
+done
+
+if [ $ATTEMPT -eq $MAX_ATTEMPTS ]; then
+    echo "❌ Backend not available after $MAX_ATTEMPTS attempts"
+    echo "   Please ensure the main Dune Weaver application is running at localhost:8080"
+    echo "   You can start it with: cd .. && python main.py"
+    exit 1
+fi
+
+# Run the application using the virtual environment
+echo ""
+echo "🚀 Starting Dune Weaver Touch (development mode)"
+echo "   Using virtual environment: $SCRIPT_DIR/venv"
+echo "   Connected to backend: $BACKEND_URL"
+echo "   Press Ctrl+C to stop"
+echo ""
+
+cd "$SCRIPT_DIR"
+exec ./venv/bin/python main.py

+ 59 - 0
dune-weaver-touch/scripts/install-scripts.sh

@@ -0,0 +1,59 @@
+#!/bin/bash
+# Install Dune Weaver Touch scripts to system locations
+
+set -e
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+
+# Check if running as root
+if [ "$EUID" -ne 0 ]; then
+    echo "❌ This script must be run as root (use sudo)"
+    echo "   These scripts need to be installed in /usr/local/bin/ with proper permissions"
+    exit 1
+fi
+
+echo "🔧 Installing Dune Weaver Touch system scripts..."
+echo ""
+
+# Function to install a script
+install_script() {
+    local script_name="$1"
+    local source_path="$SCRIPT_DIR/$script_name"
+    local target_path="/usr/local/bin/$script_name"
+    
+    if [ ! -f "$source_path" ]; then
+        echo "❌ Source script not found: $source_path"
+        return 1
+    fi
+    
+    echo "📄 Installing $script_name..."
+    
+    # Copy script
+    cp "$source_path" "$target_path"
+    
+    # Set proper permissions (executable by root, readable by all)
+    chmod 755 "$target_path"
+    chown root:root "$target_path"
+    
+    echo "   ✅ Installed: $target_path"
+}
+
+# Install all scripts
+install_script "screen-on"
+install_script "screen-off" 
+install_script "touch-monitor"
+
+echo ""
+echo "🎯 All scripts installed successfully!"
+echo ""
+echo "Installed scripts:"
+echo "   /usr/local/bin/screen-on     - Turn display on"
+echo "   /usr/local/bin/screen-off    - Turn display off"  
+echo "   /usr/local/bin/touch-monitor - Monitor touch input"
+echo ""
+echo "Test the scripts:"
+echo "   sudo /usr/local/bin/screen-off"
+echo "   sudo /usr/local/bin/screen-on"
+echo ""
+echo "⚠️  Note: The touch-monitor script is disabled by default in the app"
+echo "   due to sensitivity issues. Direct touch monitoring is used instead."

+ 6 - 0
dune-weaver-touch/scripts/screen-off

@@ -0,0 +1,6 @@
+# /usr/local/bin/screen-off
+#!/usr/bin/env bash
+set -e
+BL="/sys/class/backlight/$(ls /sys/class/backlight | head -n1)"
+if [ -e "$BL/bl_power" ]; then echo 4 | sudo tee "$BL/bl_power"; else echo 0 | sudo tee "$BL/brightness"; fi
+echo 1 | sudo tee /sys/class/graphics/fb0/blank >/dev/null

+ 12 - 0
dune-weaver-touch/scripts/screen-on

@@ -0,0 +1,12 @@
+#!/usr/bin/env bash
+set -e
+BL="/sys/class/backlight/$(ls /sys/class/backlight | head -n1)"
+# Unblank framebuffer first
+echo 0 | sudo tee /sys/class/graphics/fb0/blank >/dev/null
+# Then restore backlight
+if [ -e "$BL/bl_power" ]; then
+    echo 0 | sudo tee "$BL/bl_power"
+else
+    MAX=$(cat "$BL/max_brightness")
+    echo "$MAX" | sudo tee "$BL/brightness"
+fi

+ 37 - 0
dune-weaver-touch/scripts/touch-monitor

@@ -0,0 +1,37 @@
+# /usr/local/bin/screen-off
+#!/usr/bin/env bash
+set -e
+BL="/sys/class/backlight/$(ls /sys/class/backlight | head -n1)"
+if [ -e "$BL/bl_power" ]; then echo 4 | sudo tee "$BL/bl_power"; else echo 0 | sudo tee "$BL/brightness"; fi
+echo 1 | sudo tee /sys/class/graphics/fb0/blank >/dev/null
+tuannguyen@dune-weaver-test:~ $ cat /usr/local/bin/touch-monitor
+#!/usr/bin/env bash
+# Monitor touch events and wake screen when detected
+
+# Find touch input device
+TOUCH_DEV=""
+for dev in /dev/input/event*; do
+    if udevadm info --query=all --name=$dev 2>/dev/null | grep -q -i "touch\|ft5406"; then
+        TOUCH_DEV=$dev
+        break
+    fi
+done
+
+if [ -z "$TOUCH_DEV" ]; then
+    # Fallback to first event device
+    TOUCH_DEV="/dev/input/event0"
+fi
+
+echo "Monitoring touch device: $TOUCH_DEV"
+
+# Monitor for any input
+while true; do
+    # Use timeout to read one event (16 bytes)
+    if timeout 0.1 cat "$TOUCH_DEV" 2>/dev/null | head -c 16 > /dev/null; then
+        echo "Touch detected - waking screen"
+        /usr/local/bin/screen-on
+        # Exit so the main app can take over
+        exit 0
+    fi
+    sleep 0.1
+done

+ 58 - 0
dune-weaver-touch/setup-autologin.sh

@@ -0,0 +1,58 @@
+#!/bin/bash
+# Quick auto-login setup for Dune Weaver Touch
+
+set -e
+
+if [ "$EUID" -ne 0 ]; then
+    echo "❌ This script must be run as root (use sudo)"
+    exit 1
+fi
+
+ACTUAL_USER="${SUDO_USER:-$USER}"
+
+echo "🔑 Dune Weaver Touch - Auto-Login Setup"
+echo "======================================="
+echo "User: $ACTUAL_USER"
+echo ""
+
+# Check if raspi-config is available
+if command -v raspi-config >/dev/null 2>&1; then
+    echo "🔧 Setting up auto-login using raspi-config..."
+    
+    # Use raspi-config to enable auto-login to desktop
+    raspi-config nonint do_boot_behaviour B4
+    
+    if [ $? -eq 0 ]; then
+        echo "✅ Auto-login configured successfully!"
+        echo ""
+        echo "📝 What was configured:"
+        echo "   - Boot to desktop with auto-login enabled"
+        echo "   - User: $ACTUAL_USER"
+        echo "   - The Dune Weaver service will start automatically"
+        echo ""
+        echo "🚀 Reboot to see the changes:"
+        echo "   sudo reboot"
+    else
+        echo "❌ raspi-config failed. Try manual configuration:"
+        echo "   sudo raspi-config"
+        echo "   → System Options → Boot/Auto Login → Desktop Autologin"
+    fi
+else
+    echo "⚠️  raspi-config not found."
+    echo ""
+    echo "Manual alternatives:"
+    echo "1. If you have a desktop environment with lightdm:"
+    echo "   sudo nano /etc/lightdm/lightdm.conf"
+    echo "   Uncomment and set: autologin-user=$ACTUAL_USER"
+    echo ""
+    echo "2. For console auto-login (minimal systems):"
+    echo "   sudo systemctl edit getty@tty1"
+    echo "   Add: [Service]"
+    echo "        ExecStart="
+    echo "        ExecStart=-/sbin/agetty --autologin $ACTUAL_USER --noclear %I \\$TERM"
+    echo ""
+fi
+
+echo ""
+echo "ℹ️  The Dune Weaver Touch service is already configured to start automatically."
+echo "   After enabling auto-login and rebooting, you'll have a complete kiosk setup!"

+ 148 - 0
dune-weaver-touch/setup-autostart.sh

@@ -0,0 +1,148 @@
+#!/bin/bash
+
+# Setup Dune Weaver Touch to start automatically on boot
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+ACTUAL_USER="${SUDO_USER:-$USER}"
+USER_HOME=$(eval echo ~$ACTUAL_USER)
+
+echo "=== Dune Weaver Touch Auto-Start Setup ==="
+echo "App directory: $SCRIPT_DIR"
+echo "User: $ACTUAL_USER"
+echo ""
+
+# Function to install system scripts
+install_scripts() {
+    echo "Installing system scripts..."
+    
+    if [ "$EUID" -ne 0 ]; then
+        echo "❌ Script installation requires root privileges. Run with sudo."
+        return 1
+    fi
+    
+    "$SCRIPT_DIR/scripts/install-scripts.sh"
+    echo "✅ System scripts installed"
+}
+
+# Function to setup systemd service
+setup_systemd() {
+    echo "Setting up systemd service..."
+    
+    # Check if running as root
+    if [ "$EUID" -ne 0 ]; then
+        echo "❌ Systemd setup requires root privileges. Run with sudo."
+        return 1
+    fi
+    
+    # Install scripts first
+    install_scripts
+    
+    # Update paths in the service file
+    sed "s|/home/pi/dune-weaver-touch|$SCRIPT_DIR|g" "$SCRIPT_DIR/dune-weaver-touch.service" > /tmp/dune-weaver-touch.service
+    sed -i "s|User=pi|User=$ACTUAL_USER|g" /tmp/dune-weaver-touch.service
+    sed -i "s|Group=pi|Group=$ACTUAL_USER|g" /tmp/dune-weaver-touch.service
+    
+    # Copy service file
+    cp /tmp/dune-weaver-touch.service /etc/systemd/system/
+    
+    # Enable service
+    systemctl daemon-reload
+    systemctl enable dune-weaver-touch.service
+    
+    echo "✅ Systemd service installed and enabled"
+    echo "   The app will start automatically on boot"
+    echo ""
+    echo "Service commands:"
+    echo "   sudo systemctl start dune-weaver-touch"
+    echo "   sudo systemctl stop dune-weaver-touch" 
+    echo "   sudo systemctl status dune-weaver-touch"
+    echo "   sudo journalctl -u dune-weaver-touch -f"
+}
+
+# Function to setup desktop autostart
+setup_desktop() {
+    echo "Setting up desktop autostart..."
+    
+    # Create autostart directory if it doesn't exist
+    mkdir -p "$USER_HOME/.config/autostart"
+    
+    # Update paths in desktop file
+    sed "s|/home/pi/dune-weaver-touch|$SCRIPT_DIR|g" "$SCRIPT_DIR/dune-weaver-touch.desktop" > "$USER_HOME/.config/autostart/dune-weaver-touch.desktop"
+    
+    # Make sure the user owns the file
+    chown $ACTUAL_USER:$ACTUAL_USER "$USER_HOME/.config/autostart/dune-weaver-touch.desktop"
+    
+    echo "✅ Desktop autostart configured"
+    echo "   The app will start when the user logs in"
+}
+
+# Function to setup boot splash (optional)
+setup_boot_splash() {
+    echo "Setting up boot splash screen..."
+    
+    if [ "$EUID" -ne 0 ]; then
+        echo "❌ Boot splash setup requires root privileges. Run with sudo."
+        return 1
+    fi
+    
+    # Disable boot messages for cleaner boot
+    if ! grep -q "quiet splash" /boot/cmdline.txt; then
+        sed -i 's/$/ quiet splash/' /boot/cmdline.txt
+        echo "✅ Boot splash enabled"
+    else
+        echo "ℹ️  Boot splash already enabled"
+    fi
+    
+    # Disable rainbow splash
+    if ! grep -q "disable_splash=1" /boot/config.txt; then
+        echo "disable_splash=1" >> /boot/config.txt
+        echo "✅ Rainbow splash disabled"
+    else
+        echo "ℹ️  Rainbow splash already disabled"
+    fi
+}
+
+# Main menu
+echo "Choose setup method:"
+echo "1) Systemd service (recommended for headless/kiosk mode)"
+echo "2) Desktop autostart (for desktop environments)" 
+echo "3) Both systemd + desktop autostart"
+echo "4) Install system scripts only"
+echo "5) Add boot splash optimizations"
+echo "6) Complete kiosk setup (scripts + systemd + boot splash)"
+echo ""
+read -p "Enter your choice (1-6): " choice
+
+case $choice in
+    1)
+        setup_systemd
+        ;;
+    2)
+        setup_desktop
+        ;;
+    3)
+        setup_systemd
+        setup_desktop
+        ;;
+    4)
+        install_scripts
+        ;;
+    5)
+        setup_boot_splash
+        ;;
+    6)
+        install_scripts
+        setup_systemd
+        setup_boot_splash
+        echo ""
+        echo "🎯 Complete kiosk setup done!"
+        echo "   Reboot to see the full kiosk experience"
+        ;;
+    *)
+        echo "❌ Invalid choice"
+        exit 1
+        ;;
+esac
+
+echo ""
+echo "✅ Setup complete!"

+ 441 - 0
dune-weaver-touch/setup_kiosk.sh

@@ -0,0 +1,441 @@
+#!/bin/bash
+
+#############################################
+# Dune Weaver Touch - Kiosk Mode Setup Script
+# For Raspberry Pi with Freenove 5" DSI Display
+# Author: Dune Weaver Team
+# Version: 1.0
+#############################################
+
+set -e  # Exit on error
+
+# Color codes for output
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+NC='\033[0m' # No Color
+
+# Configuration variables
+SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+USER_NAME=$(whoami)
+HOME_DIR="/home/$USER_NAME"
+APP_DIR="$SCRIPT_DIR"
+SERVICE_NAME="dune-weaver-kiosk"
+VENV_PATH="$APP_DIR/bin/activate"
+
+# Display banner
+echo -e "${GREEN}"
+echo "================================================"
+echo "   Dune Weaver Touch - Kiosk Setup"
+echo "   Freenove 5\" DSI Display (800x480)"
+echo "================================================"
+echo -e "${NC}"
+
+# Function to detect Raspberry Pi model
+detect_pi_model() {
+    local model=$(cat /proc/cpuinfo | grep "Model" | cut -d ':' -f 2 | sed 's/^ *//')
+    if [[ $model == *"Pi 3"* ]]; then
+        echo "3"
+    elif [[ $model == *"Pi 4"* ]]; then
+        echo "4"
+    elif [[ $model == *"Pi 5"* ]]; then
+        echo "5"
+    else
+        echo "unknown"
+    fi
+}
+
+# Function to backup files
+backup_file() {
+    local file=$1
+    if [ -f "$file" ]; then
+        cp "$file" "${file}.backup.$(date +%Y%m%d_%H%M%S)"
+        echo -e "${YELLOW}Backed up: $file${NC}"
+    fi
+}
+
+# Function to setup boot configuration
+setup_boot_config() {
+    echo -e "${GREEN}Configuring boot settings for DSI display...${NC}"
+    
+    backup_file "/boot/config.txt"
+    
+    local pi_model=$(detect_pi_model)
+    echo -e "${GREEN}Detected Raspberry Pi Model: $pi_model${NC}"
+    
+    # Remove conflicting HDMI settings if they exist
+    sudo sed -i '/hdmi_force_hotplug/d' /boot/config.txt
+    sudo sed -i '/hdmi_group/d' /boot/config.txt
+    sudo sed -i '/hdmi_mode/d' /boot/config.txt
+    sudo sed -i '/hdmi_cvt/d' /boot/config.txt
+    sudo sed -i '/hdmi_drive/d' /boot/config.txt
+    sudo sed -i '/config_hdmi_boost/d' /boot/config.txt
+    sudo sed -i '/dtoverlay=ads7846/d' /boot/config.txt
+    
+    # Remove old overlays
+    sudo sed -i '/dtoverlay=vc4-fkms-v3d/d' /boot/config.txt
+    sudo sed -i '/dtoverlay=vc4-kms-v3d/d' /boot/config.txt
+    
+    # Add proper configuration based on Pi model
+    if [ "$pi_model" == "3" ]; then
+        # Pi 3 configuration
+        echo -e "${GREEN}Configuring for Pi 3...${NC}"
+        cat << EOF | sudo tee -a /boot/config.txt > /dev/null
+
+# Dune Weaver Kiosk - DSI Display Configuration (Pi 3)
+dtoverlay=vc4-fkms-v3d
+gpu_mem=128
+max_framebuffers=2
+display_auto_detect=1
+disable_overscan=1
+EOF
+    else
+        # Pi 4/5 configuration
+        echo -e "${GREEN}Configuring for Pi 4/5...${NC}"
+        cat << EOF | sudo tee -a /boot/config.txt > /dev/null
+
+# Dune Weaver Kiosk - DSI Display Configuration (Pi 4/5)
+dtoverlay=vc4-kms-v3d
+gpu_mem=128
+max_framebuffers=2
+display_auto_detect=1
+disable_overscan=1
+EOF
+    fi
+    
+    # Add common settings
+    cat << EOF | sudo tee -a /boot/config.txt > /dev/null
+
+# Common settings
+dtparam=i2c_arm=on
+dtparam=spi=on
+enable_uart=1
+max_usb_current=1
+
+# Disable splash screens for faster boot
+disable_splash=1
+EOF
+    
+    echo -e "${GREEN}Boot configuration updated${NC}"
+}
+
+# Function to install dependencies
+install_dependencies() {
+    echo -e "${GREEN}Installing required dependencies...${NC}"
+    
+    sudo apt-get update
+    sudo apt-get install -y \
+        libgles2-mesa \
+        libgles2-mesa-dev \
+        libgbm-dev \
+        libdrm-dev \
+        libinput-dev \
+        libudev-dev \
+        libxkbcommon-dev \
+        fbset \
+        evtest \
+        qtvirtualkeyboard-plugin \
+        qml-module-qtquick-virtualkeyboard \
+        qt6-virtualkeyboard-plugin \
+        qml6-module-qt-labs-qmlmodels || {
+        echo -e "${YELLOW}Some packages may not be available, continuing...${NC}"
+    }
+    
+    echo -e "${GREEN}Dependencies installed${NC}"
+}
+
+# Function to create startup script
+create_startup_script() {
+    echo -e "${GREEN}Creating startup script...${NC}"
+    
+    local pi_model=$(detect_pi_model)
+    local START_SCRIPT="$APP_DIR/start_kiosk.sh"
+    
+    cat << 'EOF' > "$START_SCRIPT"
+#!/bin/bash
+
+# Dune Weaver Touch Kiosk Startup Script
+# Auto-generated by setup_kiosk.sh
+
+# Wait for system to fully boot
+sleep 10
+
+# Log file
+LOG_FILE="/tmp/dune-weaver-kiosk.log"
+exec &> >(tee -a "$LOG_FILE")
+
+echo "========================================="
+echo "Starting Dune Weaver Touch Kiosk"
+echo "Date: $(date)"
+echo "========================================="
+
+# Detect Pi model for platform selection
+PI_MODEL=$(cat /proc/cpuinfo | grep "Model" | cut -d ':' -f 2)
+
+# Set working directory
+cd "$(dirname "$0")"
+
+# Check if virtual environment exists
+if [ ! -f "bin/activate" ]; then
+    echo "ERROR: Virtual environment not found!"
+    exit 1
+fi
+
+# Activate virtual environment
+source bin/activate
+
+# Function to run with LinuxFB (Pi 3 with DSI)
+run_linuxfb() {
+    export QT_QPA_PLATFORM=linuxfb:fb=/dev/fb0
+    export QT_QPA_FONTDIR=/usr/share/fonts/truetype
+    export QT_QPA_FB_HIDECURSOR=1
+    export QT_QPA_EVDEV_TOUCHSCREEN_PARAMETERS=/dev/input/event0:rotate=0
+    
+    # Enable Virtual Keyboard
+    export QT_IM_MODULE=qtvirtualkeyboard
+    export QT_VIRTUALKEYBOARD_STYLE=default
+    export QT_VIRTUALKEYBOARD_LAYOUT_PATH=/usr/share/qt5/qtvirtualkeyboard/layouts
+    
+    echo "Starting with LinuxFB platform and virtual keyboard..."
+    python main.py
+}
+
+# Function to run with EGLFS (Pi 4/5 with DSI)
+run_eglfs() {
+    export QT_QPA_PLATFORM=eglfs
+    export QT_QPA_EGLFS_WIDTH=800
+    export QT_QPA_EGLFS_HEIGHT=480
+    export QT_QPA_EGLFS_INTEGRATION=eglfs_kms
+    export QT_QPA_EGLFS_KMS_ATOMIC=1
+    export QT_QPA_EGLFS_HIDECURSOR=1
+    export QT_QPA_EVDEV_TOUCHSCREEN_PARAMETERS=/dev/input/event0:rotate=0
+    
+    # Enable Virtual Keyboard
+    export QT_IM_MODULE=qtvirtualkeyboard
+    export QT_VIRTUALKEYBOARD_STYLE=default
+    export QT_VIRTUALKEYBOARD_LAYOUT_PATH=/usr/share/qt5/qtvirtualkeyboard/layouts
+    
+    echo "Starting with EGLFS platform and virtual keyboard..."
+    python main.py
+}
+
+# Check for DRM device (needed for EGLFS)
+if [ -e /dev/dri/card0 ] || [ -e /dev/dri/card1 ]; then
+    echo "DRM device found, using EGLFS"
+    run_eglfs
+else
+    echo "No DRM device, using LinuxFB"
+    run_linuxfb
+fi
+
+echo "Application exited at $(date)"
+EOF
+    
+    chmod +x "$START_SCRIPT"
+    echo -e "${GREEN}Startup script created: $START_SCRIPT${NC}"
+}
+
+# Function to create systemd service
+create_systemd_service() {
+    echo -e "${GREEN}Creating systemd service...${NC}"
+    
+    local SERVICE_FILE="/etc/systemd/system/${SERVICE_NAME}.service"
+    
+    cat << EOF | sudo tee "$SERVICE_FILE" > /dev/null
+[Unit]
+Description=Dune Weaver Touch Kiosk Mode
+After=multi-user.target graphical.target
+Wants=network-online.target
+After=network-online.target
+
+[Service]
+Type=simple
+User=$USER_NAME
+Group=$USER_NAME
+WorkingDirectory=$APP_DIR
+
+# Auto-restart on failure
+Restart=always
+RestartSec=10
+
+# Start script
+ExecStart=$APP_DIR/start_kiosk.sh
+
+# Stop timeout
+TimeoutStopSec=10
+
+# Logging
+StandardOutput=journal
+StandardError=journal
+
+[Install]
+WantedBy=multi-user.target
+EOF
+    
+    # Reload systemd and enable service
+    sudo systemctl daemon-reload
+    sudo systemctl enable "${SERVICE_NAME}.service"
+    
+    echo -e "${GREEN}Systemd service created and enabled${NC}"
+}
+
+# Function to create uninstall script
+create_uninstall_script() {
+    echo -e "${GREEN}Creating uninstall script...${NC}"
+    
+    cat << 'EOF' > "$APP_DIR/uninstall_kiosk.sh"
+#!/bin/bash
+
+# Dune Weaver Touch - Kiosk Mode Uninstall Script
+
+set -e
+
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+NC='\033[0m'
+
+SERVICE_NAME="dune-weaver-kiosk"
+
+echo -e "${YELLOW}Uninstalling Dune Weaver Kiosk Mode...${NC}"
+
+# Stop and disable service
+if systemctl list-units --full -all | grep -Fq "${SERVICE_NAME}.service"; then
+    echo "Stopping and disabling service..."
+    sudo systemctl stop "${SERVICE_NAME}.service" 2>/dev/null || true
+    sudo systemctl disable "${SERVICE_NAME}.service" 2>/dev/null || true
+    sudo rm -f "/etc/systemd/system/${SERVICE_NAME}.service"
+    sudo systemctl daemon-reload
+    echo -e "${GREEN}Service removed${NC}"
+fi
+
+# Remove startup script
+if [ -f "start_kiosk.sh" ]; then
+    rm -f "start_kiosk.sh"
+    echo -e "${GREEN}Startup script removed${NC}"
+fi
+
+# Restore boot config if backup exists
+LATEST_BACKUP=$(ls -t /boot/config.txt.backup.* 2>/dev/null | head -n1)
+if [ -n "$LATEST_BACKUP" ]; then
+    echo -e "${YELLOW}Found backup: $LATEST_BACKUP${NC}"
+    read -p "Restore boot configuration from backup? (y/n): " -n 1 -r
+    echo
+    if [[ $REPLY =~ ^[Yy]$ ]]; then
+        sudo cp "$LATEST_BACKUP" /boot/config.txt
+        echo -e "${GREEN}Boot configuration restored${NC}"
+        echo -e "${YELLOW}Reboot required for changes to take effect${NC}"
+    fi
+fi
+
+echo -e "${GREEN}Kiosk mode uninstalled successfully${NC}"
+echo -e "${YELLOW}You may want to reboot your Raspberry Pi${NC}"
+EOF
+    
+    chmod +x "$APP_DIR/uninstall_kiosk.sh"
+    echo -e "${GREEN}Uninstall script created: $APP_DIR/uninstall_kiosk.sh${NC}"
+}
+
+# Function to test the display
+test_display() {
+    echo -e "${GREEN}Testing display configuration...${NC}"
+    
+    # Check framebuffer
+    if [ -e /dev/fb0 ]; then
+        echo -e "${GREEN}✓ Framebuffer device found${NC}"
+        fbset -i | head -5
+    else
+        echo -e "${RED}✗ No framebuffer device found${NC}"
+    fi
+    
+    # Check for DRM device
+    if [ -e /dev/dri/card0 ] || [ -e /dev/dri/card1 ]; then
+        echo -e "${GREEN}✓ DRM device found${NC}"
+        ls -la /dev/dri/
+    else
+        echo -e "${YELLOW}! No DRM device (normal for Pi 3)${NC}"
+    fi
+    
+    # Check touch input
+    echo -e "${GREEN}Available input devices:${NC}"
+    ls -la /dev/input/event* 2>/dev/null || echo "No input devices found"
+}
+
+# Main installation function
+install_kiosk() {
+    echo -e "${GREEN}Starting Kiosk Mode Installation...${NC}"
+    
+    # Check if running on Raspberry Pi
+    if ! grep -q "Raspberry Pi" /proc/cpuinfo; then
+        echo -e "${YELLOW}Warning: This doesn't appear to be a Raspberry Pi${NC}"
+        read -p "Continue anyway? (y/n): " -n 1 -r
+        echo
+        if [[ ! $REPLY =~ ^[Yy]$ ]]; then
+            exit 1
+        fi
+    fi
+    
+    # Check if virtual environment exists
+    if [ ! -f "$VENV_PATH" ]; then
+        echo -e "${RED}Error: Virtual environment not found at $VENV_PATH${NC}"
+        echo "Please run this script from the dune-weaver-touch directory"
+        exit 1
+    fi
+    
+    # Install steps
+    install_dependencies
+    setup_boot_config
+    create_startup_script
+    create_systemd_service
+    create_uninstall_script
+    test_display
+    
+    echo -e "${GREEN}"
+    echo "================================================"
+    echo "   Installation Complete!"
+    echo "================================================"
+    echo -e "${NC}"
+    echo
+    echo "Next steps:"
+    echo "1. Reboot your Raspberry Pi:"
+    echo "   ${GREEN}sudo reboot${NC}"
+    echo
+    echo "2. The kiosk will start automatically after reboot"
+    echo
+    echo "3. To manually control the service:"
+    echo "   Start:   ${GREEN}sudo systemctl start ${SERVICE_NAME}${NC}"
+    echo "   Stop:    ${GREEN}sudo systemctl stop ${SERVICE_NAME}${NC}"
+    echo "   Status:  ${GREEN}sudo systemctl status ${SERVICE_NAME}${NC}"
+    echo "   Logs:    ${GREEN}journalctl -u ${SERVICE_NAME} -f${NC}"
+    echo
+    echo "4. To uninstall:"
+    echo "   ${GREEN}./uninstall_kiosk.sh${NC}"
+    echo
+    
+    read -p "Reboot now? (y/n): " -n 1 -r
+    echo
+    if [[ $REPLY =~ ^[Yy]$ ]]; then
+        sudo reboot
+    fi
+}
+
+# Parse command line arguments
+case "${1:-}" in
+    uninstall)
+        if [ -f "$APP_DIR/uninstall_kiosk.sh" ]; then
+            "$APP_DIR/uninstall_kiosk.sh"
+        else
+            echo -e "${RED}Uninstall script not found${NC}"
+            exit 1
+        fi
+        ;;
+    test)
+        test_display
+        ;;
+    status)
+        sudo systemctl status "${SERVICE_NAME}.service"
+        ;;
+    *)
+        install_kiosk
+        ;;
+esac