Explorar el Código

Add automatic pattern preview sync for touchscreen

When patterns are uploaded via the web interface, the touchscreen now
automatically receives a notification and updates its pattern list with
the new preview image. The touchscreen handles its own PNG conversion
from WebP for Qt/QML compatibility.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
tuanchris hace 3 semanas
padre
commit
a77c50d8e4
Se han modificado 4 ficheros con 145 adiciones y 3 borrados
  1. 49 2
      dune-weaver-touch/backend.py
  2. 6 0
      dune-weaver-touch/qml/main.qml
  3. 31 1
      main.py
  4. 59 0
      modules/core/cache_manager.py

+ 49 - 2
dune-weaver-touch/backend.py

@@ -10,6 +10,7 @@ import threading
 import time
 from pathlib import Path
 import os
+from png_cache_manager import PngCacheManager
 
 QML_IMPORT_NAME = "DuneWeaver"
 QML_IMPORT_MAJOR_VERSION = 1
@@ -81,6 +82,9 @@ class Backend(QObject):
     ledStatusChanged = Signal()
     ledEffectsLoaded = Signal(list)  # List of available effects
     ledPalettesLoaded = Signal(list)  # List of available palettes
+
+    # Pattern updates signal (for touchscreen to refresh when patterns are uploaded)
+    patternsUpdated = Signal(str)  # pattern_file that was added/updated (or empty string)
     
     def __init__(self):
         super().__init__()
@@ -143,7 +147,10 @@ class Backend(QObject):
         # Load local settings first
         self._load_local_settings()
         print(f"🖥️ Screen management initialized: timeout={self._screen_timeout}s, timer started")
-        
+
+        # PNG cache manager for converting WebP previews to PNG
+        self._png_cache_manager = PngCacheManager()
+
         # HTTP session - initialize lazily
         self.session = None
         self._session_initialized = False
@@ -360,6 +367,19 @@ class Backend(QObject):
 
                 self.statusChanged.emit()
                 self.progressChanged.emit()
+
+            # Handle patterns_updated notification (when patterns are uploaded via web interface)
+            elif data.get("type") == "patterns_updated":
+                pattern_data = data.get("data", {})
+                pattern_file = pattern_data.get("pattern_file", "")
+                print(f"📥 Patterns updated notification received: {pattern_file}")
+                # Convert the new pattern's WebP preview to PNG for touchscreen compatibility
+                if pattern_file:
+                    asyncio.create_task(self._convert_pattern_preview(pattern_file))
+                else:
+                    # If no specific pattern, just refresh the model
+                    self.patternsUpdated.emit(pattern_file)
+
         except json.JSONDecodeError:
             pass
     
@@ -379,7 +399,34 @@ class Backend(QObject):
                         print(f"🔌 Updated current port from WebSocket trigger: {current_port}")
         except Exception as e:
             print(f"💥 Exception getting current port: {e}")
-    
+
+    async def _convert_pattern_preview(self, pattern_file: str):
+        """Convert a pattern's WebP preview to PNG for touchscreen compatibility.
+
+        Called when we receive a patterns_updated notification from the main app.
+        The main app generates WebP previews, but Qt/QML works better with PNG.
+        """
+        try:
+            # Strip .thr extension if present to get the pattern name
+            pattern_name = pattern_file
+            if pattern_name.endswith('.thr'):
+                pattern_name = pattern_name[:-4]
+
+            print(f"🖼️ Converting preview for {pattern_name} to PNG...")
+            success = await self._png_cache_manager.convert_specific_pattern(pattern_name)
+
+            if success:
+                print(f"✅ PNG preview ready for {pattern_name}")
+            else:
+                print(f"⚠️ PNG conversion skipped or failed for {pattern_name}")
+
+        except Exception as e:
+            print(f"💥 Error converting pattern preview: {e}")
+
+        # Always emit the signal to refresh the pattern model
+        # (even if conversion failed, the WebP might still work as fallback)
+        self.patternsUpdated.emit(pattern_file)
+
     # API Methods
     @Slot(str, str)
     def executePattern(self, fileName, preExecution="adaptive"):

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

@@ -85,6 +85,12 @@ ApplicationWindow {
                 stackView.replace(connectionSplash)
             }
         }
+
+        onPatternsUpdated: function(patternFile) {
+            console.log("📥 QML: Patterns updated notification received:", patternFile)
+            console.log("📥 Refreshing pattern model to pick up new pattern/preview")
+            patternModel.refresh()
+        }
     }
     
     // Global touch/mouse handler for activity tracking

