Add dynamic FPS to static, gradient, and color cycle streams
All three non-picture color strip stream types had their animation loops hardcoded at 30 FPS and lacked set_capture_fps(), so target FPS changes had no effect. Now each stream reads self._fps per iteration and exposes set_capture_fps() for the stream manager. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -470,6 +470,7 @@ class StaticColorStripStream(ColorStripStream):
|
||||
self._colors_lock = threading.Lock()
|
||||
self._running = False
|
||||
self._thread: Optional[threading.Thread] = None
|
||||
self._fps = 30
|
||||
self._update_from_source(source)
|
||||
|
||||
def _update_from_source(self, source) -> None:
|
||||
@@ -502,12 +503,17 @@ class StaticColorStripStream(ColorStripStream):
|
||||
|
||||
@property
|
||||
def target_fps(self) -> int:
|
||||
return 30
|
||||
return self._fps
|
||||
|
||||
@property
|
||||
def led_count(self) -> int:
|
||||
return self._led_count
|
||||
|
||||
def set_capture_fps(self, fps: int) -> None:
|
||||
"""Update animation loop rate. Thread-safe (read atomically by the loop)."""
|
||||
fps = max(1, min(90, fps))
|
||||
self._fps = fps
|
||||
|
||||
def start(self) -> None:
|
||||
if self._running:
|
||||
return
|
||||
@@ -545,13 +551,12 @@ class StaticColorStripStream(ColorStripStream):
|
||||
logger.info("StaticColorStripStream params updated in-place")
|
||||
|
||||
def _animate_loop(self) -> None:
|
||||
"""Background thread: compute animated colors at ~30 fps when animation is active.
|
||||
"""Background thread: compute animated colors at target fps when animation is active.
|
||||
|
||||
Uses double-buffered output arrays (buf_a / buf_b) to avoid per-frame
|
||||
numpy allocations while preserving the identity check used by the
|
||||
processing loop (``colors is prev_colors``).
|
||||
"""
|
||||
frame_time = 1.0 / 30
|
||||
# Double-buffer pool — re-allocated only when LED count changes
|
||||
_pool_n = 0
|
||||
_buf_a = _buf_b = None
|
||||
@@ -560,6 +565,7 @@ class StaticColorStripStream(ColorStripStream):
|
||||
with high_resolution_timer():
|
||||
while self._running:
|
||||
loop_start = time.perf_counter()
|
||||
frame_time = 1.0 / self._fps
|
||||
anim = self._animation
|
||||
if anim and anim.get("enabled"):
|
||||
speed = float(anim.get("speed", 1.0))
|
||||
@@ -645,7 +651,7 @@ class ColorCycleColorStripStream(ColorStripStream):
|
||||
"""Color strip stream that smoothly cycles through a user-defined color list.
|
||||
|
||||
All LEDs receive the same solid color at any moment, continuously interpolating
|
||||
between the configured colors in a loop at 30 fps.
|
||||
between the configured colors in a loop.
|
||||
|
||||
LED count auto-sizes from the connected device when led_count == 0 in
|
||||
the source config; configure(device_led_count) is called by
|
||||
@@ -656,6 +662,7 @@ class ColorCycleColorStripStream(ColorStripStream):
|
||||
self._colors_lock = threading.Lock()
|
||||
self._running = False
|
||||
self._thread: Optional[threading.Thread] = None
|
||||
self._fps = 30
|
||||
self._update_from_source(source)
|
||||
|
||||
def _update_from_source(self, source) -> None:
|
||||
@@ -687,12 +694,17 @@ class ColorCycleColorStripStream(ColorStripStream):
|
||||
|
||||
@property
|
||||
def target_fps(self) -> int:
|
||||
return 30
|
||||
return self._fps
|
||||
|
||||
@property
|
||||
def led_count(self) -> int:
|
||||
return self._led_count
|
||||
|
||||
def set_capture_fps(self, fps: int) -> None:
|
||||
"""Update animation loop rate. Thread-safe (read atomically by the loop)."""
|
||||
fps = max(1, min(90, fps))
|
||||
self._fps = fps
|
||||
|
||||
def start(self) -> None:
|
||||
if self._running:
|
||||
return
|
||||
@@ -729,11 +741,10 @@ class ColorCycleColorStripStream(ColorStripStream):
|
||||
logger.info("ColorCycleColorStripStream params updated in-place")
|
||||
|
||||
def _animate_loop(self) -> None:
|
||||
"""Background thread: interpolate between colors at ~30 fps.
|
||||
"""Background thread: interpolate between colors at target fps.
|
||||
|
||||
Uses double-buffered output arrays to avoid per-frame allocations.
|
||||
"""
|
||||
frame_time = 1.0 / 30
|
||||
_pool_n = 0
|
||||
_buf_a = _buf_b = None
|
||||
_use_a = True
|
||||
@@ -741,6 +752,7 @@ class ColorCycleColorStripStream(ColorStripStream):
|
||||
with high_resolution_timer():
|
||||
while self._running:
|
||||
loop_start = time.perf_counter()
|
||||
frame_time = 1.0 / self._fps
|
||||
color_list = self._color_list
|
||||
speed = self._cycle_speed
|
||||
n = self._led_count
|
||||
@@ -776,7 +788,7 @@ class GradientColorStripStream(ColorStripStream):
|
||||
"""Color strip stream that distributes a gradient across all LEDs.
|
||||
|
||||
Produces a pre-computed (led_count, 3) uint8 array from user-defined
|
||||
color stops. When animation is enabled a 30 fps background thread applies
|
||||
color stops. When animation is enabled a background thread applies
|
||||
dynamic effects (breathing, gradient_shift, wave).
|
||||
|
||||
LED count auto-sizes from the connected device when led_count == 0 in
|
||||
@@ -788,6 +800,7 @@ class GradientColorStripStream(ColorStripStream):
|
||||
self._colors_lock = threading.Lock()
|
||||
self._running = False
|
||||
self._thread: Optional[threading.Thread] = None
|
||||
self._fps = 30
|
||||
self._update_from_source(source)
|
||||
|
||||
def _update_from_source(self, source) -> None:
|
||||
@@ -816,12 +829,17 @@ class GradientColorStripStream(ColorStripStream):
|
||||
|
||||
@property
|
||||
def target_fps(self) -> int:
|
||||
return 30
|
||||
return self._fps
|
||||
|
||||
@property
|
||||
def led_count(self) -> int:
|
||||
return self._led_count
|
||||
|
||||
def set_capture_fps(self, fps: int) -> None:
|
||||
"""Update animation loop rate. Thread-safe (read atomically by the loop)."""
|
||||
fps = max(1, min(90, fps))
|
||||
self._fps = fps
|
||||
|
||||
def start(self) -> None:
|
||||
if self._running:
|
||||
return
|
||||
@@ -859,12 +877,11 @@ class GradientColorStripStream(ColorStripStream):
|
||||
logger.info("GradientColorStripStream params updated in-place")
|
||||
|
||||
def _animate_loop(self) -> None:
|
||||
"""Background thread: apply animation effects at ~30 fps when animation is active.
|
||||
"""Background thread: apply animation effects at target fps when animation is active.
|
||||
|
||||
Uses double-buffered output arrays plus a uint16 scratch buffer for
|
||||
integer-math brightness scaling, avoiding per-frame numpy allocations.
|
||||
"""
|
||||
frame_time = 1.0 / 30
|
||||
_cached_base: Optional[np.ndarray] = None
|
||||
_cached_n: int = 0
|
||||
_cached_stops: Optional[list] = None
|
||||
@@ -876,6 +893,7 @@ class GradientColorStripStream(ColorStripStream):
|
||||
with high_resolution_timer():
|
||||
while self._running:
|
||||
loop_start = time.perf_counter()
|
||||
frame_time = 1.0 / self._fps
|
||||
anim = self._animation
|
||||
if anim and anim.get("enabled"):
|
||||
speed = float(anim.get("speed", 1.0))
|
||||
|
||||
Reference in New Issue
Block a user