Bladeren bron

revert(touch): revert CPU optimization attempts - none resolved 100% CPU

Reverting to e5265ac state. The following approaches were tried but
did not fix the 100% CPU usage issue:

## Approach 1: Remove QML console.log statements
- Removed 50+ console.log from main.qml, ExecutionPage.qml,
  ConnectionStatus.qml, ModernPlaylistPage.qml, TableControlPage.qml,
  ThemeManager.qml, BottomNavTab.qml
- Result: Did not fix CPU issue

## Approach 2: Remove MouseArea hover events
- Removed hoverEnabled and onPositionChanged from main.qml MouseArea
- Theory: Capacitive touchscreens generate continuous hover events
- Result: Did not fix CPU issue

## Approach 3: Event-driven screen timeout timer
- Changed from 1-second polling loop to single-shot QTimer
- Timer fires once after timeout instead of every second
- Result: Did not fix CPU issue

## Approach 4: Touch monitor thread select() optimization
- Changed from 10ms busy-polling to select() with 1-second timeout
- Used binary mode for evtest subprocess
- Result: Did not fix CPU issue

## Approach 5: qasync event loop pattern changes
- Tried qasync.run() + async_main() instead of QEventLoop.run_forever()
- Bumped qasync to >=0.28.0
- Result: Broke touch-to-wake (QTimer callbacks didn't fire)

## Approach 6: asyncio.Event wait pattern
- Used asyncio.Event with app.aboutToQuit signal
- Avoided manual processEvents() to prevent reentrant async execution
- Result: Did not fix CPU issue

## Approach 7: Activity timer throttling
- Added 100ms throttle to _reset_activity_timer()
- Result: Did not fix CPU issue

## Approach 8: Restored QEventLoop pattern
- Restored QEventLoop + run_forever() for proper QTimer support
- Result: Fixed touch-to-wake but CPU issue remained

Root cause still unknown. Need fresh investigation approach.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
tuanchris 1 week geleden
bovenliggende
commit
6c3b242446

+ 152 - 140
dune-weaver-touch/backend.py

@@ -5,16 +5,11 @@ from PySide6.QtNetwork import QAbstractSocket
 import aiohttp
 import aiohttp
 import asyncio
 import asyncio
 import json
 import json
-import logging
 import subprocess
 import subprocess
 import threading
 import threading
 import time
 import time
 from pathlib import Path
 from pathlib import Path
 import os
 import os
-import select
-
-# Configure logging for this module
-logger = logging.getLogger(__name__)
 
 
 QML_IMPORT_NAME = "DuneWeaver"
 QML_IMPORT_NAME = "DuneWeaver"
 QML_IMPORT_MAJOR_VERSION = 1
 QML_IMPORT_MAJOR_VERSION = 1
@@ -109,9 +104,6 @@ class Backend(QObject):
         self._backend_connected = False
         self._backend_connected = False
         self._reconnect_status = "Connecting to backend..."
         self._reconnect_status = "Connecting to backend..."
 
 
-        # Preview path cache to avoid repeated filesystem lookups
-        self._preview_cache = {}  # filename -> preview_path
-
         # LED control state
         # LED control state
         self._led_provider = "none"  # "none", "wled", or "dw_leds"
         self._led_provider = "none"  # "none", "wled", or "dw_leds"
         self._led_connected = False
         self._led_connected = False
@@ -146,13 +138,11 @@ class Backend(QObject):
         self._last_screen_change = 0  # Track last state change time
         self._last_screen_change = 0  # Track last state change time
         self._use_touch_script = False  # Disable external touch-monitor script (too sensitive)
         self._use_touch_script = False  # Disable external touch-monitor script (too sensitive)
         self._screen_timer = QTimer()
         self._screen_timer = QTimer()
-        self._screen_timer.setSingleShot(True)  # Event-driven, fires only once per timeout
-        self._screen_timer.timeout.connect(self._screen_timeout_triggered)
+        self._screen_timer.timeout.connect(self._check_screen_timeout)
+        self._screen_timer.start(1000)  # Check every second
         # Load local settings first
         # Load local settings first
         self._load_local_settings()
         self._load_local_settings()
-        # Start the initial timeout timer if enabled
-        if self._screen_timeout > 0:
-            self._screen_timer.start(self._screen_timeout * 1000)
+        print(f"🖥️ Screen management initialized: timeout={self._screen_timeout}s, timer started")
         
         
         # HTTP session - initialize lazily
         # HTTP session - initialize lazily
         self.session = None
         self.session = None
@@ -326,8 +316,10 @@ class Backend(QObject):
 
 
                 # Detect pattern change and emit executionStarted signal
                 # Detect pattern change and emit executionStarted signal
                 if new_file and new_file != self._current_file:
                 if new_file and new_file != self._current_file:
+                    print(f"🎯 Pattern changed from '{self._current_file}' to '{new_file}'")
                     # Find preview for the new pattern
                     # Find preview for the new pattern
                     preview_path = self._find_pattern_preview(new_file)
                     preview_path = self._find_pattern_preview(new_file)
+                    print(f"🖼️ Preview path for new pattern: {preview_path}")
                     # Emit signal so UI can update
                     # Emit signal so UI can update
                     self.executionStarted.emit(new_file, preview_path)
                     self.executionStarted.emit(new_file, preview_path)
 
 
@@ -337,12 +329,14 @@ class Backend(QObject):
                 # Handle pause state from WebSocket
                 # Handle pause state from WebSocket
                 new_paused = status.get("is_paused", False)
                 new_paused = status.get("is_paused", False)
                 if new_paused != self._is_paused:
                 if new_paused != self._is_paused:
+                    print(f"⏸️ Pause state changed: {self._is_paused} -> {new_paused}")
                     self._is_paused = new_paused
                     self._is_paused = new_paused
                     self.pausedChanged.emit(new_paused)
                     self.pausedChanged.emit(new_paused)
 
 
                 # Handle serial connection status from WebSocket
                 # Handle serial connection status from WebSocket
                 ws_connection_status = status.get("connection_status", False)
                 ws_connection_status = status.get("connection_status", False)
                 if ws_connection_status != self._serial_connected:
                 if ws_connection_status != self._serial_connected:
+                    print(f"🔌 WebSocket serial connection status changed: {ws_connection_status}")
                     self._serial_connected = ws_connection_status
                     self._serial_connected = ws_connection_status
                     self.serialConnectionChanged.emit(ws_connection_status)
                     self.serialConnectionChanged.emit(ws_connection_status)
 
 
@@ -357,6 +351,7 @@ class Backend(QObject):
                 # Handle speed updates from WebSocket
                 # Handle speed updates from WebSocket
                 ws_speed = status.get("speed", None)
                 ws_speed = status.get("speed", None)
                 if ws_speed and ws_speed != self._current_speed:
                 if ws_speed and ws_speed != self._current_speed:
+                    print(f"⚡ WebSocket speed changed: {ws_speed}")
                     self._current_speed = ws_speed
                     self._current_speed = ws_speed
                     self.speedChanged.emit(ws_speed)
                     self.speedChanged.emit(ws_speed)
 
 
@@ -431,14 +426,11 @@ class Backend(QObject):
             self.errorOccurred.emit(str(e))
             self.errorOccurred.emit(str(e))
     
     
     def _find_pattern_preview(self, fileName):
     def _find_pattern_preview(self, fileName):
-        """Find the preview image for a pattern (with caching)"""
-        # Check cache first
-        if fileName in self._preview_cache:
-            return self._preview_cache[fileName]
-
+        """Find the preview image for a pattern"""
         try:
         try:
             # Extract just the filename from the path (remove any directory prefixes)
             # Extract just the filename from the path (remove any directory prefixes)
             clean_filename = fileName.split('/')[-1]  # Get last part of path
             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
             # Check multiple possible locations for patterns directory
             # Use relative paths that work across different environments
             # Use relative paths that work across different environments
@@ -451,6 +443,8 @@ class Backend(QObject):
             for patterns_dir in possible_dirs:
             for patterns_dir in possible_dirs:
                 cache_dir = patterns_dir / "cached_images"
                 cache_dir = patterns_dir / "cached_images"
                 if cache_dir.exists():
                 if cache_dir.exists():
+                    print(f"🔍 Searching for preview in cache directory: {cache_dir}")
+
                     # Extensions to try - PNG first for better kiosk compatibility
                     # Extensions to try - PNG first for better kiosk compatibility
                     extensions = [".png", ".webp", ".jpg", ".jpeg"]
                     extensions = [".png", ".webp", ".jpg", ".jpeg"]
 
 
@@ -463,11 +457,11 @@ class Backend(QObject):
                         for ext in extensions:
                         for ext in extensions:
                             preview_file = cache_dir / (filename + ext)
                             preview_file = cache_dir / (filename + ext)
                             if preview_file.exists():
                             if preview_file.exists():
-                                preview_path = str(preview_file.absolute())
-                                self._preview_cache[fileName] = preview_path
-                                return preview_path
+                                print(f"✅ Found preview (direct): {preview_file}")
+                                return str(preview_file.absolute())
 
 
                     # If not found directly, search recursively through subdirectories
                     # If not found directly, search recursively through subdirectories
+                    print(f"🔍 Searching recursively in {cache_dir}...")
                     for filename in filenames_to_try:
                     for filename in filenames_to_try:
                         for ext in extensions:
                         for ext in extensions:
                             target_name = filename + ext
                             target_name = filename + ext
@@ -475,17 +469,15 @@ class Backend(QObject):
                             matches = list(cache_dir.rglob(target_name))
                             matches = list(cache_dir.rglob(target_name))
                             if matches:
                             if matches:
                                 # Return the first match found
                                 # Return the first match found
-                                preview_path = str(matches[0].absolute())
-                                self._preview_cache[fileName] = preview_path
-                                return preview_path
+                                preview_file = matches[0]
+                                print(f"✅ Found preview (recursive): {preview_file}")
+                                return str(preview_file.absolute())
 
 
+            print("❌ No preview image found")
             return ""
             return ""
         except Exception as e:
         except Exception as e:
+            print(f"💥 Exception finding preview: {e}")
             return ""
             return ""
-
-    def _clear_preview_cache(self):
-        """Clear the preview path cache (call when patterns might change)"""
-        self._preview_cache = {}
     
     
     @Slot()
     @Slot()
     def stopExecution(self):
     def stopExecution(self):
@@ -1136,155 +1128,175 @@ class Backend(QObject):
             traceback.print_exc()
             traceback.print_exc()
     
     
     def _reset_activity_timer(self):
     def _reset_activity_timer(self):
-        """Reset the screen timeout timer (event-driven approach)"""
-        current_time = time.time()
-        # Throttle: only reset if more than 100ms since last reset
-        if current_time - self._last_activity < 0.1:
-            return
-        self._last_activity = current_time
-        # Stop existing timer and restart with full timeout duration
-        self._screen_timer.stop()
-        if self._screen_timeout > 0 and self._screen_on:
-            self._screen_timer.start(self._screen_timeout * 1000)
-
-    def _screen_timeout_triggered(self):
-        """Handle screen timeout - timer has already waited the full duration"""
-        logger.info("🖥️ Screen timeout triggered")
-        if self._screen_on and self._screen_timeout > 0:
-            self._turn_screen_off()
-            # Add delay before starting touch monitoring to avoid catching residual events
-            logger.info("🖥️ Scheduling touch monitoring in 1 second")
-            QTimer.singleShot(1000, self._start_touch_monitoring)
-
+        """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 and self._screen_timeout > 0:  # Only check if timeout is enabled
+            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
+        # If timeout is 0 (Never), screen stays on indefinitely
+    
     def _start_touch_monitoring(self):
     def _start_touch_monitoring(self):
         """Start monitoring touch input for wake-up"""
         """Start monitoring touch input for wake-up"""
-        logger.info("👆 _start_touch_monitoring called")
         if self._touch_monitor_thread is None or not self._touch_monitor_thread.is_alive():
         if self._touch_monitor_thread is None or not self._touch_monitor_thread.is_alive():
-            logger.info("👆 Creating touch monitor thread")
             self._touch_monitor_thread = threading.Thread(target=self._monitor_touch_input, daemon=True)
             self._touch_monitor_thread = threading.Thread(target=self._monitor_touch_input, daemon=True)
             self._touch_monitor_thread.start()
             self._touch_monitor_thread.start()
-            logger.info("👆 Touch monitor thread started")
-        else:
-            logger.info("👆 Touch monitor thread already running")
-
+    
     def _monitor_touch_input(self):
     def _monitor_touch_input(self):
         """Monitor touch input to wake up the screen"""
         """Monitor touch input to wake up the screen"""
-        logger.info("👆 Touch monitoring thread started")
+        print("👆 Starting touch monitoring for wake-up")
         # Add delay to let any residual touch events clear
         # Add delay to let any residual touch events clear
         time.sleep(2)
         time.sleep(2)
-
+        
         # Flush touch device to clear any buffered events
         # Flush touch device to clear any buffered events
         try:
         try:
-            import fcntl
+            # Find and flush touch device
             for i in range(5):
             for i in range(5):
                 device = f'/dev/input/event{i}'
                 device = f'/dev/input/event{i}'
                 if Path(device).exists():
                 if Path(device).exists():
                     try:
                     try:
+                        # Read and discard any pending events
                         with open(device, 'rb') as f:
                         with open(device, 'rb') as f:
+                            import fcntl
+                            import os
                             fcntl.fcntl(f.fileno(), fcntl.F_SETFL, os.O_NONBLOCK)
                             fcntl.fcntl(f.fileno(), fcntl.F_SETFL, os.O_NONBLOCK)
                             while True:
                             while True:
                                 try:
                                 try:
-                                    f.read(24)
+                                    f.read(24)  # Standard input_event size
                                 except:
                                 except:
                                     break
                                     break
-                        logger.debug(f"👆 Flushed touch device: {device}")
+                        print(f"👆 Flushed touch device: {device}")
                         break
                         break
                     except:
                     except:
                         continue
                         continue
         except Exception as e:
         except Exception as e:
-            logger.warning(f"👆 Could not flush touch device: {e}")
-
-        logger.info("👆 Touch monitoring active - waiting for touch events")
+            print(f"👆 Could not flush touch device: {e}")
+        
+        print("👆 Touch monitoring active")
         try:
         try:
-            # Find touch input device
-            touch_device = None
-            for i in range(5):
-                device = f'/dev/input/event{i}'
-                if Path(device).exists():
-                    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'
-
-            logger.info(f"👆 Monitoring touch device: {touch_device}")
-
-            # Use evtest with select() for non-blocking I/O
-            evtest_available = subprocess.run(['which', 'evtest'],
-                                             capture_output=True).returncode == 0
-
-            if evtest_available:
-                logger.info("👆 Using evtest for touch detection")
-                process = subprocess.Popen(['sudo', 'evtest', touch_device],
-                                         stdout=subprocess.PIPE,
-                                         stderr=subprocess.DEVNULL,
-                                         text=False)  # Binary mode for select()
-
-                # Use select() with timeout for CPU-efficient blocking
+            # 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:
                 while not self._screen_on:
-                    try:
-                        ready, _, _ = select.select([process.stdout], [], [], 1.0)
-                        if ready:
-                            data = process.stdout.read(1024)
-                            if data and b'Event:' in data:
-                                logger.info("👆 Touch detected via evtest - waking screen")
-                                process.terminate()
-                                self._turn_screen_on()
-                                self._reset_activity_timer()
-                                break
-                    except Exception as e:
-                        logger.error(f"👆 Error in evtest select: {e}")
+                    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
                         break
-
-                    if process.poll() is not None:
-                        logger.warning("👆 evtest process died unexpectedly")
-                        break
-
-                # Cleanup
+                    time.sleep(0.1)
+                
                 if process.poll() is None:
                 if process.poll() is None:
                     process.terminate()
                     process.terminate()
             else:
             else:
-                # Fallback: Use cat with select()
-                logger.info("👆 Using cat for touch detection (evtest not available)")
-                process = subprocess.Popen(['sudo', 'cat', touch_device],
-                                         stdout=subprocess.PIPE,
-                                         stderr=subprocess.DEVNULL)
-
-                while not self._screen_on:
-                    try:
-                        ready, _, _ = select.select([process.stdout], [], [], 1.0)
-                        if ready:
-                            data = process.stdout.read(1)
-                            if data:
-                                logger.info("👆 Touch detected via cat - waking screen")
+                # 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()
                                 process.terminate()
                                 self._turn_screen_on()
                                 self._turn_screen_on()
                                 self._reset_activity_timer()
                                 self._reset_activity_timer()
                                 break
                                 break
-                    except Exception as e:
-                        logger.error(f"👆 Error in cat select: {e}")
-                        break
-
-                    if process.poll() is not None:
-                        logger.warning("👆 cat process died unexpectedly")
-                        break
-
-                # Cleanup
-                if process.poll() is None:
-                    process.terminate()
-
+                        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:
         except Exception as e:
-            logger.error(f"❌ Error monitoring touch input: {e}")
-            import traceback
-            logger.error(traceback.format_exc())
+            print(f"❌ Error monitoring touch input: {e}")
 
 
-        logger.info("👆 Touch monitoring stopped")
+        print("👆 Touch monitoring stopped")
 
 
     # ==================== LED Control Methods ====================
     # ==================== LED Control Methods ====================
 
 

+ 24 - 19
dune-weaver-touch/main.py

@@ -73,7 +73,7 @@ class FirstTouchFilter(QObject):
 async def startup_tasks():
 async def startup_tasks():
     """Run async startup tasks"""
     """Run async startup tasks"""
     logger.info("🚀 Starting dune-weaver-touch async initialization...")
     logger.info("🚀 Starting dune-weaver-touch async initialization...")
-
+    
     # Ensure PNG cache is available for all WebP previews
     # Ensure PNG cache is available for all WebP previews
     try:
     try:
         logger.info("🎨 Checking PNG preview cache...")
         logger.info("🎨 Checking PNG preview cache...")
@@ -84,7 +84,7 @@ async def startup_tasks():
             logger.warning("⚠️ PNG cache check completed with warnings")
             logger.warning("⚠️ PNG cache check completed with warnings")
     except Exception as e:
     except Exception as e:
         logger.error(f"❌ PNG cache check failed: {e}")
         logger.error(f"❌ PNG cache check failed: {e}")
-
+    
     logger.info("✨ dune-weaver-touch startup tasks completed")
     logger.info("✨ dune-weaver-touch startup tasks completed")
 
 
 def is_pi5():
 def is_pi5():
@@ -103,24 +103,25 @@ def main():
     app = QGuiApplication(sys.argv)
     app = QGuiApplication(sys.argv)
 
 
     # Install first-touch filter to ignore wake-up touches
     # Install first-touch filter to ignore wake-up touches
+    # Ignores the first touch after 2 seconds of inactivity
     first_touch_filter = FirstTouchFilter(idle_threshold_seconds=2.0)
     first_touch_filter = FirstTouchFilter(idle_threshold_seconds=2.0)
     app.installEventFilter(first_touch_filter)
     app.installEventFilter(first_touch_filter)
     logger.info("✅ First-touch filter installed on application")
     logger.info("✅ First-touch filter installed on application")
 
 
-    # Setup async event loop using QEventLoop
-    # This properly integrates Qt and asyncio, including QTimer callbacks
+    # Setup async event loop
     loop = QEventLoop(app)
     loop = QEventLoop(app)
     asyncio.set_event_loop(loop)
     asyncio.set_event_loop(loop)
-
+    
     # Register types
     # Register types
     qmlRegisterType(Backend, "DuneWeaver", 1, 0, "Backend")
     qmlRegisterType(Backend, "DuneWeaver", 1, 0, "Backend")
     qmlRegisterType(PatternModel, "DuneWeaver", 1, 0, "PatternModel")
     qmlRegisterType(PatternModel, "DuneWeaver", 1, 0, "PatternModel")
     qmlRegisterType(PlaylistModel, "DuneWeaver", 1, 0, "PlaylistModel")
     qmlRegisterType(PlaylistModel, "DuneWeaver", 1, 0, "PlaylistModel")
-
+    
     # Load QML
     # Load QML
     engine = QQmlApplicationEngine()
     engine = QQmlApplicationEngine()
 
 
     # Set rotation flag for Pi 5 (display needs 180° rotation via QML)
     # Set rotation flag for Pi 5 (display needs 180° rotation via QML)
+    # This applies regardless of Qt backend (eglfs or linuxfb)
     rotate_display = is_pi5()
     rotate_display = is_pi5()
     engine.rootContext().setContextProperty("rotateDisplay", rotate_display)
     engine.rootContext().setContextProperty("rotateDisplay", rotate_display)
     if rotate_display:
     if rotate_display:
@@ -128,16 +129,25 @@ def main():
 
 
     qml_file = Path(__file__).parent / "qml" / "main.qml"
     qml_file = Path(__file__).parent / "qml" / "main.qml"
     engine.load(QUrl.fromLocalFile(str(qml_file)))
     engine.load(QUrl.fromLocalFile(str(qml_file)))
-
+    
     if not engine.rootObjects():
     if not engine.rootObjects():
-        logger.error("❌ Failed to load QML - no root objects")
         return -1
         return -1
-
-    # Schedule startup tasks after event loop starts
+    
+    # Schedule startup tasks after a brief delay to ensure event loop is running
     def schedule_startup():
     def schedule_startup():
-        asyncio.create_task(startup_tasks())
-
-    QTimer.singleShot(100, schedule_startup)
+        try:
+            # Check if we're in an event loop context
+            current_loop = asyncio.get_running_loop()
+            current_loop.create_task(startup_tasks())
+        except RuntimeError:
+            # No running loop, create task directly
+            asyncio.create_task(startup_tasks())
+    
+    # Use QTimer to delay startup tasks
+    startup_timer = QTimer()
+    startup_timer.timeout.connect(schedule_startup)
+    startup_timer.setSingleShot(True)
+    startup_timer.start(100)  # 100ms delay
 
 
     # Setup signal handlers for clean shutdown
     # Setup signal handlers for clean shutdown
     def signal_handler(signum, frame):
     def signal_handler(signum, frame):
@@ -148,10 +158,6 @@ def main():
     signal.signal(signal.SIGINT, signal_handler)
     signal.signal(signal.SIGINT, signal_handler)
     signal.signal(signal.SIGTERM, signal_handler)
     signal.signal(signal.SIGTERM, signal_handler)
 
 
-    logger.info("✅ Qt application started successfully")
-
-    # Run the event loop
-    # Using qasync 0.28.0 which should have CPU spin fixes
     try:
     try:
         with loop:
         with loop:
             loop.run_forever()
             loop.run_forever()
@@ -160,8 +166,7 @@ def main():
     finally:
     finally:
         loop.close()
         loop.close()
 
 
-    logger.info("🛑 Application shutdown complete")
     return 0
     return 0
 
 
 if __name__ == "__main__":
 if __name__ == "__main__":
-    sys.exit(main())
+    sys.exit(main())

+ 7 - 1
dune-weaver-touch/qml/components/BottomNavTab.qml

@@ -31,6 +31,9 @@ Rectangle {
         Text {
         Text {
             property string iconValue: parent.parent.icon
             property string iconValue: parent.parent.icon
             text: {
             text: {
+                // Debug log the icon value
+                console.log("BottomNavTab icon value:", iconValue)
+
                 // Map icon names to Unicode symbols that work on Raspberry Pi
                 // Map icon names to Unicode symbols that work on Raspberry Pi
                 switch(iconValue) {
                 switch(iconValue) {
                     case "search": return "⌕"      // U+2315 - Works better than magnifying glass
                     case "search": return "⌕"      // U+2315 - Works better than magnifying glass
@@ -38,7 +41,10 @@ Rectangle {
                     case "table_chart": return "⚙"  // U+2699 - Gear without variant selector
                     case "table_chart": return "⚙"  // U+2699 - Gear without variant selector
                     case "play_arrow": return "▶"   // U+25B6 - Play without variant selector
                     case "play_arrow": return "▶"   // U+25B6 - Play without variant selector
                     case "lightbulb": return "☀"   // U+2600 - Sun symbol for LED
                     case "lightbulb": return "☀"   // U+2600 - Sun symbol for LED
-                    default: return "□"  // U+25A1 - Simple box, universally supported
+                    default: {
+                        console.log("Unknown icon:", iconValue, "- using default")
+                        return "□"  // U+25A1 - Simple box, universally supported
+                    }
                 }
                 }
             }
             }
             font.pixelSize: 20
             font.pixelSize: 20

+ 42 - 6
dune-weaver-touch/qml/components/ConnectionStatus.qml

@@ -2,19 +2,55 @@ import QtQuick 2.15
 
 
 Rectangle {
 Rectangle {
     id: connectionDot
     id: connectionDot
-
+    
     property var backend: null
     property var backend: null
-
+    
     width: 12
     width: 12
     height: 12
     height: 12
     radius: 6
     radius: 6
-
+    
     // Direct property binding to backend.serialConnected
     // Direct property binding to backend.serialConnected
     color: {
     color: {
-        if (!backend) return "#FF5722"
-        return backend.serialConnected ? "#4CAF50" : "#FF5722"
+        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
     // Animate color changes
     Behavior on color {
     Behavior on color {
         ColorAnimation {
         ColorAnimation {

+ 1 - 0
dune-weaver-touch/qml/components/ThemeManager.qml

@@ -67,6 +67,7 @@ QtObject {
     onDarkModeChanged: {
     onDarkModeChanged: {
         // Save preference
         // Save preference
         settings.darkMode = darkMode
         settings.darkMode = darkMode
+        console.log("🎨 Dark mode:", darkMode ? "enabled" : "disabled")
     }
     }
 
 
     // Helper function to get contrast color
     // Helper function to get contrast color

+ 27 - 6
dune-weaver-touch/qml/main.qml

@@ -25,17 +25,22 @@ ApplicationWindow {
     property string currentPatternPreview: ""
     property string currentPatternPreview: ""
 
 
     onCurrentPageIndexChanged: {
     onCurrentPageIndexChanged: {
-        // Page index changed - no debug logging needed
+        console.log("📱 currentPageIndex changed to:", currentPageIndex)
     }
     }
 
 
     onShouldNavigateToExecutionChanged: {
     onShouldNavigateToExecutionChanged: {
         if (shouldNavigateToExecution) {
         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 we're in a sub-page (like PatternDetailPage), pop back to main view first
             if (stackView.depth > 1) {
             if (stackView.depth > 1) {
+                console.log("🎯 Popping back to main view first")
                 stackView.pop()
                 stackView.pop()
             }
             }
 
 
             // Then navigate to ExecutionPage tab (index 4)
             // Then navigate to ExecutionPage tab (index 4)
+            console.log("🎯 Setting currentPageIndex to 4")
             currentPageIndex = 4
             currentPageIndex = 4
             shouldNavigateToExecution = false
             shouldNavigateToExecution = false
         }
         }
@@ -45,11 +50,14 @@ ApplicationWindow {
         id: backend
         id: backend
         
         
         onExecutionStarted: function(patternName, patternPreview) {
         onExecutionStarted: function(patternName, patternPreview) {
+            console.log("🎯 QML: ExecutionStarted signal received! patternName='" + patternName + "', preview='" + patternPreview + "'")
+            console.log("🎯 Setting shouldNavigateToExecution = true")
             // Store pattern info for ExecutionPage
             // Store pattern info for ExecutionPage
             window.currentPatternName = patternName
             window.currentPatternName = patternName
             window.currentPatternPreview = patternPreview
             window.currentPatternPreview = patternPreview
-            // Navigate to Execution tab (index 4)
+            // Navigate to Execution tab (index 3) instead of pushing page
             shouldNavigateToExecution = true
             shouldNavigateToExecution = true
+            console.log("🎯 shouldNavigateToExecution set to:", shouldNavigateToExecution)
         }
         }
         
         
         onErrorOccurred: function(error) {
         onErrorOccurred: function(error) {
@@ -64,13 +72,16 @@ ApplicationWindow {
         }
         }
         
         
         onScreenStateChanged: function(isOn) {
         onScreenStateChanged: function(isOn) {
-            // Screen state changed - handled by backend
+            console.log("🖥️ Screen state changed:", isOn ? "ON" : "OFF")
         }
         }
         
         
         onBackendConnectionChanged: function(connected) {
         onBackendConnectionChanged: function(connected) {
+            console.log("🔗 Backend connection changed:", connected)
             if (connected && stackView.currentItem.toString().indexOf("ConnectionSplash") !== -1) {
             if (connected && stackView.currentItem.toString().indexOf("ConnectionSplash") !== -1) {
+                console.log("✅ Backend connected, switching to main view")
                 stackView.replace(mainSwipeView)
                 stackView.replace(mainSwipeView)
             } else if (!connected && stackView.currentItem.toString().indexOf("ConnectionSplash") === -1) {
             } else if (!connected && stackView.currentItem.toString().indexOf("ConnectionSplash") === -1) {
+                console.log("❌ Backend disconnected, switching to splash screen")
                 stackView.replace(connectionSplash)
                 stackView.replace(connectionSplash)
             }
             }
         }
         }
@@ -80,13 +91,21 @@ ApplicationWindow {
     MouseArea {
     MouseArea {
         anchors.fill: parent
         anchors.fill: parent
         acceptedButtons: Qt.NoButton  // Don't interfere with other mouse areas
         acceptedButtons: Qt.NoButton  // Don't interfere with other mouse areas
+        hoverEnabled: true
         propagateComposedEvents: true
         propagateComposedEvents: true
-
+        
         onPressed: {
         onPressed: {
+            console.log("🖥️ QML: Touch/press detected - resetting activity timer")
             backend.resetActivityTimer()
             backend.resetActivityTimer()
         }
         }
-
+        
+        onPositionChanged: {
+            console.log("🖥️ QML: Mouse movement detected - resetting activity timer")
+            backend.resetActivityTimer()
+        }
+        
         onClicked: {
         onClicked: {
+            console.log("🖥️ QML: Click detected - resetting activity timer")
             backend.resetActivityTimer()
             backend.resetActivityTimer()
         }
         }
     }
     }
@@ -115,6 +134,7 @@ ApplicationWindow {
                 showRetryButton: backend.reconnectStatus === "Cannot connect to backend"
                 showRetryButton: backend.reconnectStatus === "Cannot connect to backend"
                 
                 
                 onRetryConnection: {
                 onRetryConnection: {
+                    console.log("🔄 Manual retry requested")
                     backend.retryConnection()
                     backend.retryConnection()
                 }
                 }
             }
             }
@@ -134,7 +154,7 @@ ApplicationWindow {
                     currentIndex: window.currentPageIndex
                     currentIndex: window.currentPageIndex
                     
                     
                     Component.onCompleted: {
                     Component.onCompleted: {
-                        // StackLayout initialized
+                        console.log("📱 StackLayout created with currentIndex:", currentIndex, "bound to window.currentPageIndex:", window.currentPageIndex)
                     }
                     }
                     
                     
                     // Patterns Page
                     // Patterns Page
@@ -194,6 +214,7 @@ ApplicationWindow {
                     currentIndex: window.currentPageIndex
                     currentIndex: window.currentPageIndex
                     
                     
                     onTabClicked: function(index) {
                     onTabClicked: function(index) {
+                        console.log("📱 Tab clicked:", index)
                         window.currentPageIndex = index
                         window.currentPageIndex = index
                     }
                     }
                 }
                 }

+ 60 - 2
dune-weaver-touch/qml/pages/ExecutionPage.qml

@@ -12,11 +12,41 @@ Page {
     property string patternName: ""
     property string patternName: ""
     property string patternPreview: ""  // Backend provides this via executionStarted signal
     property string patternPreview: ""  // Backend provides this via executionStarted signal
     
     
-    // Backend signal connections
+    // 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)
+        }
+    }
+    
+    // Direct connection to backend signals
     Connections {
     Connections {
         target: backend
         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)
+            }
+        }
+
         function onExecutionStarted(fileName, preview) {
         function onExecutionStarted(fileName, preview) {
+            console.log("🎯 ExecutionPage: executionStarted signal received!")
+            console.log("🎯 Pattern:", fileName)
+            console.log("🎯 Preview path:", preview)
             // Update preview directly from backend signal
             // Update preview directly from backend signal
             patternName = fileName
             patternName = fileName
             patternPreview = preview
             patternPreview = preview
@@ -88,8 +118,36 @@ Page {
                     Image {
                     Image {
                         anchors.fill: parent
                         anchors.fill: parent
                         anchors.margins: 10
                         anchors.margins: 10
-                        source: patternPreview ? "file://" + patternPreview : ""
+                        source: {
+                            var finalSource = ""
+
+                            // Trust the backend's preview path - it already has recursive search
+                            if (patternPreview) {
+                                // Backend returns absolute path, just add file:// prefix
+                                finalSource = "file://" + patternPreview
+                                console.log("🖼️ Using backend patternPreview:", finalSource)
+                            } else {
+                                console.log("🖼️ No preview from backend")
+                            }
+
+                            return finalSource
+                        }
                         fillMode: Image.PreserveAspectFit
                         fillMode: Image.PreserveAspectFit
+
+                        onStatusChanged: {
+                            console.log("📷 Image status:", status, "for source:", source)
+                            if (status === Image.Error) {
+                                console.log("❌ Image failed to load:", source)
+                            } else if (status === Image.Ready) {
+                                console.log("✅ Image loaded successfully:", source)
+                            } else if (status === Image.Loading) {
+                                console.log("🔄 Image loading:", source)
+                            }
+                        }
+
+                        onSourceChanged: {
+                            console.log("🔄 Image source changed to:", source)
+                        }
                         
                         
                         Rectangle {
                         Rectangle {
                             anchors.fill: parent
                             anchors.fill: parent

+ 25 - 1
dune-weaver-touch/qml/pages/ModernPlaylistPage.qml

@@ -32,11 +32,18 @@ Page {
     onSelectedPlaylistChanged: {
     onSelectedPlaylistChanged: {
         if (selectedPlaylist) {
         if (selectedPlaylist) {
             currentPlaylistPatterns = playlistModel.getPatternsForPlaylist(selectedPlaylist)
             currentPlaylistPatterns = playlistModel.getPatternsForPlaylist(selectedPlaylist)
+            console.log("Loaded patterns for", selectedPlaylist + ":", currentPlaylistPatterns)
         } else {
         } else {
             currentPlaylistPatterns = []
             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 to navigate to playlist detail
     function showPlaylistDetail(playlistName, playlistData) {
     function showPlaylistDetail(playlistName, playlistData) {
         selectedPlaylist = playlistName
         selectedPlaylist = playlistName
@@ -451,11 +458,21 @@ Page {
                                         anchors.fill: parent
                                         anchors.fill: parent
                                         onClicked: {
                                         onClicked: {
                                             if (backend) {
                                             if (backend) {
+                                                console.log("Playing playlist:", selectedPlaylist, "with settings:", {
+                                                    pauseTime: pauseTime,
+                                                    clearPattern: clearPattern,
+                                                    runMode: runMode,
+                                                    shuffle: shuffle
+                                                })
                                                 backend.executePlaylist(selectedPlaylist, pauseTime, clearPattern, runMode, shuffle)
                                                 backend.executePlaylist(selectedPlaylist, pauseTime, clearPattern, runMode, shuffle)
-
+                                                
                                                 // Navigate to execution page
                                                 // Navigate to execution page
+                                                console.log("🎵 Navigating to execution page after playlist start")
                                                 if (mainWindow) {
                                                 if (mainWindow) {
+                                                    console.log("🎵 Setting shouldNavigateToExecution = true")
                                                     mainWindow.shouldNavigateToExecution = true
                                                     mainWindow.shouldNavigateToExecution = true
+                                                } else {
+                                                    console.log("🎵 ERROR: mainWindow is null, cannot navigate")
                                                 }
                                                 }
                                             }
                                             }
                                         }
                                         }
@@ -481,6 +498,7 @@ Page {
                                         anchors.fill: parent
                                         anchors.fill: parent
                                         onClicked: {
                                         onClicked: {
                                             shuffle = !shuffle
                                             shuffle = !shuffle
+                                            console.log("Shuffle toggled:", shuffle)
                                         }
                                         }
                                     }
                                     }
                                 }
                                 }
@@ -543,6 +561,7 @@ Page {
                                                 checked: true  // Default
                                                 checked: true  // Default
                                                 onClicked: {
                                                 onClicked: {
                                                     runMode = "single"
                                                     runMode = "single"
+                                                    console.log("Run mode set to:", runMode)
                                                 }
                                                 }
 
 
                                                 contentItem: Text {
                                                 contentItem: Text {
@@ -561,6 +580,7 @@ Page {
                                                 checked: false
                                                 checked: false
                                                 onClicked: {
                                                 onClicked: {
                                                     runMode = "loop"
                                                     runMode = "loop"
+                                                    console.log("Run mode set to:", runMode)
                                                 }
                                                 }
 
 
                                                 contentItem: Text {
                                                 contentItem: Text {
@@ -986,6 +1006,7 @@ Page {
                                                 checked: true  // Default
                                                 checked: true  // Default
                                                 onClicked: {
                                                 onClicked: {
                                                     clearPattern = "adaptive"
                                                     clearPattern = "adaptive"
+                                                    console.log("Clear pattern set to:", clearPattern)
                                                 }
                                                 }
 
 
                                                 contentItem: Text {
                                                 contentItem: Text {
@@ -1004,6 +1025,7 @@ Page {
                                                 checked: false
                                                 checked: false
                                                 onClicked: {
                                                 onClicked: {
                                                     clearPattern = "clear_center"
                                                     clearPattern = "clear_center"
+                                                    console.log("Clear pattern set to:", clearPattern)
                                                 }
                                                 }
 
 
                                                 contentItem: Text {
                                                 contentItem: Text {
@@ -1022,6 +1044,7 @@ Page {
                                                 checked: false
                                                 checked: false
                                                 onClicked: {
                                                 onClicked: {
                                                     clearPattern = "clear_perimeter"
                                                     clearPattern = "clear_perimeter"
+                                                    console.log("Clear pattern set to:", clearPattern)
                                                 }
                                                 }
 
 
                                                 contentItem: Text {
                                                 contentItem: Text {
@@ -1040,6 +1063,7 @@ Page {
                                                 checked: false
                                                 checked: false
                                                 onClicked: {
                                                 onClicked: {
                                                     clearPattern = "none"
                                                     clearPattern = "none"
+                                                    console.log("Clear pattern set to:", clearPattern)
                                                 }
                                                 }
 
 
                                                 contentItem: Text {
                                                 contentItem: Text {

+ 9 - 3
dune-weaver-touch/qml/pages/TableControlPage.qml

@@ -18,23 +18,29 @@ Page {
         target: backend
         target: backend
         
         
         function onSerialPortsUpdated(ports) {
         function onSerialPortsUpdated(ports) {
+            console.log("Serial ports updated:", ports)
             serialPorts = ports
             serialPorts = ports
         }
         }
-
+        
         function onSerialConnectionChanged(connected) {
         function onSerialConnectionChanged(connected) {
+            console.log("Serial connection changed:", connected)
             isSerialConnected = connected
             isSerialConnected = connected
         }
         }
-
+        
         function onCurrentPortChanged(port) {
         function onCurrentPortChanged(port) {
+            console.log("Current port changed:", port)
             if (port) {
             if (port) {
                 selectedPort = port
                 selectedPort = port
             }
             }
         }
         }
-
+        
+        
         function onSettingsLoaded() {
         function onSettingsLoaded() {
+            console.log("Settings loaded")
             if (backend) {
             if (backend) {
                 autoPlayOnBoot = backend.autoPlayOnBoot
                 autoPlayOnBoot = backend.autoPlayOnBoot
                 isSerialConnected = backend.serialConnected
                 isSerialConnected = backend.serialConnected
+                // Screen timeout is now managed by button selection, no need to convert
                 if (backend.currentPort) {
                 if (backend.currentPort) {
                     selectedPort = backend.currentPort
                     selectedPort = backend.currentPort
                 }
                 }

+ 1 - 1
dune-weaver-touch/requirements.txt

@@ -1,5 +1,5 @@
 PySide6>=6.5.0
 PySide6>=6.5.0
-qasync>=0.28.0
+qasync>=0.27.0
 aiohttp>=3.9.0
 aiohttp>=3.9.0
 Pillow>=10.0.0
 Pillow>=10.0.0
 python-dotenv>=1.0.0
 python-dotenv>=1.0.0