Browse Source

refactor tracking as effect

tuanchris 3 months ago
parent
commit
006f09b927

+ 15 - 1
main.py

@@ -153,7 +153,10 @@ async def lifespan(app: FastAPI):
                 # Start tracking if mode is "enabled"
                 if state.ball_tracking_mode == "enabled" and state.ball_tracking_enabled:
                     state.ball_tracking_manager.start()
-                    logger.info("Ball tracking started (enabled mode)")
+                    # Set LED to ball tracking effect
+                    controller.set_power(1)
+                    controller.set_effect(45)  # Ball tracking effect
+                    logger.info("Ball tracking started (enabled mode) with effect ID 45")
             except Exception as e:
                 logger.warning(f"Failed to initialize ball tracking manager: {e}")
                 state.ball_tracking_manager = None
@@ -2027,10 +2030,21 @@ async def set_ball_tracking_config(request: dict):
                 # Always-on mode
                 if state.ball_tracking_manager:
                     state.ball_tracking_manager.start()
+                    # Switch to ball tracking effect
+                    if state.led_controller:
+                        controller = state.led_controller.get_controller()
+                        if controller:
+                            controller.set_power(1)
+                            controller.set_effect(45)  # Ball tracking effect
+                            logger.info("Switched to ball tracking effect (enabled mode)")
             elif state.ball_tracking_mode == "disabled" or not state.ball_tracking_enabled:
                 # Disabled
                 if state.ball_tracking_manager:
                     state.ball_tracking_manager.stop()
