Просмотр исходного кода

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 неделя назад
Родитель
Сommit
6c3b242446

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

@@ -5,16 +5,11 @@ from PySide6.QtNetwork import QAbstractSocket
 import aiohttp
 import asyncio
 import json
-import logging
 import subprocess
 import threading
 import time
 from pathlib import Path
 import os
-import select
-
-# Configure logging for this module
-logger = logging.getLogger(__name__)
 
 QML_IMPORT_NAME = "DuneWeaver"
 QML_IMPORT_MAJOR_VERSION = 1
@@ -109,9 +104,6 @@ class Backend(QObject):
         self._backend_connected = False
         self._reconnect_status = "Connecting to backend..."
 
-        # Preview path cache to avoid repeated filesystem lookups
-        self._preview_cache = {}  # filename -> preview_path
-
         # LED control state
         self._led_provider = "none"  # "none", "wled", or "dw_leds"
         self._led_connected = False
@@ -146,13 +138,11 @@ class Backend(QObject):
         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.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
         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
         self.session = None
@@ -326,8 +316,10 @@ class Backend(QObject):
 
                 # Detect pattern change and emit executionStarted signal
                 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
                     preview_path = self._find_pattern_preview(new_file)
+                    print(f"🖼️ Preview path for new pattern: {preview_path}")
                     # Emit signal so UI can update
                     self.executionStarted.emit(new_file, preview_path)
 
@@ -337,12 +329,14 @@ class Backend(QObject):
                 # Handle pause state from WebSocket
                 new_paused = status.get("is_paused", False)
                 if new_paused != self._is_paused:
+                    print(f"⏸️ Pause state changed: {self._is_paused} -> {new_paused}")
                     self._is_paused = new_paused
                     self.pausedChanged.emit(new_paused)
 
                 # 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)
 
@@ -357,6 +351,7 @@ class Backend(QObject):
                 # 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)
 
@@ -431,14 +426,11 @@ class Backend(QObject):
             self.errorOccurred.emit(str(e))
     
     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:
             # 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
@@ -451,6 +443,8 @@ class Backend(QObject):
             for patterns_dir in possible_dirs:
                 cache_dir = patterns_dir / "cached_images"
                 if cache_dir.exists():
+                    print(f"🔍 Searching for preview in cache directory: {cache_dir}")
+
                     # Extensions to try - PNG first for better kiosk compatibility
                     extensions = [".png", ".webp", ".jpg", ".jpeg"]
 
@@ -463,11 +457,11 @@ class Backend(QObject):
                         for ext in extensions:
                             preview_file = cache_dir / (filename + ext)
                             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
+                    print(f"🔍 Searching recursively in {cache_dir}...")
                     for filename in filenames_to_try:
                         for ext in extensions:
                             target_name = filename + ext
@@ -475,17 +469,15 @@ class Backend(QObject):
                             matches = list(cache_dir.rglob(target_name))
                             if matches:
                                 # 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 ""
         except Exception as e:
+            print(f"💥 Exception finding preview: {e}")
             return ""
-
-    def _clear_preview_cache(self):
-        """Clear the preview path cache (call when patterns might change)"""
-        self._preview_cache = {}
     
     @Slot()
     def stopExecution(self):
@@ -1136,155 +1128,175 @@ class Backend(QObject):
             traceback.print_exc()
     
     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):
         """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():
-            logger.info("👆 Creating touch monitor thread")
             self._touch_monitor_thread = threading.Thread(target=self._monitor_touch_input, daemon=True)
             self._touch_monitor_thread.start()
-            logger.info("👆 Touch monitor thread started")
-        else:
-            logger.info("👆 Touch monitor thread already running")
-
+    
     def _monitor_touch_input(self):
         """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
         time.sleep(2)
-
+        
         # Flush touch device to clear any buffered events
         try:
-            import fcntl
+            # 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)
+                                    f.read(24)  # Standard input_event size
                                 except:
                                     break
-                        logger.debug(f"👆 Flushed touch device: {device}")
+                        print(f"👆 Flushed touch device: {device}")
                         break
                     except:
                         continue
         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:
