Move FPS from color strip source to target; dynamic capture rate

FPS is a consumption property (how fast to send to a device), not a
production property. Two targets sharing the same source may need
different FPS. This moves the fps field from PictureColorStripSource
to WledPictureTarget across the full stack.

The capture stream now auto-adjusts its rate to max(all connected
target FPS values) via ColorStripStreamManager tracking per-consumer
FPS. UI updates: FPS slider in target editor, FPS badge on target
cards, LED count repositioned in CSS editor, consistent speed icons.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-21 03:46:08 +03:00
parent 1204676c30
commit 1f6c913343
14 changed files with 126 additions and 57 deletions

View File

@@ -59,7 +59,6 @@ def _css_to_response(source, overlay_active: bool = False) -> ColorStripSourceRe
name=source.name,
source_type=source.source_type,
picture_source_id=getattr(source, "picture_source_id", None),
fps=getattr(source, "fps", None),
brightness=getattr(source, "brightness", None),
saturation=getattr(source, "saturation", None),
gamma=getattr(source, "gamma", None),
@@ -127,7 +126,6 @@ async def create_color_strip_source(
name=data.name,
source_type=data.source_type,
picture_source_id=data.picture_source_id,
fps=data.fps,
brightness=data.brightness,
saturation=data.saturation,
gamma=data.gamma,
@@ -187,7 +185,6 @@ async def update_color_strip_source(
source_id=source_id,
name=data.name,
picture_source_id=data.picture_source_id,
fps=data.fps,
brightness=data.brightness,
saturation=data.saturation,
gamma=data.gamma,

View File

@@ -94,6 +94,7 @@ def _target_to_response(target) -> PictureTargetResponse:
target_type=target.target_type,
device_id=target.device_id,
color_strip_source_id=target.color_strip_source_id,
fps=target.fps,
standby_interval=target.standby_interval,
state_check_interval=target.state_check_interval,
description=target.description,
@@ -148,6 +149,7 @@ async def create_target(
target_type=data.target_type,
device_id=data.device_id,
color_strip_source_id=data.color_strip_source_id,
fps=data.fps,
standby_interval=data.standby_interval,
state_check_interval=data.state_check_interval,
picture_source_id=data.picture_source_id,
@@ -243,6 +245,7 @@ async def update_target(
name=data.name,
device_id=data.device_id,
color_strip_source_id=data.color_strip_source_id,
fps=data.fps,
standby_interval=data.standby_interval,
state_check_interval=data.state_check_interval,
picture_source_id=data.picture_source_id,
@@ -254,7 +257,8 @@ async def update_target(
try:
target.sync_with_manager(
manager,
settings_changed=(data.standby_interval is not None or
settings_changed=(data.fps is not None or
data.standby_interval is not None or
data.state_check_interval is not None or
data.key_colors_settings is not None),
source_changed=data.color_strip_source_id is not None,

View File

@@ -34,7 +34,6 @@ class ColorStripSourceCreate(BaseModel):
source_type: Literal["picture", "static", "gradient", "color_cycle"] = Field(default="picture", description="Source type")
# picture-type fields
picture_source_id: str = Field(default="", description="Picture source ID (for picture type)")
fps: int = Field(default=30, description="Target frames per second", ge=10, le=90)
brightness: float = Field(default=1.0, description="Brightness multiplier (0.0-2.0)", ge=0.0, le=2.0)
saturation: float = Field(default=1.0, description="Saturation (0.0=grayscale, 1.0=unchanged, 2.0=double)", ge=0.0, le=2.0)
gamma: float = Field(default=1.0, description="Gamma correction (1.0=none, <1=brighter, >1=darker mids)", ge=0.1, le=3.0)
@@ -61,7 +60,6 @@ class ColorStripSourceUpdate(BaseModel):
name: Optional[str] = Field(None, description="Source name", min_length=1, max_length=100)
# picture-type fields
picture_source_id: Optional[str] = Field(None, description="Picture source ID")
fps: Optional[int] = Field(None, description="Target FPS", ge=10, le=90)
brightness: Optional[float] = Field(None, description="Brightness multiplier (0.0-2.0)", ge=0.0, le=2.0)
saturation: Optional[float] = Field(None, description="Saturation (0.0-2.0)", ge=0.0, le=2.0)
gamma: Optional[float] = Field(None, description="Gamma correction (0.1-3.0)", ge=0.1, le=3.0)
@@ -90,7 +88,6 @@ class ColorStripSourceResponse(BaseModel):
source_type: str = Field(description="Source type")
# picture-type fields
picture_source_id: Optional[str] = Field(None, description="Picture source ID")
fps: Optional[int] = Field(None, description="Target FPS")
brightness: Optional[float] = Field(None, description="Brightness multiplier")
saturation: Optional[float] = Field(None, description="Saturation")
gamma: Optional[float] = Field(None, description="Gamma correction")

View File

@@ -53,6 +53,7 @@ class PictureTargetCreate(BaseModel):
# LED target fields
device_id: str = Field(default="", description="LED device ID")
color_strip_source_id: str = Field(default="", description="Color strip source ID")
fps: int = Field(default=30, ge=10, le=90, description="Target send FPS (10-90)")
standby_interval: float = Field(default=1.0, description="Keepalive send interval when screen is static (0.5-5.0s)", ge=0.5, le=5.0)
state_check_interval: int = Field(default=DEFAULT_STATE_CHECK_INTERVAL, description="Device health check interval (5-600s)", ge=5, le=600)
# KC target fields
@@ -68,6 +69,7 @@ class PictureTargetUpdate(BaseModel):
# LED target fields
device_id: Optional[str] = Field(None, description="LED device ID")
color_strip_source_id: Optional[str] = Field(None, description="Color strip source ID")
fps: Optional[int] = Field(None, ge=10, le=90, description="Target send FPS (10-90)")
standby_interval: Optional[float] = Field(None, description="Keepalive interval (0.5-5.0s)", ge=0.5, le=5.0)
state_check_interval: Optional[int] = Field(None, description="Health check interval (5-600s)", ge=5, le=600)
# KC target fields
@@ -85,6 +87,7 @@ class PictureTargetResponse(BaseModel):
# LED target fields
device_id: str = Field(default="", description="LED device ID")
color_strip_source_id: str = Field(default="", description="Color strip source ID")
fps: Optional[int] = Field(None, description="Target send FPS")
standby_interval: float = Field(default=1.0, description="Keepalive interval (s)")
state_check_interval: int = Field(default=DEFAULT_STATE_CHECK_INTERVAL, description="Health check interval (s)")
# KC target fields

View File

@@ -135,7 +135,7 @@ class PictureColorStripStream(ColorStripStream):
from wled_controller.storage.color_strip_source import PictureColorStripSource
self._live_stream = live_stream
self._fps: int = source.fps
self._fps: int = 30 # internal capture rate (send FPS is on the target)
self._smoothing: float = source.smoothing
self._brightness: float = source.brightness
self._saturation: float = source.saturation
@@ -217,6 +217,14 @@ class PictureColorStripStream(ColorStripStream):
def get_last_timing(self) -> dict:
return dict(self._last_timing)
def set_capture_fps(self, fps: int) -> None:
"""Update the internal capture rate. Thread-safe (read atomically by the loop)."""
fps = max(10, min(90, fps))
if fps != self._fps:
self._fps = fps
self._interp_duration = 1.0 / fps
logger.info(f"PictureColorStripStream capture FPS set to {fps}")
def update_source(self, source) -> None:
"""Hot-update processing parameters. Thread-safe for scalar params.
@@ -227,7 +235,6 @@ class PictureColorStripStream(ColorStripStream):
if not isinstance(source, PictureColorStripSource):
return
self._fps = source.fps
self._smoothing = source.smoothing
self._brightness = source.brightness
self._saturation = source.saturation

View File

@@ -32,6 +32,12 @@ class _ColorStripEntry:
ref_count: int
# ID of the picture source whose LiveStream we acquired (for release)
picture_source_id: str
# Per-consumer target FPS values (target_id → fps)
target_fps: Dict[str, int] = None
def __post_init__(self):
if self.target_fps is None:
self.target_fps = {}
class ColorStripStreamManager:
@@ -213,6 +219,36 @@ class ColorStripStreamManager:
logger.info(f"Updated running color strip stream {css_id}")
def notify_target_fps(self, css_id: str, target_id: str, fps: int) -> None:
"""Register or update a consumer's target FPS.
Recalculates the capture rate for PictureColorStripStreams as
max(all consumer FPS values). Non-picture streams are unaffected.
"""
entry = self._streams.get(css_id)
if not entry:
return
entry.target_fps[target_id] = fps
self._recalc_capture_fps(entry)
def remove_target_fps(self, css_id: str, target_id: str) -> None:
"""Unregister a consumer's target FPS (e.g. on stop)."""
entry = self._streams.get(css_id)
if not entry:
return
entry.target_fps.pop(target_id, None)
self._recalc_capture_fps(entry)
def _recalc_capture_fps(self, entry: _ColorStripEntry) -> None:
"""Push max(consumer FPS) to the stream if it supports set_capture_fps."""
if not hasattr(entry.stream, "set_capture_fps"):
return
if entry.target_fps:
new_fps = max(entry.target_fps.values())
else:
new_fps = 30 # default when no consumers
entry.stream.set_capture_fps(new_fps)
def release_all(self) -> None:
"""Stop and remove all managed color strip streams. Called on shutdown."""
css_ids = list(self._streams.keys())

View File

@@ -272,6 +272,7 @@ class ProcessorManager:
target_id: str,
device_id: str,
color_strip_source_id: str = "",
fps: int = 30,
standby_interval: float = 1.0,
state_check_interval: int = DEFAULT_STATE_CHECK_INTERVAL,
):
@@ -285,6 +286,7 @@ class ProcessorManager:
target_id=target_id,
device_id=device_id,
color_strip_source_id=color_strip_source_id,
fps=fps,
standby_interval=standby_interval,
state_check_interval=state_check_interval,
ctx=self._build_context(),

View File

@@ -40,6 +40,7 @@ class WledTargetProcessor(TargetProcessor):
target_id: str,
device_id: str,
color_strip_source_id: str,
fps: int,
standby_interval: float,
state_check_interval: int,
ctx: TargetContext,
@@ -47,6 +48,7 @@ class WledTargetProcessor(TargetProcessor):
super().__init__(target_id, ctx)
self._device_id = device_id
self._color_strip_source_id = color_strip_source_id
self._target_fps = fps if fps > 0 else 30
self._standby_interval = standby_interval
self._state_check_interval = state_check_interval
@@ -58,7 +60,6 @@ class WledTargetProcessor(TargetProcessor):
# Resolved stream metadata (set once stream is acquired)
self._resolved_display_index: Optional[int] = None
self._resolved_target_fps: Optional[int] = None
# ----- Properties -----
@@ -114,7 +115,6 @@ class WledTargetProcessor(TargetProcessor):
stream = await asyncio.to_thread(css_manager.acquire, self._color_strip_source_id)
self._color_strip_stream = stream
self._resolved_display_index = stream.display_index
self._resolved_target_fps = stream.target_fps
# For auto-sized static/gradient/color_cycle streams (led_count == 0), size to device LED count
from wled_controller.core.processing.color_strip_stream import (
@@ -125,10 +125,15 @@ class WledTargetProcessor(TargetProcessor):
if isinstance(stream, (StaticColorStripStream, GradientColorStripStream, ColorCycleColorStripStream)) and device_info.led_count > 0:
stream.configure(device_info.led_count)
# Notify stream manager of our target FPS so it can adjust capture rate
css_manager.notify_target_fps(
self._color_strip_source_id, self._target_id, self._target_fps
)
logger.info(
f"Acquired color strip stream for target {self._target_id} "
f"(css={self._color_strip_source_id}, display={self._resolved_display_index}, "
f"fps={self._resolved_target_fps})"
f"fps={self._target_fps})"
)
except Exception as e:
logger.error(f"Failed to acquire color strip stream for target {self._target_id}: {e}")
@@ -176,6 +181,7 @@ class WledTargetProcessor(TargetProcessor):
css_manager = self._ctx.color_strip_stream_manager
if css_manager and self._color_strip_source_id:
try:
css_manager.remove_target_fps(self._color_strip_source_id, self._target_id)
await asyncio.to_thread(css_manager.release, self._color_strip_source_id)
except Exception as e:
logger.warning(f"Error releasing color strip stream for {self._target_id}: {e}")
@@ -189,6 +195,14 @@ class WledTargetProcessor(TargetProcessor):
def update_settings(self, settings: dict) -> None:
"""Update target-specific timing settings."""
if isinstance(settings, dict):
if "fps" in settings:
self._target_fps = settings["fps"] if settings["fps"] > 0 else 30
# Notify stream manager so capture rate adjusts to max of all consumers
css_manager = self._ctx.color_strip_stream_manager
if css_manager and self._color_strip_source_id and self._is_running:
css_manager.notify_target_fps(
self._color_strip_source_id, self._target_id, self._target_fps
)
if "standby_interval" in settings:
self._standby_interval = settings["standby_interval"]
if "state_check_interval" in settings:
@@ -213,11 +227,12 @@ class WledTargetProcessor(TargetProcessor):
old_id = self._color_strip_source_id
try:
new_stream = css_manager.acquire(color_strip_source_id)
css_manager.remove_target_fps(old_id, self._target_id)
css_manager.release(old_id)
self._color_strip_stream = new_stream
self._resolved_display_index = new_stream.display_index
self._resolved_target_fps = new_stream.target_fps
self._color_strip_source_id = color_strip_source_id
css_manager.notify_target_fps(color_strip_source_id, self._target_id, self._target_fps)
logger.info(f"Swapped color strip source for {self._target_id}: {old_id}{color_strip_source_id}")
except Exception as e:
logger.error(f"Failed to swap color strip source for {self._target_id}: {e}")
@@ -234,7 +249,7 @@ class WledTargetProcessor(TargetProcessor):
def get_state(self) -> dict:
metrics = self._metrics
fps_target = self._color_strip_stream.target_fps if self._color_strip_stream else None
fps_target = self._target_fps
# Pull per-stage timing from the CSS stream (runs in a background thread)
css_timing: dict = {}
@@ -277,7 +292,7 @@ class WledTargetProcessor(TargetProcessor):
def get_metrics(self) -> dict:
metrics = self._metrics
fps_target = self._color_strip_stream.target_fps if self._color_strip_stream else None
fps_target = self._target_fps
uptime_seconds = 0.0
if metrics.start_time and self._is_running:
uptime_seconds = (datetime.utcnow() - metrics.start_time).total_seconds()
@@ -406,15 +421,15 @@ class WledTargetProcessor(TargetProcessor):
logger.info(
f"Processing loop started for target {self._target_id} "
f"(display={self._resolved_display_index}, fps={self._resolved_target_fps})"
f"(display={self._resolved_display_index}, fps={self._target_fps})"
)
try:
with high_resolution_timer():
while self._is_running:
loop_start = now = time.perf_counter()
# Re-read target_fps each tick so hot-updates to the CSS source take effect
target_fps = stream.target_fps if stream.target_fps > 0 else 30
# Re-read target_fps each tick so hot-updates take effect immediately
target_fps = self._target_fps if self._target_fps > 0 else 30
frame_time = 1.0 / target_fps
# Re-fetch device info every ~30 iterations instead of every

View File

@@ -18,7 +18,6 @@ class CSSEditorModal extends Modal {
name: document.getElementById('css-editor-name').value,
type,
picture_source: document.getElementById('css-editor-picture-source').value,
fps: document.getElementById('css-editor-fps').value,
interpolation: document.getElementById('css-editor-interpolation').value,
smoothing: document.getElementById('css-editor-smoothing').value,
brightness: document.getElementById('css-editor-brightness').value,
@@ -26,7 +25,7 @@ class CSSEditorModal extends Modal {
gamma: document.getElementById('css-editor-gamma').value,
color: document.getElementById('css-editor-color').value,
frame_interpolation: document.getElementById('css-editor-frame-interpolation').checked,
led_count: (type === 'static' || type === 'gradient' || type === 'color_cycle') ? '0' : document.getElementById('css-editor-led-count').value,
led_count: document.getElementById('css-editor-led-count').value,
gradient_stops: type === 'gradient' ? JSON.stringify(_gradientStops) : '[]',
animation_enabled: document.getElementById('css-editor-animation-enabled').checked,
animation_type: document.getElementById('css-editor-animation-type').value,
@@ -47,9 +46,6 @@ export function onCSSTypeChange() {
document.getElementById('css-editor-static-section').style.display = type === 'static' ? '' : 'none';
document.getElementById('css-editor-color-cycle-section').style.display = type === 'color_cycle' ? '' : 'none';
document.getElementById('css-editor-gradient-section').style.display = type === 'gradient' ? '' : 'none';
// LED count is only meaningful for picture sources; static/gradient/color_cycle auto-size from device
document.getElementById('css-editor-led-count-group').style.display =
(type === 'static' || type === 'gradient' || type === 'color_cycle') ? 'none' : '';
// Animation section — shown for static/gradient only (color_cycle is always animating)
const animSection = document.getElementById('css-editor-animation-section');
@@ -205,7 +201,7 @@ export function createColorStripCard(source, pictureSourceMap) {
).join('');
propsHtml = `
<span class="stream-card-prop">${swatches}</span>
<span class="stream-card-prop" title="${t('color_strip.color_cycle.speed')}">🔄 ${(source.cycle_speed || 1.0).toFixed(1)}×</span>
<span class="stream-card-prop" title="${t('color_strip.color_cycle.speed')}"> ${(source.cycle_speed || 1.0).toFixed(1)}×</span>
${source.led_count ? `<span class="stream-card-prop" title="${t('color_strip.leds')}">💡 ${source.led_count}</span>` : ''}
`;
} else if (isGradient) {
@@ -227,6 +223,7 @@ export function createColorStripCard(source, pictureSourceMap) {
propsHtml = `
${cssGradient ? `<span style="flex:1 1 100%;height:12px;background:${cssGradient};border-radius:3px;border:1px solid rgba(128,128,128,0.3)"></span>` : ''}
<span class="stream-card-prop">🎨 ${stops.length} ${t('color_strip.gradient.stops_count')}</span>
${source.led_count ? `<span class="stream-card-prop" title="${t('color_strip.leds')}">💡 ${source.led_count}</span>` : ''}
${animBadge}
`;
} else {
@@ -237,9 +234,8 @@ export function createColorStripCard(source, pictureSourceMap) {
const calLeds = (cal.leds_top || 0) + (cal.leds_right || 0) + (cal.leds_bottom || 0) + (cal.leds_left || 0);
const ledCount = (source.led_count > 0) ? source.led_count : calLeds;
propsHtml = `
<span class="stream-card-prop" title="${t('color_strip.fps')}">⚡ ${source.fps || 30} fps</span>
${ledCount ? `<span class="stream-card-prop" title="${t('color_strip.leds')}">💡 ${ledCount}</span>` : ''}
<span class="stream-card-prop stream-card-prop-full" title="${t('color_strip.picture_source')}">📺 ${escapeHtml(srcName)}</span>
${ledCount ? `<span class="stream-card-prop" title="${t('color_strip.leds')}">💡 ${ledCount}</span>` : ''}
`;
}
@@ -311,10 +307,6 @@ export async function showCSSEditor(cssId = null) {
} else {
sourceSelect.value = css.picture_source_id || '';
const fps = css.fps ?? 30;
document.getElementById('css-editor-fps').value = fps;
document.getElementById('css-editor-fps-value').textContent = fps;
document.getElementById('css-editor-interpolation').value = css.interpolation_mode || 'average';
const smoothing = css.smoothing ?? 0.3;
@@ -343,8 +335,6 @@ export async function showCSSEditor(cssId = null) {
document.getElementById('css-editor-name').value = '';
document.getElementById('css-editor-type').value = 'picture';
onCSSTypeChange();
document.getElementById('css-editor-fps').value = 30;
document.getElementById('css-editor-fps-value').textContent = '30';
document.getElementById('css-editor-interpolation').value = 'average';
document.getElementById('css-editor-smoothing').value = 0.3;
document.getElementById('css-editor-smoothing-value').textContent = '0.30';
@@ -432,7 +422,6 @@ export async function saveCSSEditor() {
payload = {
name,
picture_source_id: document.getElementById('css-editor-picture-source').value,
fps: parseInt(document.getElementById('css-editor-fps').value) || 30,
interpolation_mode: document.getElementById('css-editor-interpolation').value,
smoothing: parseFloat(document.getElementById('css-editor-smoothing').value),
brightness: parseFloat(document.getElementById('css-editor-brightness').value),

View File

@@ -72,6 +72,7 @@ class TargetEditorModal extends Modal {
name: document.getElementById('target-editor-name').value,
device: document.getElementById('target-editor-device').value,
css: document.getElementById('target-editor-css').value,
fps: document.getElementById('target-editor-fps').value,
standby_interval: document.getElementById('target-editor-standby-interval').value,
};
}
@@ -146,6 +147,9 @@ export async function showTargetEditor(targetId = null) {
document.getElementById('target-editor-name').value = target.name;
deviceSelect.value = target.device_id || '';
cssSelect.value = target.color_strip_source_id || '';
const fps = target.fps ?? 30;
document.getElementById('target-editor-fps').value = fps;
document.getElementById('target-editor-fps-value').textContent = fps;
document.getElementById('target-editor-standby-interval').value = target.standby_interval ?? 1.0;
document.getElementById('target-editor-standby-interval-value').textContent = target.standby_interval ?? 1.0;
document.getElementById('target-editor-title').textContent = t('targets.edit');
@@ -153,6 +157,8 @@ export async function showTargetEditor(targetId = null) {
// Creating new target — first option is selected by default
document.getElementById('target-editor-id').value = '';
document.getElementById('target-editor-name').value = '';
document.getElementById('target-editor-fps').value = 30;
document.getElementById('target-editor-fps-value').textContent = '30';
document.getElementById('target-editor-standby-interval').value = 1.0;
document.getElementById('target-editor-standby-interval-value').textContent = '1.0';
document.getElementById('target-editor-title').textContent = t('targets.add');
@@ -203,10 +209,13 @@ export async function saveTargetEditor() {
return;
}
const fps = parseInt(document.getElementById('target-editor-fps').value) || 30;
const payload = {
name,
device_id: deviceId,
color_strip_source_id: cssId,
fps,
standby_interval: standbyInterval,
};
@@ -508,6 +517,7 @@ export function createTargetCard(target, deviceMap, colorStripSourceMap) {
</div>
<div class="stream-card-props">
<span class="stream-card-prop" title="${t('targets.device')}">💡 ${escapeHtml(deviceName)}</span>
<span class="stream-card-prop" title="${t('targets.fps')}">⚡ ${target.fps || 30} fps</span>
<span class="stream-card-prop stream-card-prop-full" title="${t('targets.color_strip_source')}">🎞️ ${escapeHtml(cssName)}</span>
</div>
<div class="card-content">

View File

@@ -102,6 +102,7 @@ class PictureTargetStore:
target_type: str,
device_id: str = "",
color_strip_source_id: str = "",
fps: int = 30,
standby_interval: float = 1.0,
state_check_interval: int = DEFAULT_STATE_CHECK_INTERVAL,
key_colors_settings: Optional[KeyColorsSettings] = None,
@@ -146,6 +147,7 @@ class PictureTargetStore:
target_type="led",
device_id=device_id,
color_strip_source_id=color_strip_source_id,
fps=fps,
standby_interval=standby_interval,
state_check_interval=state_check_interval,
description=description,
@@ -178,6 +180,7 @@ class PictureTargetStore:
name: Optional[str] = None,
device_id: Optional[str] = None,
color_strip_source_id: Optional[str] = None,
fps: Optional[int] = None,
standby_interval: Optional[float] = None,
state_check_interval: Optional[int] = None,
key_colors_settings: Optional[KeyColorsSettings] = None,
@@ -206,6 +209,7 @@ class PictureTargetStore:
name=name,
device_id=device_id,
color_strip_source_id=color_strip_source_id,
fps=fps,
standby_interval=standby_interval,
state_check_interval=state_check_interval,
key_colors_settings=key_colors_settings,

View File

@@ -13,13 +13,13 @@ DEFAULT_STATE_CHECK_INTERVAL = 30 # seconds
class WledPictureTarget(PictureTarget):
"""LED picture target — pairs an LED device with a ColorStripSource.
The ColorStripSource encapsulates everything needed to produce LED colors
(calibration, color correction, smoothing, fps). The LED target itself only
holds device-specific timing/keepalive settings.
The ColorStripSource produces LED colors (calibration, color correction,
smoothing). The target controls device-specific settings including send FPS.
"""
device_id: str = ""
color_strip_source_id: str = ""
fps: int = 30 # target send FPS (10-90)
standby_interval: float = 1.0 # seconds between keepalive sends when screen is static
state_check_interval: int = DEFAULT_STATE_CHECK_INTERVAL
@@ -34,6 +34,7 @@ class WledPictureTarget(PictureTarget):
target_id=self.id,
device_id=self.device_id,
color_strip_source_id=self.color_strip_source_id,
fps=self.fps,
standby_interval=self.standby_interval,
state_check_interval=self.state_check_interval,
)
@@ -42,6 +43,7 @@ class WledPictureTarget(PictureTarget):
"""Push changed fields to the processor manager."""
if settings_changed:
manager.update_target_settings(self.id, {
"fps": self.fps,
"standby_interval": self.standby_interval,
"state_check_interval": self.state_check_interval,
})
@@ -51,7 +53,7 @@ class WledPictureTarget(PictureTarget):
manager.update_target_device(self.id, self.device_id)
def update_fields(self, *, name=None, device_id=None, color_strip_source_id=None,
standby_interval=None, state_check_interval=None,
fps=None, standby_interval=None, state_check_interval=None,
# Legacy params accepted but ignored to keep base class compat:
picture_source_id=None, settings=None,
key_colors_settings=None, description=None) -> None:
@@ -61,6 +63,8 @@ class WledPictureTarget(PictureTarget):
self.device_id = device_id
if color_strip_source_id is not None:
self.color_strip_source_id = color_strip_source_id
if fps is not None:
self.fps = fps
if standby_interval is not None:
self.standby_interval = standby_interval
if state_check_interval is not None:
@@ -75,6 +79,7 @@ class WledPictureTarget(PictureTarget):
d = super().to_dict()
d["device_id"] = self.device_id
d["color_strip_source_id"] = self.color_strip_source_id
d["fps"] = self.fps
d["standby_interval"] = self.standby_interval
d["state_check_interval"] = self.state_check_interval
return d
@@ -88,6 +93,7 @@ class WledPictureTarget(PictureTarget):
target_type="led",
device_id=data.get("device_id", ""),
color_strip_source_id=data.get("color_strip_source_id", ""),
fps=data.get("fps", 30),
standby_interval=data.get("standby_interval", 1.0),
state_check_interval=data.get("state_check_interval", DEFAULT_STATE_CHECK_INTERVAL),
description=data.get("description"),

View File

@@ -39,19 +39,13 @@
<select id="css-editor-picture-source"></select>
</div>
<div class="form-group">
<div id="css-editor-led-count-group" class="form-group">
<div class="label-row">
<label for="css-editor-fps">
<span data-i18n="color_strip.fps">Target FPS:</span>
<span id="css-editor-fps-value">30</span>
</label>
<label for="css-editor-led-count" data-i18n="color_strip.led_count">LED Count:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.fps.hint">Target frames per second for LED color updates (10-90)</small>
<div class="slider-row">
<input type="range" id="css-editor-fps" min="10" max="90" value="30" oninput="document.getElementById('css-editor-fps-value').textContent = this.value">
<span class="slider-value">fps</span>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.led_count.hint">Total number of LEDs on the strip. Set to 0 to use the sum from calibration. If your strip has LEDs behind the TV that are not mapped to screen edges, set the exact count here and they will be filled with black.</small>
<input type="number" id="css-editor-led-count" min="0" max="1500" step="1" value="0">
</div>
<div class="form-group">
@@ -236,16 +230,6 @@
</details>
</div>
<!-- LED count — picture type only (auto-sized from device for static/gradient) -->
<div id="css-editor-led-count-group" class="form-group">
<div class="label-row">
<label for="css-editor-led-count" data-i18n="color_strip.led_count">LED Count:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.led_count.hint">Total number of LEDs on the strip. Set to 0 to use the sum from calibration. If your strip has LEDs behind the TV that are not mapped to screen edges, set the exact count here and they will be filled with black.</small>
<input type="number" id="css-editor-led-count" min="0" max="1500" step="1" value="0">
</div>
<div id="css-editor-error" class="error-message" style="display: none;"></div>
</form>
</div>

View File

@@ -32,6 +32,21 @@
<select id="target-editor-css"></select>
</div>
<div class="form-group" id="target-editor-fps-group">
<div class="label-row">
<label for="target-editor-fps">
<span data-i18n="targets.fps">Target FPS:</span>
<span id="target-editor-fps-value">30</span>
</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="targets.fps.hint">How many frames per second to send to the device (10-90). Higher values give smoother animations but use more bandwidth.</small>
<div class="slider-row">
<input type="range" id="target-editor-fps" min="10" max="90" value="30" oninput="document.getElementById('target-editor-fps-value').textContent = this.value">
<span class="slider-value">fps</span>
</div>
</div>
<div class="form-group" id="target-editor-standby-group">
<div class="label-row">
<label for="target-editor-standby-interval">