ball_tracking_manager.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. """
  2. Ball Tracking LED Manager
  3. Tracks the ball bearing's position and updates LEDs in real-time to follow its movement.
  4. """
  5. import asyncio
  6. import time
  7. import logging
  8. from collections import deque
  9. from typing import Optional, Tuple, Dict
  10. from .dw_led_controller import DWLEDController
  11. logger = logging.getLogger(__name__)
  12. class BallTrackingManager:
  13. """Manages real-time LED tracking of ball bearing position"""
  14. def __init__(self, led_controller: DWLEDController, num_leds: int, config: Dict):
  15. """
  16. Initialize ball tracking manager
  17. Args:
  18. led_controller: DWLEDController instance
  19. num_leds: Number of LEDs in the strip
  20. config: Configuration dict with keys:
  21. - led_offset: LED index offset (0 to num_leds-1)
  22. - reversed: Reverse LED direction (bool)
  23. - spread: Number of adjacent LEDs to light (1-10)
  24. - lookback: Number of coordinates to look back (0-15)
  25. - brightness: LED brightness 0-100
  26. - color: Hex color string (e.g., "#ffffff")
  27. - trail_enabled: Enable fade trail (bool)
  28. - trail_length: Trail length in LEDs (1-20)
  29. """
  30. self.led_controller = led_controller
  31. self.num_leds = num_leds
  32. self.config = config
  33. # Coordinate history buffer (max 15 coordinates)
  34. self.position_buffer = deque(maxlen=15)
  35. # Tracking state
  36. self._active = False
  37. self._update_task = None
  38. self._last_led_index = None
  39. logger.info(f"BallTrackingManager initialized with {num_leds} LEDs")
  40. def start(self):
  41. """Start ball tracking"""
  42. if self._active:
  43. logger.warning("Ball tracking already active")
  44. return
  45. self._active = True
  46. logger.info("Ball tracking started")
  47. def stop(self):
  48. """Stop ball tracking and clear LEDs"""
  49. if not self._active:
  50. return
  51. self._active = False
  52. self.position_buffer.clear()
  53. self._last_led_index = None
  54. # Clear all LEDs
  55. if self.led_controller and self.led_controller._initialized:
  56. try:
  57. self.led_controller.clear_all_leds()
  58. except Exception as e:
  59. logger.error(f"Error clearing LEDs: {e}")
  60. logger.info("Ball tracking stopped")
  61. def update_position(self, theta: float, rho: float):
  62. """
  63. Update ball position (called from pattern execution)
  64. Args:
  65. theta: Angular position in degrees (0-360)
  66. rho: Radial distance (0.0-1.0)
  67. """
  68. if not self._active:
  69. return
  70. # Add to buffer
  71. self.position_buffer.append((theta, rho, time.time()))
  72. # Trigger LED update
  73. self._update_leds()
  74. def _update_leds(self):
  75. """Update LED strip based on current position"""
  76. if not self._active or not self.led_controller or not self.led_controller._initialized:
  77. return
  78. # Get position to track (with lookback)
  79. position = self._get_tracked_position()
  80. if position is None:
  81. return
  82. theta, rho, _ = position
  83. # Calculate LED index
  84. led_index = self._theta_to_led(theta)
  85. # Render LEDs
  86. self._render_leds(led_index)
  87. self._last_led_index = led_index
  88. def _get_tracked_position(self) -> Optional[Tuple[float, float, float]]:
  89. """Get position to track (accounting for lookback delay)"""
  90. lookback = self.config.get("lookback", 0)
  91. if len(self.position_buffer) == 0:
  92. return None
  93. # Clamp lookback to buffer size
  94. lookback = min(lookback, len(self.position_buffer) - 1)
  95. lookback = max(0, lookback)
  96. # Get position from buffer
  97. # Index -1 = most recent, -2 = one back, etc.
  98. index = -(lookback + 1)
  99. return self.position_buffer[index]
  100. def _theta_to_led(self, theta: float) -> int:
  101. """
  102. Convert theta angle to LED index
  103. Args:
  104. theta: Angle in degrees (0-360)
  105. Returns:
  106. LED index (0 to num_leds-1)
  107. """
  108. # Normalize theta to 0-360
  109. theta = theta % 360
  110. if theta < 0:
  111. theta += 360
  112. # Calculate LED index (0° = LED 0 before offset)
  113. led_index = int((theta / 360.0) * self.num_leds)
  114. # Apply user-defined offset
  115. offset = self.config.get("led_offset", 0)
  116. led_index = (led_index + offset) % self.num_leds
  117. # Reverse direction if needed
  118. if self.config.get("reversed", False):
  119. led_index = (self.num_leds - led_index) % self.num_leds
  120. return led_index
  121. def _render_leds(self, center_led: int):
  122. """
  123. Render LEDs with spread and optional trail
  124. Args:
  125. center_led: Center LED index to light up
  126. """
  127. try:
  128. spread = self.config.get("spread", 3)
  129. brightness = self.config.get("brightness", 50) / 100.0
  130. color_hex = self.config.get("color", "#ffffff")
  131. # Convert hex color to RGB
  132. color_hex = color_hex.lstrip('#')
  133. r = int(color_hex[0:2], 16)
  134. g = int(color_hex[2:4], 16)
  135. b = int(color_hex[4:6], 16)
  136. # Clear previous LEDs first
  137. self.led_controller.clear_all_leds()
  138. # Render with spread
  139. half_spread = spread // 2
  140. for i in range(-half_spread, half_spread + 1):
  141. led_index = (center_led + i) % self.num_leds
  142. # Calculate intensity fade from center
  143. if spread > 1:
  144. distance = abs(i)
  145. intensity = 1.0 - (distance / (spread / 2.0)) * 0.5 # 50-100%
  146. else:
  147. intensity = 1.0
  148. led_brightness = brightness * intensity
  149. self.led_controller.set_single_led(led_index, (r, g, b), led_brightness)
  150. # Show updates
  151. if self.led_controller._pixels:
  152. self.led_controller._pixels.show()
  153. except Exception as e:
  154. logger.error(f"Error rendering LEDs: {e}")
  155. def update_config(self, config: Dict):
  156. """Update configuration at runtime"""
  157. self.config.update(config)
  158. logger.info(f"Ball tracking config updated: {config}")
  159. def get_status(self) -> Dict:
  160. """Get current tracking status"""
  161. return {
  162. "active": self._active,
  163. "buffer_size": len(self.position_buffer),
  164. "last_led_index": self._last_led_index,
  165. "config": self.config
  166. }