-            # 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:
-                    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
-
-                    if process.poll() is not None:
-                        logger.warning("👆 evtest process died unexpectedly")
-                        break
-
-                # Cleanup
+                    time.sleep(0.1)
+                
                 if process.poll() is None:
                     process.terminate()
             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()
                                 self._turn_screen_on()
                                 self._reset_activity_timer()
                                 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:
-            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 ====================
 

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

@@ -73,7 +73,7 @@ class FirstTouchFilter(QObject):
 async def startup_tasks():
     """Run async startup tasks"""
     logger.info("🚀 Starting dune-weaver-touch async initialization...")
-
+    
     # Ensure PNG cache is available for all WebP previews
     try:
         logger.info("🎨 Checking PNG preview cache...")
@@ -84,7 +84,7 @@ async def startup_tasks():
             logger.warning("⚠️ PNG cache check completed with warnings")
     except Exception as e:
         logger.error(f"❌ PNG cache check failed: {e}")
-
+    
     logger.info("✨ dune-weaver-touch startup tasks completed")
 
 def is_pi5():
@@ -103,24 +103,25 @@ def main():
     app = QGuiApplication(sys.argv)
 
     # 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)
     app.installEventFilter(first_touch_filter)
     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)
     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()
 
     # 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()
     engine.rootContext().setContextProperty("rotateDisplay", rotate_display)
     if rotate_display:
@@ -128,16 +129,25 @@ def main():
 
     qml_file = Path(__file__).parent / "qml" / "main.qml"
     engine.load(QUrl.fromLocalFile(str(qml_file)))
-
+    
     if not engine.rootObjects():
-        logger.error("❌ Failed to load QML - no root objects")
         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():
-        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
     def signal_handler(signum, frame):
