Ver Fonte

fix idle timeout

tuanchris há 3 meses atrás
pai
commit
0623c5f8bf

+ 1 - 1
VERSION

@@ -1 +1 @@
-3.4.2
+3.4.3

+ 22 - 0
main.py

@@ -21,6 +21,7 @@ import asyncio
 from contextlib import asynccontextmanager
 from modules.led.led_controller import LEDController, effect_idle
 from modules.led.led_interface import LEDInterface
+from modules.led.idle_timeout_manager import idle_timeout_manager
 import math
 from modules.core.cache_manager import generate_all_image_previews, get_cache_path, generate_image_preview, get_pattern_metadata
 from modules.core.version_manager import version_manager
@@ -54,6 +55,25 @@ logging.basicConfig(
 
 logger = logging.getLogger(__name__)
 
+
+async def _check_table_is_idle() -> bool:
+    """Helper function to check if table is idle."""
+    return not state.current_playing_file or state.pause_requested
+
+
+def _start_idle_led_timeout():
+    """Start idle LED timeout if enabled."""
+    if not state.dw_led_idle_timeout_enabled or state.dw_led_idle_timeout_minutes <= 0:
+        return
+
+    logger.debug(f"Starting idle LED timeout: {state.dw_led_idle_timeout_minutes} minutes")
+    idle_timeout_manager.start_idle_timeout(
+        timeout_minutes=state.dw_led_idle_timeout_minutes,
+        state=state,
+        check_idle_callback=_check_table_is_idle
+    )
+
+
 def normalize_file_path(file_path: str) -> str:
     """Normalize file path separators for consistent cross-platform handling."""
     if not file_path:
@@ -1176,6 +1196,7 @@ async def set_wled_ip(request: WLEDRequest):
     state.led_controller = LEDInterface("wled", request.wled_ip) if request.wled_ip else None
     if state.led_controller:
         state.led_controller.effect_idle()
+        _start_idle_led_timeout()
     state.save()
     logger.info(f"WLED IP updated: {request.wled_ip}")
     return {"success": True, "wled_ip": state.wled_ip}
@@ -1274,6 +1295,7 @@ async def set_led_config(request: LEDConfigRequest):
     # Show idle effect if controller is configured
     if state.led_controller:
         state.led_controller.effect_idle()
+        _start_idle_led_timeout()
 
     state.save()
 

+ 21 - 0
modules/connection/connection_manager.py

@@ -8,10 +8,30 @@ import asyncio
 
 from modules.core.state import state
 from modules.led.led_interface import LEDInterface
+from modules.led.idle_timeout_manager import idle_timeout_manager
 logger = logging.getLogger(__name__)
 
 IGNORE_PORTS = ['/dev/cu.debug-console', '/dev/cu.Bluetooth-Incoming-Port']
 
+
+async def _check_table_is_idle() -> bool:
+    """Helper function to check if table is idle."""
+    return not state.current_playing_file or state.pause_requested
+
+
+def _start_idle_led_timeout():
+    """Start idle LED timeout if enabled."""
+    if not state.dw_led_idle_timeout_enabled or state.dw_led_idle_timeout_minutes <= 0:
+        return
+
+    logger.debug(f"Starting idle LED timeout: {state.dw_led_idle_timeout_minutes} minutes")
+    idle_timeout_manager.start_idle_timeout(
+        timeout_minutes=state.dw_led_idle_timeout_minutes,
+        state=state,
+        check_idle_callback=_check_table_is_idle
+    )
+
+
 ###############################################################################
 # Connection Abstraction
 ###############################################################################
@@ -220,6 +240,7 @@ def connect_device(homing=True):
         # Set the configured idle effect after connection
         logger.info(f"Setting LED to idle effect: {state.dw_led_idle_effect}")
         state.led_controller.effect_idle(state.dw_led_idle_effect)
+        _start_idle_led_timeout()
 
 def check_and_unlock_alarm():
     """

+ 42 - 1
modules/core/pattern_manager.py

@@ -13,6 +13,7 @@ import asyncio
 import json
 # Import for legacy support, but we'll use LED interface through state
 from modules.led.led_controller import effect_playing, effect_idle
+from modules.led.idle_timeout_manager import idle_timeout_manager
 import queue
 from dataclasses import dataclass
 from typing import Optional, Callable
@@ -136,6 +137,37 @@ def is_in_scheduled_pause_period():
 
     return False
 
+
+async def check_table_is_idle() -> bool:
+    """
+    Check if the table is currently idle (not playing anything).
+    Returns True if idle, False if playing.
+    """
+    return not state.current_playing_file or state.pause_requested
+
+
+def start_idle_led_timeout():
+    """
+    Start the idle LED timeout if enabled.
+    Should be called whenever the idle effect is activated.
+    """
+    if not state.dw_led_idle_timeout_enabled:
+        logger.debug("Idle LED timeout not enabled")
+        return
+
+    timeout_minutes = state.dw_led_idle_timeout_minutes
+    if timeout_minutes <= 0:
+        logger.debug("Idle LED timeout not configured (timeout <= 0)")
+        return
+
+    logger.debug(f"Starting idle LED timeout: {timeout_minutes} minutes")
+    idle_timeout_manager.start_idle_timeout(
+        timeout_minutes=timeout_minutes,
+        state=state,
+        check_idle_callback=check_table_is_idle
+    )
+
+
 # Motion Control Thread Infrastructure
 @dataclass
 class MotionCommand:
@@ -642,7 +674,9 @@ async def run_theta_rho_file(file_path, is_playlist=False):
         if state.led_controller:
             logger.info(f"Setting LED to playing effect: {state.dw_led_playing_effect}")
             state.led_controller.effect_playing(state.dw_led_playing_effect)
-            
+            # Cancel idle timeout when playing starts
+            idle_timeout_manager.cancel_timeout()
+
         with tqdm(
             total=total_coordinates,
             unit="coords",
@@ -657,6 +691,7 @@ async def run_theta_rho_file(file_path, is_playlist=False):
                     logger.info("Execution stopped by user")
                     if state.led_controller:
                         state.led_controller.effect_idle(state.dw_led_idle_effect)
+                        start_idle_led_timeout()
                     break
 
                 if state.skip_requested:
@@ -664,6 +699,7 @@ async def run_theta_rho_file(file_path, is_playlist=False):
                     await connection_manager.check_idle_async()
                     if state.led_controller:
                         state.led_controller.effect_idle(state.dw_led_idle_effect)
+                        start_idle_led_timeout()
                     break
 
                 # Wait for resume if paused (manual or scheduled)
@@ -686,6 +722,7 @@ async def run_theta_rho_file(file_path, is_playlist=False):
                     # (manual pause always shows idle effect)
                     if state.led_controller and not (scheduled_pause and state.scheduled_pause_control_wled):
                         state.led_controller.effect_idle(state.dw_led_idle_effect)
+                        start_idle_led_timeout()
 
                     # Remember if we turned off LED controller for scheduled pause
                     wled_was_off_for_scheduled = scheduled_pause and state.scheduled_pause_control_wled and not manual_pause
@@ -707,6 +744,8 @@ async def run_theta_rho_file(file_path, is_playlist=False):
                             # Without this delay, rapid-fire requests can crash controllers on resource-constrained Pis
                             await asyncio.sleep(0.5)
                         state.led_controller.effect_playing(state.dw_led_playing_effect)
+                        # Cancel idle timeout when resuming from pause
+                        idle_timeout_manager.cancel_timeout()
 
                 # Dynamically determine the speed for each movement
                 # Use clear_pattern_speed if it's set and this is a clear file, otherwise use state.speed
@@ -742,6 +781,7 @@ async def run_theta_rho_file(file_path, is_playlist=False):
         if state.led_controller and not state.stop_requested:
             logger.info(f"Setting LED to idle effect: {state.dw_led_idle_effect}")
             state.led_controller.effect_idle(state.dw_led_idle_effect)
+            start_idle_led_timeout()
             logger.debug("LED effect set to idle after pattern completion")
         
         # Only clear state if not part of a playlist
@@ -884,6 +924,7 @@ async def run_theta_rho_files(file_paths, pause_time=0, clear_pattern=None, run_
 
         if state.led_controller:
             state.led_controller.effect_idle(state.dw_led_idle_effect)
+            start_idle_led_timeout()
 
         logger.info("All requested patterns completed (or stopped) and state cleared")
 

+ 95 - 0
modules/led/idle_timeout_manager.py

@@ -0,0 +1,95 @@
+"""
+Idle LED Timeout Manager
+Handles automatic LED turn-off after a period of inactivity.
+"""
+import asyncio
+import logging
+from datetime import datetime
+from typing import Optional
+
+logger = logging.getLogger(__name__)
+
+
+class IdleTimeoutManager:
+    """
+    Manages idle timeout for LED effects.
+    When idle effect is played, starts a timer. When timer expires,
+    checks if table is still idle and turns off LEDs if so.
+    """
+
+    def __init__(self):
+        self._timeout_task: Optional[asyncio.Task] = None
+        self._last_idle_time: Optional[datetime] = None
+
+    def start_idle_timeout(self, timeout_minutes: float, state, check_idle_callback):
+        """
+        Start or restart the idle timeout timer.
+
+        Args:
+            timeout_minutes: Minutes to wait before turning off LEDs
+            state: Application state object
+            check_idle_callback: Async callback to check if table is still idle
+        """
+        # Cancel any existing timeout
+        self.cancel_timeout()
+
+        if timeout_minutes <= 0:
+            logger.debug("Idle timeout disabled (timeout <= 0)")
+            return
+
+        # Record when idle effect was started
+        self._last_idle_time = datetime.now()
+        logger.info(f"Starting idle LED timeout: {timeout_minutes} minutes")
+
+        # Create background task to handle timeout
+        self._timeout_task = asyncio.create_task(
+            self._timeout_handler(timeout_minutes, state, check_idle_callback)
+        )
+
+    async def _timeout_handler(self, timeout_minutes: float, state, check_idle_callback):
+        """
+        Background task that waits for timeout and turns off LEDs if still idle.
+        """
+        try:
+            # Wait for the specified timeout
+            timeout_seconds = timeout_minutes * 60
+            await asyncio.sleep(timeout_seconds)
+
+            # Check if we should turn off the LEDs
+            logger.debug("Idle timeout expired, checking table state...")
+
+            # Check if table is still idle (not playing anything)
+            is_idle = await check_idle_callback()
+
+            if is_idle:
+                logger.info("Table is still idle after timeout - turning off LEDs")
+                if state.led_controller:
+                    try:
+                        state.led_controller.set_power(0)  # Turn off LEDs
+                        logger.info("LEDs turned off successfully")
+                    except Exception as e:
+                        logger.error(f"Failed to turn off LEDs: {e}")
+                else:
+                    logger.warning("LED controller not configured")
+            else:
+                logger.debug("Table is not idle - skipping LED turn-off")
+
+        except asyncio.CancelledError:
+            logger.debug("Idle timeout cancelled")
+        except Exception as e:
+            logger.error(f"Error in idle timeout handler: {e}")
+
+    def cancel_timeout(self):
+        """Cancel any running timeout task."""
+        if self._timeout_task and not self._timeout_task.done():
+            logger.debug("Cancelling existing idle timeout")
+            self._timeout_task.cancel()
+            self._timeout_task = None
+
+    def is_timeout_active(self) -> bool:
+        """Check if a timeout is currently active."""
+        return self._timeout_task is not None and not self._timeout_task.done()
+
+
+# Singleton instance
+idle_timeout_manager = IdleTimeoutManager()