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:
2026-02-22 00:52:19 +03:00
parent ee52e2d98f
commit 2a01c2947a

View File

@@ -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))