+ 31 - 1
main.py

@@ -453,6 +453,33 @@ async def broadcast_status_update(status: dict):
     
     active_status_connections.difference_update(disconnected)
 
+async def broadcast_patterns_updated(pattern_file: str = None):
+    """Broadcast pattern update notification to all connected clients.
+
+    This notifies touchscreen and other clients that patterns have changed,
+    so they can refresh their pattern lists.
+
+    Args:
+        pattern_file: Optional specific pattern that was added/updated
+    """
+    disconnected = set()
+    message = {
+        "type": "patterns_updated",
+        "data": {
+            "pattern_file": pattern_file
+        }
+    }
+    for websocket in active_status_connections:
+        try:
+            await websocket.send_json(message)
+        except WebSocketDisconnect:
+            disconnected.add(websocket)
+        except RuntimeError:
+            disconnected.add(websocket)
+
+    active_status_connections.difference_update(disconnected)
+    logger.info(f"Broadcast patterns_updated notification for: {pattern_file}")
+
 @app.websocket("/ws/cache-progress")
 async def websocket_cache_progress_endpoint(websocket: WebSocket):
     from modules.core.cache_manager import get_cache_progress
@@ -1215,7 +1242,10 @@ async def upload_theta_rho(file: UploadFile = File(...)):
                 logger.error(f"Error generating preview for {file_path_in_patterns_dir} (attempt {attempt + 1}): {str(e)}")
                 if attempt < max_retries - 1:
                     await asyncio.sleep(0.5)  # Small delay before retry
-        
+
+        # Notify connected clients (including touchscreen) about the new pattern
+        await broadcast_patterns_updated(file_path_in_patterns_dir)
+
         return {"success": True, "message": f"File {file.filename} uploaded successfully"}
     except Exception as e:
         logger.error(f"Error uploading file: {str(e)}")

+ 59 - 0
modules/core/cache_manager.py

@@ -7,6 +7,11 @@ from pathlib import Path
 from modules.core.pattern_manager import list_theta_rho_files, THETA_RHO_DIR, parse_theta_rho_file
 from modules.core.process_pool import get_pool as _get_process_pool
 
+try:
+    from PIL import Image
+except ImportError:
+    Image = None
+
 logger = logging.getLogger(__name__)
 
 # Global cache progress state
@@ -504,6 +509,60 @@ async def generate_image_preview(pattern_file):
         logger.error(f"Failed to generate image for {pattern_file}: {str(e)}")
         return False
 
+async def convert_webp_to_png(pattern_file: str) -> bool:
+    """Convert a WebP preview to PNG format for touchscreen compatibility.
+
+    The touchscreen (Qt/QML) has better support for PNG than WebP,
+    so we generate both formats when a pattern is uploaded.
+
+    Args:
+        pattern_file: The pattern file name (e.g., 'custom_patterns/my_pattern.thr')
+
+    Returns:
+        True if conversion succeeded or PNG already exists, False on error
+    """
+    if not Image:
+        logger.warning("PIL (Pillow) not available - cannot convert WebP to PNG")
+        return False
+
+    try:
+        webp_path = Path(get_cache_path(pattern_file))
+        png_path = webp_path.with_suffix('.png')
+
+        # Skip if PNG already exists
+        if png_path.exists():
+            logger.debug(f"PNG already exists for {pattern_file}")
+            return True
+
+        # Skip if WebP doesn't exist
+        if not webp_path.exists():
+            logger.warning(f"WebP not found for {pattern_file}, cannot convert to PNG")
+            return False
+
+        def _convert():
+            with Image.open(webp_path) as img:
+                # Keep transparency for modes that support it
+                if img.mode in ('RGBA', 'LA', 'P'):
+                    img.save(png_path, "PNG", optimize=True)
+                else:
+                    rgb_img = img.convert('RGB')
+                    rgb_img.save(png_path, "PNG", optimize=True)
+
+            # Set file permissions to match WebP file
+            try:
+                webp_stat = webp_path.stat()
+                os.chmod(png_path, webp_stat.st_mode)
+            except (OSError, PermissionError):
+                pass
+
+        await asyncio.to_thread(_convert)
+        logger.info(f"Converted {pattern_file} preview to PNG for touchscreen")
+        return True
+
+    except Exception as e:
+        logger.error(f"Failed to convert {pattern_file} to PNG: {e}")
+        return False
+
 async def generate_all_image_previews():
     """Generate image previews for missing patterns using set difference."""
     global cache_progress