Show max FPS hint in target editor, fix gradient sharing for multi-target

- Add dynamic "Hardware max ≈ N fps" recommendation below FPS slider,
  computed from LED count (WLED: protocol timing) or baud rate (serial).
  Reuses shared _computeMaxFps from devices.js with named constants.
- Fix gradient looking different across targets sharing the same stream:
  configure() now uses max LED count across all consumers; _fit_to_device
  uses np.interp linear interpolation instead of truncate/tile.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-22 01:27:57 +03:00
parent 27575930b8
commit 1d5f542603
7 changed files with 66 additions and 20 deletions

View File

@@ -819,13 +819,16 @@ class GradientColorStripStream(ColorStripStream):
def configure(self, device_led_count: int) -> None:
"""Size to device LED count when led_count was 0 (auto-size).
Only takes effect when the source was configured with led_count==0.
Silently ignored when an explicit led_count was set.
When multiple targets share this stream, uses the maximum LED count
so the gradient covers enough resolution for all consumers. Targets
with fewer LEDs get a properly resampled gradient via _fit_to_device.
"""
if self._auto_size and device_led_count > 0 and device_led_count != self._led_count:
self._led_count = device_led_count
self._rebuild_colors()
logger.debug(f"GradientColorStripStream auto-sized to {device_led_count} LEDs")
if self._auto_size and device_led_count > 0:
new_count = max(self._led_count, device_led_count)
if new_count != self._led_count:
self._led_count = new_count
self._rebuild_colors()
logger.debug(f"GradientColorStripStream auto-sized to {new_count} LEDs")
@property
def target_fps(self) -> int:

View File

@@ -382,19 +382,23 @@ class WledTargetProcessor(TargetProcessor):
@staticmethod
def _fit_to_device(colors: np.ndarray, device_led_count: int) -> np.ndarray:
"""Truncate or repeat colors to match the target device LED count.
"""Resample colors to match the target device LED count.
Shared streams may be sized to a different consumer's LED count.
This ensures each target only sends what its device actually needs.
Uses linear interpolation so gradients look correct regardless of
source/target LED count mismatch (shared streams may be sized to a
different consumer's LED count).
"""
n = len(colors)
if n == device_led_count or device_led_count <= 0:
return colors
if n > device_led_count:
return colors[:device_led_count]
# Tile to fill (rare — usually streams are >= device count)
reps = (device_led_count + n - 1) // n
return np.tile(colors, (reps, 1))[:device_led_count]
# Linear interpolation — preserves gradient appearance at any size
src_x = np.linspace(0, 1, n)
dst_x = np.linspace(0, 1, device_led_count)
result = np.column_stack([
np.interp(dst_x, src_x, colors[:, ch]).astype(np.uint8)
for ch in range(colors.shape[1])
])
return result
async def _processing_loop(self) -> None:
"""Main processing loop — poll ColorStripStream → apply brightness → send."""