@@ -148,10 +158,6 @@ def main():
     signal.signal(signal.SIGINT, 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:
         with loop:
             loop.run_forever()
@@ -160,8 +166,7 @@ def main():
     finally:
         loop.close()
 
-    logger.info("🛑 Application shutdown complete")
     return 0
 
 if __name__ == "__main__":
-    sys.exit(main())
+    sys.exit(main())

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

@@ -31,6 +31,9 @@ Rectangle {
         Text {
             property string iconValue: parent.parent.icon
             text: {
+                // Debug log the icon value
+                console.log("BottomNavTab icon value:", iconValue)
+
                 // Map icon names to Unicode symbols that work on Raspberry Pi
                 switch(iconValue) {
                     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 "play_arrow": return "▶"   // U+25B6 - Play without variant selector
                     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

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

@@ -2,19 +2,55 @@ 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) 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
     Behavior on color {
         ColorAnimation {

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

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

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

@@ -25,17 +25,22 @@ ApplicationWindow {
     property string currentPatternPreview: ""
 
     onCurrentPageIndexChanged: {
-        // Page index changed - no debug logging needed
+        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 (index 4)
+            console.log("🎯 Setting currentPageIndex to 4")
             currentPageIndex = 4
             shouldNavigateToExecution = false
         }
@@ -45,11 +50,14 @@ ApplicationWindow {
         id: backend
         
         onExecutionStarted: function(patternName, patternPreview) {
+            console.log("🎯 QML: ExecutionStarted signal received! patternName='" + patternName + "', preview='" + patternPreview + "'")
+            console.log("🎯 Setting shouldNavigateToExecution = true")
             // Store pattern info for ExecutionPage
             window.currentPatternName = patternName
             window.currentPatternPreview = patternPreview
-            // Navigate to Execution tab (index 4)
+            // Navigate to Execution tab (index 3) instead of pushing page
             shouldNavigateToExecution = true
+            console.log("🎯 shouldNavigateToExecution set to:", shouldNavigateToExecution)
         }
         
         onErrorOccurred: function(error) {
@@ -64,13 +72,16 @@ ApplicationWindow {
         }
         
         onScreenStateChanged: function(isOn) {
-            // Screen state changed - handled by backend
+            console.log("🖥️ Screen state changed:", isOn ? "ON" : "OFF")
         }
         
         onBackendConnectionChanged: function(connected) {
+            console.log("🔗 Backend connection changed:", connected)
             if (connected && stackView.currentItem.toString().indexOf("ConnectionSplash") !== -1) {
+                console.log("✅ Backend connected, switching to main view")
                 stackView.replace(mainSwipeView)
             } else if (!connected && stackView.currentItem.toString().indexOf("ConnectionSplash") === -1) {
+                console.log("❌ Backend disconnected, switching to splash screen")
                 stackView.replace(connectionSplash)
             }
         }
@@ -80,13 +91,21 @@ ApplicationWindow {
     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()
         }
     }
@@ -115,6 +134,7 @@ ApplicationWindow {
                 showRetryButton: backend.reconnectStatus === "Cannot connect to backend"
                 
                 onRetryConnection: {
+                    console.log("🔄 Manual retry requested")
                     backend.retryConnection()
                 }
             }
@@ -134,7 +154,7 @@ ApplicationWindow {
                     currentIndex: window.currentPageIndex
                     
                     Component.onCompleted: {
-                        // StackLayout initialized
+                        console.log("📱 StackLayout created with currentIndex:", currentIndex, "bound to window.currentPageIndex:", window.currentPageIndex)
                     }
                     
                     // Patterns Page
@@ -194,6 +214,7 @@ ApplicationWindow {
                     currentIndex: window.currentPageIndex
                     
                     onTabClicked: function(index) {
+                        console.log("📱 Tab clicked:", index)
                         window.currentPageIndex = index
                     }
                 }

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

@@ -12,11 +12,41 @@ Page {
     property string patternName: ""
     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 {
         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) {
+            console.log("🎯 ExecutionPage: executionStarted signal received!")
+            console.log("🎯 Pattern:", fileName)
+            console.log("🎯 Preview path:", preview)
             // Update preview directly from backend signal
             patternName = fileName
             patternPreview = preview
@@ -88,8 +118,36 @@ Page {
                     Image {
                         anchors.fill: parent
                         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
+
+                        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 {
                             anchors.fill: parent

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

@@ -32,11 +32,18 @@ Page {
     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
@@ -451,11 +458,21 @@ Page {
                                         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")
                                                 }
                                             }
                                         }
@@ -481,6 +498,7 @@ Page {
                                         anchors.fill: parent
                                         onClicked: {
                                             shuffle = !shuffle
+                                            console.log("Shuffle toggled:", shuffle)
                                         }
                                     }
                                 }
@@ -543,6 +561,7 @@ Page {
                                                 checked: true  // Default
                                                 onClicked: {
                                                     runMode = "single"
+                                                    console.log("Run mode set to:", runMode)
                                                 }
 
                                                 contentItem: Text {
@@ -561,6 +580,7 @@ Page {
                                                 checked: false
                                                 onClicked: {
                                                     runMode = "loop"
+                                                    console.log("Run mode set to:", runMode)
                                                 }
 
                                                 contentItem: Text {
@@ -986,6 +1006,7 @@ Page {
                                                 checked: true  // Default
                                                 onClicked: {
                                                     clearPattern = "adaptive"
+                                                    console.log("Clear pattern set to:", clearPattern)
                                                 }
 
                                                 contentItem: Text {
@@ -1004,6 +1025,7 @@ Page {
                                                 checked: false
                                                 onClicked: {
                                                     clearPattern = "clear_center"
+                                                    console.log("Clear pattern set to:", clearPattern)
                                                 }
 
                                                 contentItem: Text {
@@ -1022,6 +1044,7 @@ Page {
                                                 checked: false
                                                 onClicked: {
                                                     clearPattern = "clear_perimeter"
+                                                    console.log("Clear pattern set to:", clearPattern)
                                                 }
 
                                                 contentItem: Text {
@@ -1040,6 +1063,7 @@ Page {
                                                 checked: false
                                                 onClicked: {
                                                     clearPattern = "none"
+                                                    console.log("Clear pattern set to:", clearPattern)
                                                 }
 
                                                 contentItem: Text {

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

@@ -18,23 +18,29 @@ Page {
         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 onSettingsLoaded() {
+            console.log("Settings loaded")
             if (backend) {
                 autoPlayOnBoot = backend.autoPlayOnBoot
                 isSerialConnected = backend.serialConnected
+                // Screen timeout is now managed by button selection, no need to convert
                 if (backend.currentPort) {
                     selectedPort = backend.currentPort
                 }

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

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