+                # Switch back to idle effect
+                if state.led_controller:
+                    state.led_controller.effect_idle(state.dw_led_idle_effect)
+                    logger.info("Switched back to idle effect (ball tracking disabled)")
 
         return {
             "success": True,

+ 18 - 5
modules/core/pattern_manager.py

@@ -680,16 +680,29 @@ async def run_theta_rho_file(file_path, is_playlist=False):
         await reset_theta()
         
         start_time = time.time()
-        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()
 
         # Start ball tracking if mode is "playing_only"
+        ball_tracking_active = False
         if state.ball_tracking_mode == "playing_only" and state.ball_tracking_manager:
             logger.info("Starting ball tracking (playing_only mode)")
             state.ball_tracking_manager.start()
+            ball_tracking_active = True
+
+        # Set LED effect
+        if state.led_controller:
+            if ball_tracking_active:
+                # Use ball tracking effect (ID 45)
+                logger.info("Setting LED to ball tracking effect")
+                controller = state.led_controller.get_controller()
+                if controller:
+                    controller.set_power(1)
+                    controller.set_effect(45)  # Ball tracking effect
+            else:
+                # Use configured playing effect
+                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,

+ 31 - 68
modules/led/ball_tracking_manager.py

@@ -20,7 +20,7 @@ class BallTrackingManager:
         Initialize ball tracking manager
 
         Args:
-            led_controller: DWLEDController instance
+            led_controller: DWLEDController instance (kept for compatibility, not used for rendering)
             num_leds: Number of LEDs in the strip
             config: Configuration dict with keys:
                 - led_offset: LED index offset (0 to num_leds-1)
@@ -32,7 +32,7 @@ class BallTrackingManager:
                 - trail_enabled: Enable fade trail (bool)
                 - trail_length: Trail length in LEDs (1-20)
         """
-        self.led_controller = led_controller
+        self.led_controller = led_controller  # Kept for backward compatibility
         self.num_leds = num_leds
         self.config = config
 
@@ -43,7 +43,6 @@ class BallTrackingManager:
         self._active = False
         self._update_task = None
         self._last_led_index = None
-        self._last_lit_leds = set()  # Track which LEDs were lit in previous frame
 
         logger.info(f"BallTrackingManager initialized with {num_leds} LEDs")
 
@@ -57,21 +56,13 @@ class BallTrackingManager:
         logger.info("Ball tracking started")
 
     def stop(self):
-        """Stop ball tracking and clear LEDs"""
+        """Stop ball tracking"""
         if not self._active:
             return
 
         self._active = False
         self.position_buffer.clear()
         self._last_led_index = None
-        self._last_lit_leds.clear()
-
-        # Clear all LEDs
-        if self.led_controller and self.led_controller._initialized:
-            try:
-                self.led_controller.clear_all_leds()
-            except Exception as e:
-                logger.error(f"Error clearing LEDs: {e}")
 
         logger.info("Ball tracking stopped")
 
@@ -93,8 +84,8 @@ class BallTrackingManager:
         self._update_leds()
 
     def _update_leds(self):
-        """Update LED strip based on current position"""
-        if not self._active or not self.led_controller or not self.led_controller._initialized:
+        """Update LED tracking state (rendering is done by effect loop)"""
+        if not self._active:
             return
 
         # Get position to track (with lookback)
@@ -107,9 +98,7 @@ class BallTrackingManager:
         # Calculate LED index
         led_index = self._theta_to_led(theta)
 
-        # Render LEDs
-        self._render_leds(led_index)
-
+        # Store the LED index (effect will read this)
         self._last_led_index = led_index
 
     def _get_tracked_position(self) -> Optional[Tuple[float, float, float]]:
@@ -162,60 +151,34 @@ class BallTrackingManager:
 
         return led_index
 
-    def _render_leds(self, center_led: int):
+    def get_tracking_data(self) -> Optional[Dict]:
         """
-        Render LEDs with spread and optional trail
+        Get current tracking data for effect rendering
 
-        Args:
-            center_led: Center LED index to light up
+        Returns:
+            Dictionary with led_index, spread, brightness, color
+            or None if no tracking data available
         """
-        try:
-            spread = self.config.get("spread", 3)
-            brightness = self.config.get("brightness", 50) / 100.0
-            color_hex = self.config.get("color", "#ffffff")
-
-            # Convert hex color to RGB
-            color_hex = color_hex.lstrip('#')
-            r = int(color_hex[0:2], 16)
-            g = int(color_hex[2:4], 16)
-            b = int(color_hex[4:6], 16)
-
-            # Calculate which LEDs should be lit this frame
-            current_lit_leds = set()
-            half_spread = spread // 2
-
-            for i in range(-half_spread, half_spread + 1):
-                led_index = (center_led + i) % self.num_leds
-                current_lit_leds.add(led_index)
-
-            # Turn off LEDs that were on but shouldn't be anymore
-            leds_to_turn_off = self._last_lit_leds - current_lit_leds
-            for led_index in leds_to_turn_off:
-                self.led_controller.set_single_led(led_index, (0, 0, 0), 1.0)
-
-            # Update LEDs that should be on
-            for i in range(-half_spread, half_spread + 1):
-                led_index = (center_led + i) % self.num_leds
-
-                # Calculate intensity fade from center
-                if spread > 1:
-                    distance = abs(i)
-                    intensity = 1.0 - (distance / (spread / 2.0)) * 0.5  # 50-100%
-                else:
-                    intensity = 1.0
-
-                led_brightness = brightness * intensity
-                self.led_controller.set_single_led(led_index, (r, g, b), led_brightness)
-
-            # Show all updates at once
-            if self.led_controller._pixels:
-                self.led_controller._pixels.show()
-
-            # Remember which LEDs are lit for next frame
-            self._last_lit_leds = current_lit_leds
-
-        except Exception as e:
-            logger.error(f"Error rendering LEDs: {e}")
+        if self._last_led_index is None:
+            return None
+
+        # Get configuration
+        spread = self.config.get("spread", 3)
+        brightness = self.config.get("brightness", 50) / 100.0
+        color_hex = self.config.get("color", "#ffffff")
+
+        # Convert hex color to RGB tuple
+        color_hex = color_hex.lstrip('#')
+        r = int(color_hex[0:2], 16)
+        g = int(color_hex[2:4], 16)
+        b = int(color_hex[4:6], 16)
+
+        return {
+            'led_index': self._last_led_index,
+            'spread': spread,
+            'brightness': brightness,
+            'color': (r, g, b)
+        }
 
     def update_config(self, config: Dict):
         """Update configuration at runtime"""

+ 79 - 0
modules/led/dw_leds/effects/basic_effects.py

@@ -1066,6 +1066,84 @@ def mode_funky_plank(seg: Segment) -> int:
 
     return FRAMETIME
 
+def mode_ball_tracking(seg: Segment) -> int:
+    """
+    Ball tracking effect - follows the ball bearing's position in real-time
+    Reads position data from state.ball_tracking_manager
+    """
+    # Import state here to avoid circular dependency
+    from modules.core.state import state
+
+    # Get ball tracking manager
+    manager = state.ball_tracking_manager
+    if not manager or not manager._active:
+        # No active tracking, show static color or turn off
+        seg.fill(0x000000)
+        return FRAMETIME
+
+    # Get tracking data from manager
+    tracking_data = manager.get_tracking_data()
+    if not tracking_data:
+        # No position data yet
+        seg.fill(0x000000)
+        return FRAMETIME
+
+    center_led = tracking_data['led_index']
+    spread = tracking_data['spread']
+    brightness = tracking_data['brightness']
+    color_rgb = tracking_data['color']
+
+    # Convert RGB tuple to 32-bit color
+    r, g, b = color_rgb
+    color_32bit = (r << 16) | (g << 8) | b
+
+    # Initialize tracking: store last lit LEDs in seg.data
+    if seg.call == 0:
+        seg.data = []
+
+    # Calculate which LEDs should be lit this frame
+    current_lit_leds = set()
+    half_spread = spread // 2
+
+    for i in range(-half_spread, half_spread + 1):
+        led_index = (center_led + i) % seg.length
+        current_lit_leds.add(led_index)
+
+    # Convert last frame's lit LEDs from list to set
+    last_lit_leds = set(seg.data) if seg.data else set()
+
+    # Turn off LEDs that were on but shouldn't be anymore
+    leds_to_turn_off = last_lit_leds - current_lit_leds
+    for led_idx in leds_to_turn_off:
+        seg.set_pixel_color(led_idx, 0x000000)
+
+    # Update LEDs that should be on with brightness fade
+    for i in range(-half_spread, half_spread + 1):
+        led_index = (center_led + i) % seg.length
+
+        # Calculate intensity fade from center
+        if spread > 1:
+            distance = abs(i)
+            intensity = 1.0 - (distance / (spread / 2.0)) * 0.5  # 50-100%
+        else:
+            intensity = 1.0
+
+        # Apply brightness
+        final_brightness = int(brightness * intensity * 255)
+
+        # Scale color by brightness
+        final_r = (r * final_brightness) // 255
+        final_g = (g * final_brightness) // 255
+        final_b = (b * final_brightness) // 255
+        final_color = (final_r << 16) | (final_g << 8) | final_b
+
+        seg.set_pixel_color(led_index, final_color)
+
+    # Remember which LEDs are lit for next frame
+    seg.data = list(current_lit_leds)
+
+    return FRAMETIME
+
 # Effect registry
 EFFECTS = {
     0: ("Static", mode_static),
@@ -1113,6 +1191,7 @@ EFFECTS = {
     42: ("Halloween", mode_halloween),
     43: ("Noise", mode_noise),
     44: ("Funky Plank", mode_funky_plank),
+    45: ("Ball Tracking", mode_ball_tracking),
 }
 
 def get_effect(effect_id: int):