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:
@@ -59,7 +59,6 @@ def _css_to_response(source, overlay_active: bool = False) -> ColorStripSourceRe
|
|||||||
name=source.name,
|
name=source.name,
|
||||||
source_type=source.source_type,
|
source_type=source.source_type,
|
||||||
picture_source_id=getattr(source, "picture_source_id", None),
|
picture_source_id=getattr(source, "picture_source_id", None),
|
||||||
fps=getattr(source, "fps", None),
|
|
||||||
brightness=getattr(source, "brightness", None),
|
brightness=getattr(source, "brightness", None),
|
||||||
saturation=getattr(source, "saturation", None),
|
saturation=getattr(source, "saturation", None),
|
||||||
gamma=getattr(source, "gamma", None),
|
gamma=getattr(source, "gamma", None),
|
||||||
@@ -127,7 +126,6 @@ async def create_color_strip_source(
|
|||||||
name=data.name,
|
name=data.name,
|
||||||
source_type=data.source_type,
|
source_type=data.source_type,
|
||||||
picture_source_id=data.picture_source_id,
|
picture_source_id=data.picture_source_id,
|
||||||
fps=data.fps,
|
|
||||||
brightness=data.brightness,
|
brightness=data.brightness,
|
||||||
saturation=data.saturation,
|
saturation=data.saturation,
|
||||||
gamma=data.gamma,
|
gamma=data.gamma,
|
||||||
@@ -187,7 +185,6 @@ async def update_color_strip_source(
|
|||||||
source_id=source_id,
|
source_id=source_id,
|
||||||
name=data.name,
|
name=data.name,
|
||||||
picture_source_id=data.picture_source_id,
|
picture_source_id=data.picture_source_id,
|
||||||
fps=data.fps,
|
|
||||||
brightness=data.brightness,
|
brightness=data.brightness,
|
||||||
saturation=data.saturation,
|
saturation=data.saturation,
|
||||||
gamma=data.gamma,
|
gamma=data.gamma,
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ def _target_to_response(target) -> PictureTargetResponse:
|
|||||||
target_type=target.target_type,
|
target_type=target.target_type,
|
||||||
device_id=target.device_id,
|
device_id=target.device_id,
|
||||||
color_strip_source_id=target.color_strip_source_id,
|
color_strip_source_id=target.color_strip_source_id,
|
||||||
|
fps=target.fps,
|
||||||
standby_interval=target.standby_interval,
|
standby_interval=target.standby_interval,
|
||||||
state_check_interval=target.state_check_interval,
|
state_check_interval=target.state_check_interval,
|
||||||
description=target.description,
|
description=target.description,
|
||||||
@@ -148,6 +149,7 @@ async def create_target(
|
|||||||
target_type=data.target_type,
|
target_type=data.target_type,
|
||||||
device_id=data.device_id,
|
device_id=data.device_id,
|
||||||
color_strip_source_id=data.color_strip_source_id,
|
color_strip_source_id=data.color_strip_source_id,
|
||||||
|
fps=data.fps,
|
||||||
standby_interval=data.standby_interval,
|
standby_interval=data.standby_interval,
|
||||||
state_check_interval=data.state_check_interval,
|
state_check_interval=data.state_check_interval,
|
||||||
picture_source_id=data.picture_source_id,
|
picture_source_id=data.picture_source_id,
|
||||||
@@ -243,6 +245,7 @@ async def update_target(
|
|||||||
name=data.name,
|
name=data.name,
|
||||||
device_id=data.device_id,
|
device_id=data.device_id,
|
||||||
color_strip_source_id=data.color_strip_source_id,
|
color_strip_source_id=data.color_strip_source_id,
|
||||||
|
fps=data.fps,
|
||||||
standby_interval=data.standby_interval,
|
standby_interval=data.standby_interval,
|
||||||
state_check_interval=data.state_check_interval,
|
state_check_interval=data.state_check_interval,
|
||||||
picture_source_id=data.picture_source_id,
|
picture_source_id=data.picture_source_id,
|
||||||
@@ -254,7 +257,8 @@ async def update_target(
|
|||||||
try:
|
try:
|
||||||
target.sync_with_manager(
|
target.sync_with_manager(
|
||||||
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.state_check_interval is not None or
|
||||||
data.key_colors_settings is not None),
|
data.key_colors_settings is not None),
|
||||||
source_changed=data.color_strip_source_id is not None,
|
source_changed=data.color_strip_source_id is not None,
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ class ColorStripSourceCreate(BaseModel):
|
|||||||
source_type: Literal["picture", "static", "gradient", "color_cycle"] = Field(default="picture", description="Source type")
|
source_type: Literal["picture", "static", "gradient", "color_cycle"] = Field(default="picture", description="Source type")
|
||||||
# picture-type fields
|
# picture-type fields
|
||||||
picture_source_id: str = Field(default="", description="Picture source ID (for picture type)")
|
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)
|
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)
|
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)
|
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)
|
name: Optional[str] = Field(None, description="Source name", min_length=1, max_length=100)
|
||||||
# picture-type fields
|
# picture-type fields
|
||||||
picture_source_id: Optional[str] = Field(None, description="Picture source ID")
|
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)
|
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)
|
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)
|
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")
|
source_type: str = Field(description="Source type")
|
||||||
# picture-type fields
|
# picture-type fields
|
||||||
picture_source_id: Optional[str] = Field(None, description="Picture source ID")
|
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")
|
brightness: Optional[float] = Field(None, description="Brightness multiplier")
|
||||||
saturation: Optional[float] = Field(None, description="Saturation")
|
saturation: Optional[float] = Field(None, description="Saturation")
|
||||||
gamma: Optional[float] = Field(None, description="Gamma correction")
|
gamma: Optional[float] = Field(None, description="Gamma correction")
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ class PictureTargetCreate(BaseModel):
|
|||||||
# LED target fields
|
# LED target fields
|
||||||
device_id: str = Field(default="", description="LED device ID")
|
device_id: str = Field(default="", description="LED device ID")
|
||||||
color_strip_source_id: str = Field(default="", description="Color strip source 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)
|
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)
|
state_check_interval: int = Field(default=DEFAULT_STATE_CHECK_INTERVAL, description="Device health check interval (5-600s)", ge=5, le=600)
|
||||||
# KC target fields
|
# KC target fields
|
||||||
@@ -68,6 +69,7 @@ class PictureTargetUpdate(BaseModel):
|
|||||||
# LED target fields
|
# LED target fields
|
||||||
device_id: Optional[str] = Field(None, description="LED device ID")
|
device_id: Optional[str] = Field(None, description="LED device ID")
|
||||||
color_strip_source_id: Optional[str] = Field(None, description="Color strip source 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)
|
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)
|
state_check_interval: Optional[int] = Field(None, description="Health check interval (5-600s)", ge=5, le=600)
|
||||||
# KC target fields
|
# KC target fields
|
||||||
@@ -85,6 +87,7 @@ class PictureTargetResponse(BaseModel):
|
|||||||
# LED target fields
|
# LED target fields
|
||||||
device_id: str = Field(default="", description="LED device ID")
|
device_id: str = Field(default="", description="LED device ID")
|
||||||
color_strip_source_id: str = Field(default="", description="Color strip source 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)")
|
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)")
|
state_check_interval: int = Field(default=DEFAULT_STATE_CHECK_INTERVAL, description="Health check interval (s)")
|
||||||
# KC target fields
|
# KC target fields
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ class PictureColorStripStream(ColorStripStream):
|
|||||||
from wled_controller.storage.color_strip_source import PictureColorStripSource
|
from wled_controller.storage.color_strip_source import PictureColorStripSource
|
||||||
|
|
||||||
self._live_stream = live_stream
|
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._smoothing: float = source.smoothing
|
||||||
self._brightness: float = source.brightness
|
self._brightness: float = source.brightness
|
||||||
self._saturation: float = source.saturation
|
self._saturation: float = source.saturation
|
||||||
@@ -217,6 +217,14 @@ class PictureColorStripStream(ColorStripStream):
|
|||||||
def get_last_timing(self) -> dict:
|
def get_last_timing(self) -> dict:
|
||||||
return dict(self._last_timing)
|
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:
|
def update_source(self, source) -> None:
|
||||||
"""Hot-update processing parameters. Thread-safe for scalar params.
|
"""Hot-update processing parameters. Thread-safe for scalar params.
|
||||||
|
|
||||||
@@ -227,7 +235,6 @@ class PictureColorStripStream(ColorStripStream):
|
|||||||
if not isinstance(source, PictureColorStripSource):
|
if not isinstance(source, PictureColorStripSource):
|
||||||
return
|
return
|
||||||
|
|
||||||
self._fps = source.fps
|
|
||||||
self._smoothing = source.smoothing
|
self._smoothing = source.smoothing
|
||||||
self._brightness = source.brightness
|
self._brightness = source.brightness
|
||||||
self._saturation = source.saturation
|
self._saturation = source.saturation
|
||||||
|
|||||||
@@ -32,6 +32,12 @@ class _ColorStripEntry:
|
|||||||
ref_count: int
|
ref_count: int
|
||||||
# ID of the picture source whose LiveStream we acquired (for release)
|
# ID of the picture source whose LiveStream we acquired (for release)
|
||||||
picture_source_id: str
|
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:
|
class ColorStripStreamManager:
|
||||||
@@ -213,6 +219,36 @@ class ColorStripStreamManager:
|
|||||||
|
|
||||||
logger.info(f"Updated running color strip stream {css_id}")
|
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:
|
def release_all(self) -> None:
|
||||||
"""Stop and remove all managed color strip streams. Called on shutdown."""
|
"""Stop and remove all managed color strip streams. Called on shutdown."""
|
||||||
css_ids = list(self._streams.keys())
|
css_ids = list(self._streams.keys())
|
||||||
|
|||||||
@@ -272,6 +272,7 @@ class ProcessorManager:
|
|||||||
target_id: str,
|
target_id: str,
|
||||||
device_id: str,
|
device_id: str,
|
||||||
color_strip_source_id: str = "",
|
color_strip_source_id: str = "",
|
||||||
|
fps: int = 30,
|
||||||
standby_interval: float = 1.0,
|
standby_interval: float = 1.0,
|
||||||
state_check_interval: int = DEFAULT_STATE_CHECK_INTERVAL,
|
state_check_interval: int = DEFAULT_STATE_CHECK_INTERVAL,
|
||||||
):
|
):
|
||||||
@@ -285,6 +286,7 @@ class ProcessorManager:
|
|||||||
target_id=target_id,
|
target_id=target_id,
|
||||||
device_id=device_id,
|
device_id=device_id,
|
||||||
color_strip_source_id=color_strip_source_id,
|
color_strip_source_id=color_strip_source_id,
|
||||||
|
fps=fps,
|
||||||
standby_interval=standby_interval,
|
standby_interval=standby_interval,
|
||||||
state_check_interval=state_check_interval,
|
state_check_interval=state_check_interval,
|
||||||
ctx=self._build_context(),
|
ctx=self._build_context(),
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ class WledTargetProcessor(TargetProcessor):
|
|||||||
target_id: str,
|
target_id: str,
|
||||||
device_id: str,
|
device_id: str,
|
||||||
color_strip_source_id: str,
|
color_strip_source_id: str,
|
||||||
|
fps: int,
|
||||||
standby_interval: float,
|
standby_interval: float,
|
||||||
state_check_interval: int,
|
state_check_interval: int,
|
||||||
ctx: TargetContext,
|
ctx: TargetContext,
|
||||||
@@ -47,6 +48,7 @@ class WledTargetProcessor(TargetProcessor):
|
|||||||
super().__init__(target_id, ctx)
|
super().__init__(target_id, ctx)
|
||||||
self._device_id = device_id
|
self._device_id = device_id
|
||||||
self._color_strip_source_id = color_strip_source_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._standby_interval = standby_interval
|
||||||
self._state_check_interval = state_check_interval
|
self._state_check_interval = state_check_interval
|
||||||
|
|
||||||
@@ -58,7 +60,6 @@ class WledTargetProcessor(TargetProcessor):
|
|||||||
|
|
||||||
# Resolved stream metadata (set once stream is acquired)
|
# Resolved stream metadata (set once stream is acquired)
|
||||||
self._resolved_display_index: Optional[int] = None
|
self._resolved_display_index: Optional[int] = None
|
||||||
self._resolved_target_fps: Optional[int] = None
|
|
||||||
|
|
||||||
# ----- Properties -----
|
# ----- Properties -----
|
||||||
|
|
||||||
@@ -114,7 +115,6 @@ class WledTargetProcessor(TargetProcessor):
|
|||||||
stream = await asyncio.to_thread(css_manager.acquire, self._color_strip_source_id)
|
stream = await asyncio.to_thread(css_manager.acquire, self._color_strip_source_id)
|
||||||
self._color_strip_stream = stream
|
self._color_strip_stream = stream
|
||||||
self._resolved_display_index = stream.display_index
|
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
|
# 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 (
|
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:
|
if isinstance(stream, (StaticColorStripStream, GradientColorStripStream, ColorCycleColorStripStream)) and device_info.led_count > 0:
|
||||||
stream.configure(device_info.led_count)
|
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(
|
logger.info(
|
||||||
f"Acquired color strip stream for target {self._target_id} "
|
f"Acquired color strip stream for target {self._target_id} "
|
||||||
f"(css={self._color_strip_source_id}, display={self._resolved_display_index}, "
|
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:
|
except Exception as e:
|
||||||
logger.error(f"Failed to acquire color strip stream for target {self._target_id}: {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
|
css_manager = self._ctx.color_strip_stream_manager
|
||||||
if css_manager and self._color_strip_source_id:
|
if css_manager and self._color_strip_source_id:
|
||||||
try:
|
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)
|
await asyncio.to_thread(css_manager.release, self._color_strip_source_id)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Error releasing color strip stream for {self._target_id}: {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:
|
def update_settings(self, settings: dict) -> None:
|
||||||
"""Update target-specific timing settings."""
|
"""Update target-specific timing settings."""
|
||||||
if isinstance(settings, dict):
|
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:
|
if "standby_interval" in settings:
|
||||||
self._standby_interval = settings["standby_interval"]
|
self._standby_interval = settings["standby_interval"]
|
||||||
if "state_check_interval" in settings:
|
if "state_check_interval" in settings:
|
||||||
@@ -213,11 +227,12 @@ class WledTargetProcessor(TargetProcessor):
|
|||||||
old_id = self._color_strip_source_id
|
old_id = self._color_strip_source_id
|
||||||
try:
|
try:
|
||||||
new_stream = css_manager.acquire(color_strip_source_id)
|
new_stream = css_manager.acquire(color_strip_source_id)
|
||||||
|
css_manager.remove_target_fps(old_id, self._target_id)
|
||||||
css_manager.release(old_id)
|
css_manager.release(old_id)
|
||||||
self._color_strip_stream = new_stream
|
self._color_strip_stream = new_stream
|
||||||
self._resolved_display_index = new_stream.display_index
|
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
|
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}")
|
logger.info(f"Swapped color strip source for {self._target_id}: {old_id} → {color_strip_source_id}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to swap color strip source for {self._target_id}: {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:
|
def get_state(self) -> dict:
|
||||||
metrics = self._metrics
|
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)
|
# Pull per-stage timing from the CSS stream (runs in a background thread)
|
||||||
css_timing: dict = {}
|
css_timing: dict = {}
|
||||||
@@ -277,7 +292,7 @@ class WledTargetProcessor(TargetProcessor):
|
|||||||
|
|
||||||
def get_metrics(self) -> dict:
|
def get_metrics(self) -> dict:
|
||||||
metrics = self._metrics
|
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
|
uptime_seconds = 0.0
|
||||||
if metrics.start_time and self._is_running:
|
if metrics.start_time and self._is_running:
|
||||||
uptime_seconds = (datetime.utcnow() - metrics.start_time).total_seconds()
|
uptime_seconds = (datetime.utcnow() - metrics.start_time).total_seconds()
|
||||||
@@ -406,15 +421,15 @@ class WledTargetProcessor(TargetProcessor):
|
|||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Processing loop started for target {self._target_id} "
|
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:
|
try:
|
||||||
with high_resolution_timer():
|
with high_resolution_timer():
|
||||||
while self._is_running:
|
while self._is_running:
|
||||||
loop_start = now = time.perf_counter()
|
loop_start = now = time.perf_counter()
|
||||||
# Re-read target_fps each tick so hot-updates to the CSS source take effect
|
# Re-read target_fps each tick so hot-updates take effect immediately
|
||||||
target_fps = stream.target_fps if stream.target_fps > 0 else 30
|
target_fps = self._target_fps if self._target_fps > 0 else 30
|
||||||
frame_time = 1.0 / target_fps
|
frame_time = 1.0 / target_fps
|
||||||
|
|
||||||
# Re-fetch device info every ~30 iterations instead of every
|
# Re-fetch device info every ~30 iterations instead of every
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ class CSSEditorModal extends Modal {
|
|||||||
name: document.getElementById('css-editor-name').value,
|
name: document.getElementById('css-editor-name').value,
|
||||||
type,
|
type,
|
||||||
picture_source: document.getElementById('css-editor-picture-source').value,
|
picture_source: document.getElementById('css-editor-picture-source').value,
|
||||||
fps: document.getElementById('css-editor-fps').value,
|
|
||||||
interpolation: document.getElementById('css-editor-interpolation').value,
|
interpolation: document.getElementById('css-editor-interpolation').value,
|
||||||
smoothing: document.getElementById('css-editor-smoothing').value,
|
smoothing: document.getElementById('css-editor-smoothing').value,
|
||||||
brightness: document.getElementById('css-editor-brightness').value,
|
brightness: document.getElementById('css-editor-brightness').value,
|
||||||
@@ -26,7 +25,7 @@ class CSSEditorModal extends Modal {
|
|||||||
gamma: document.getElementById('css-editor-gamma').value,
|
gamma: document.getElementById('css-editor-gamma').value,
|
||||||
color: document.getElementById('css-editor-color').value,
|
color: document.getElementById('css-editor-color').value,
|
||||||
frame_interpolation: document.getElementById('css-editor-frame-interpolation').checked,
|
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) : '[]',
|
gradient_stops: type === 'gradient' ? JSON.stringify(_gradientStops) : '[]',
|
||||||
animation_enabled: document.getElementById('css-editor-animation-enabled').checked,
|
animation_enabled: document.getElementById('css-editor-animation-enabled').checked,
|
||||||
animation_type: document.getElementById('css-editor-animation-type').value,
|
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-static-section').style.display = type === 'static' ? '' : 'none';
|
||||||
document.getElementById('css-editor-color-cycle-section').style.display = type === 'color_cycle' ? '' : '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';
|
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)
|
// Animation section — shown for static/gradient only (color_cycle is always animating)
|
||||||
const animSection = document.getElementById('css-editor-animation-section');
|
const animSection = document.getElementById('css-editor-animation-section');
|
||||||
@@ -205,7 +201,7 @@ export function createColorStripCard(source, pictureSourceMap) {
|
|||||||
).join('');
|
).join('');
|
||||||
propsHtml = `
|
propsHtml = `
|
||||||
<span class="stream-card-prop">${swatches}</span>
|
<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>` : ''}
|
${source.led_count ? `<span class="stream-card-prop" title="${t('color_strip.leds')}">💡 ${source.led_count}</span>` : ''}
|
||||||
`;
|
`;
|
||||||
} else if (isGradient) {
|
} else if (isGradient) {
|
||||||
@@ -227,6 +223,7 @@ export function createColorStripCard(source, pictureSourceMap) {
|
|||||||
propsHtml = `
|
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>` : ''}
|
${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>
|
<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}
|
${animBadge}
|
||||||
`;
|
`;
|
||||||
} else {
|
} 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 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;
|
const ledCount = (source.led_count > 0) ? source.led_count : calLeds;
|
||||||
propsHtml = `
|
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>
|
<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 {
|
} else {
|
||||||
sourceSelect.value = css.picture_source_id || '';
|
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';
|
document.getElementById('css-editor-interpolation').value = css.interpolation_mode || 'average';
|
||||||
|
|
||||||
const smoothing = css.smoothing ?? 0.3;
|
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-name').value = '';
|
||||||
document.getElementById('css-editor-type').value = 'picture';
|
document.getElementById('css-editor-type').value = 'picture';
|
||||||
onCSSTypeChange();
|
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-interpolation').value = 'average';
|
||||||
document.getElementById('css-editor-smoothing').value = 0.3;
|
document.getElementById('css-editor-smoothing').value = 0.3;
|
||||||
document.getElementById('css-editor-smoothing-value').textContent = '0.30';
|
document.getElementById('css-editor-smoothing-value').textContent = '0.30';
|
||||||
@@ -432,7 +422,6 @@ export async function saveCSSEditor() {
|
|||||||
payload = {
|
payload = {
|
||||||
name,
|
name,
|
||||||
picture_source_id: document.getElementById('css-editor-picture-source').value,
|
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,
|
interpolation_mode: document.getElementById('css-editor-interpolation').value,
|
||||||
smoothing: parseFloat(document.getElementById('css-editor-smoothing').value),
|
smoothing: parseFloat(document.getElementById('css-editor-smoothing').value),
|
||||||
brightness: parseFloat(document.getElementById('css-editor-brightness').value),
|
brightness: parseFloat(document.getElementById('css-editor-brightness').value),
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ class TargetEditorModal extends Modal {
|
|||||||
name: document.getElementById('target-editor-name').value,
|
name: document.getElementById('target-editor-name').value,
|
||||||
device: document.getElementById('target-editor-device').value,
|
device: document.getElementById('target-editor-device').value,
|
||||||
css: document.getElementById('target-editor-css').value,
|
css: document.getElementById('target-editor-css').value,
|
||||||
|
fps: document.getElementById('target-editor-fps').value,
|
||||||
standby_interval: document.getElementById('target-editor-standby-interval').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;
|
document.getElementById('target-editor-name').value = target.name;
|
||||||
deviceSelect.value = target.device_id || '';
|
deviceSelect.value = target.device_id || '';
|
||||||
cssSelect.value = target.color_strip_source_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 = target.standby_interval ?? 1.0;
|
||||||
document.getElementById('target-editor-standby-interval-value').textContent = 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');
|
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
|
// Creating new target — first option is selected by default
|
||||||
document.getElementById('target-editor-id').value = '';
|
document.getElementById('target-editor-id').value = '';
|
||||||
document.getElementById('target-editor-name').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 = 1.0;
|
||||||
document.getElementById('target-editor-standby-interval-value').textContent = '1.0';
|
document.getElementById('target-editor-standby-interval-value').textContent = '1.0';
|
||||||
document.getElementById('target-editor-title').textContent = t('targets.add');
|
document.getElementById('target-editor-title').textContent = t('targets.add');
|
||||||
@@ -203,10 +209,13 @@ export async function saveTargetEditor() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fps = parseInt(document.getElementById('target-editor-fps').value) || 30;
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
name,
|
name,
|
||||||
device_id: deviceId,
|
device_id: deviceId,
|
||||||
color_strip_source_id: cssId,
|
color_strip_source_id: cssId,
|
||||||
|
fps,
|
||||||
standby_interval: standbyInterval,
|
standby_interval: standbyInterval,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -508,6 +517,7 @@ export function createTargetCard(target, deviceMap, colorStripSourceMap) {
|
|||||||
</div>
|
</div>
|
||||||
<div class="stream-card-props">
|
<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.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>
|
<span class="stream-card-prop stream-card-prop-full" title="${t('targets.color_strip_source')}">🎞️ ${escapeHtml(cssName)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
|
|||||||
@@ -102,6 +102,7 @@ class PictureTargetStore:
|
|||||||
target_type: str,
|
target_type: str,
|
||||||
device_id: str = "",
|
device_id: str = "",
|
||||||
color_strip_source_id: str = "",
|
color_strip_source_id: str = "",
|
||||||
|
fps: int = 30,
|
||||||
standby_interval: float = 1.0,
|
standby_interval: float = 1.0,
|
||||||
state_check_interval: int = DEFAULT_STATE_CHECK_INTERVAL,
|
state_check_interval: int = DEFAULT_STATE_CHECK_INTERVAL,
|
||||||
key_colors_settings: Optional[KeyColorsSettings] = None,
|
key_colors_settings: Optional[KeyColorsSettings] = None,
|
||||||
@@ -146,6 +147,7 @@ class PictureTargetStore:
|
|||||||
target_type="led",
|
target_type="led",
|
||||||
device_id=device_id,
|
device_id=device_id,
|
||||||
color_strip_source_id=color_strip_source_id,
|
color_strip_source_id=color_strip_source_id,
|
||||||
|
fps=fps,
|
||||||
standby_interval=standby_interval,
|
standby_interval=standby_interval,
|
||||||
state_check_interval=state_check_interval,
|
state_check_interval=state_check_interval,
|
||||||
description=description,
|
description=description,
|
||||||
@@ -178,6 +180,7 @@ class PictureTargetStore:
|
|||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
device_id: Optional[str] = None,
|
device_id: Optional[str] = None,
|
||||||
color_strip_source_id: Optional[str] = None,
|
color_strip_source_id: Optional[str] = None,
|
||||||
|
fps: Optional[int] = None,
|
||||||
standby_interval: Optional[float] = None,
|
standby_interval: Optional[float] = None,
|
||||||
state_check_interval: Optional[int] = None,
|
state_check_interval: Optional[int] = None,
|
||||||
key_colors_settings: Optional[KeyColorsSettings] = None,
|
key_colors_settings: Optional[KeyColorsSettings] = None,
|
||||||
@@ -206,6 +209,7 @@ class PictureTargetStore:
|
|||||||
name=name,
|
name=name,
|
||||||
device_id=device_id,
|
device_id=device_id,
|
||||||
color_strip_source_id=color_strip_source_id,
|
color_strip_source_id=color_strip_source_id,
|
||||||
|
fps=fps,
|
||||||
standby_interval=standby_interval,
|
standby_interval=standby_interval,
|
||||||
state_check_interval=state_check_interval,
|
state_check_interval=state_check_interval,
|
||||||
key_colors_settings=key_colors_settings,
|
key_colors_settings=key_colors_settings,
|
||||||
|
|||||||
@@ -13,13 +13,13 @@ DEFAULT_STATE_CHECK_INTERVAL = 30 # seconds
|
|||||||
class WledPictureTarget(PictureTarget):
|
class WledPictureTarget(PictureTarget):
|
||||||
"""LED picture target — pairs an LED device with a ColorStripSource.
|
"""LED picture target — pairs an LED device with a ColorStripSource.
|
||||||
|
|
||||||
The ColorStripSource encapsulates everything needed to produce LED colors
|
The ColorStripSource produces LED colors (calibration, color correction,
|
||||||
(calibration, color correction, smoothing, fps). The LED target itself only
|
smoothing). The target controls device-specific settings including send FPS.
|
||||||
holds device-specific timing/keepalive settings.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
device_id: str = ""
|
device_id: str = ""
|
||||||
color_strip_source_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
|
standby_interval: float = 1.0 # seconds between keepalive sends when screen is static
|
||||||
state_check_interval: int = DEFAULT_STATE_CHECK_INTERVAL
|
state_check_interval: int = DEFAULT_STATE_CHECK_INTERVAL
|
||||||
|
|
||||||
@@ -34,6 +34,7 @@ class WledPictureTarget(PictureTarget):
|
|||||||
target_id=self.id,
|
target_id=self.id,
|
||||||
device_id=self.device_id,
|
device_id=self.device_id,
|
||||||
color_strip_source_id=self.color_strip_source_id,
|
color_strip_source_id=self.color_strip_source_id,
|
||||||
|
fps=self.fps,
|
||||||
standby_interval=self.standby_interval,
|
standby_interval=self.standby_interval,
|
||||||
state_check_interval=self.state_check_interval,
|
state_check_interval=self.state_check_interval,
|
||||||
)
|
)
|
||||||
@@ -42,6 +43,7 @@ class WledPictureTarget(PictureTarget):
|
|||||||
"""Push changed fields to the processor manager."""
|
"""Push changed fields to the processor manager."""
|
||||||
if settings_changed:
|
if settings_changed:
|
||||||
manager.update_target_settings(self.id, {
|
manager.update_target_settings(self.id, {
|
||||||
|
"fps": self.fps,
|
||||||
"standby_interval": self.standby_interval,
|
"standby_interval": self.standby_interval,
|
||||||
"state_check_interval": self.state_check_interval,
|
"state_check_interval": self.state_check_interval,
|
||||||
})
|
})
|
||||||
@@ -51,7 +53,7 @@ class WledPictureTarget(PictureTarget):
|
|||||||
manager.update_target_device(self.id, self.device_id)
|
manager.update_target_device(self.id, self.device_id)
|
||||||
|
|
||||||
def update_fields(self, *, name=None, device_id=None, color_strip_source_id=None,
|
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:
|
# Legacy params accepted but ignored to keep base class compat:
|
||||||
picture_source_id=None, settings=None,
|
picture_source_id=None, settings=None,
|
||||||
key_colors_settings=None, description=None) -> None:
|
key_colors_settings=None, description=None) -> None:
|
||||||
@@ -61,6 +63,8 @@ class WledPictureTarget(PictureTarget):
|
|||||||
self.device_id = device_id
|
self.device_id = device_id
|
||||||
if color_strip_source_id is not None:
|
if color_strip_source_id is not None:
|
||||||
self.color_strip_source_id = color_strip_source_id
|
self.color_strip_source_id = color_strip_source_id
|
||||||
|
if fps is not None:
|
||||||
|
self.fps = fps
|
||||||
if standby_interval is not None:
|
if standby_interval is not None:
|
||||||
self.standby_interval = standby_interval
|
self.standby_interval = standby_interval
|
||||||
if state_check_interval is not None:
|
if state_check_interval is not None:
|
||||||
@@ -75,6 +79,7 @@ class WledPictureTarget(PictureTarget):
|
|||||||
d = super().to_dict()
|
d = super().to_dict()
|
||||||
d["device_id"] = self.device_id
|
d["device_id"] = self.device_id
|
||||||
d["color_strip_source_id"] = self.color_strip_source_id
|
d["color_strip_source_id"] = self.color_strip_source_id
|
||||||
|
d["fps"] = self.fps
|
||||||
d["standby_interval"] = self.standby_interval
|
d["standby_interval"] = self.standby_interval
|
||||||
d["state_check_interval"] = self.state_check_interval
|
d["state_check_interval"] = self.state_check_interval
|
||||||
return d
|
return d
|
||||||
@@ -88,6 +93,7 @@ class WledPictureTarget(PictureTarget):
|
|||||||
target_type="led",
|
target_type="led",
|
||||||
device_id=data.get("device_id", ""),
|
device_id=data.get("device_id", ""),
|
||||||
color_strip_source_id=data.get("color_strip_source_id", ""),
|
color_strip_source_id=data.get("color_strip_source_id", ""),
|
||||||
|
fps=data.get("fps", 30),
|
||||||
standby_interval=data.get("standby_interval", 1.0),
|
standby_interval=data.get("standby_interval", 1.0),
|
||||||
state_check_interval=data.get("state_check_interval", DEFAULT_STATE_CHECK_INTERVAL),
|
state_check_interval=data.get("state_check_interval", DEFAULT_STATE_CHECK_INTERVAL),
|
||||||
description=data.get("description"),
|
description=data.get("description"),
|
||||||
|
|||||||
@@ -39,19 +39,13 @@
|
|||||||
<select id="css-editor-picture-source"></select>
|
<select id="css-editor-picture-source"></select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div id="css-editor-led-count-group" class="form-group">
|
||||||
<div class="label-row">
|
<div class="label-row">
|
||||||
<label for="css-editor-fps">
|
<label for="css-editor-led-count" data-i18n="color_strip.led_count">LED Count:</label>
|
||||||
<span data-i18n="color_strip.fps">Target FPS:</span>
|
|
||||||
<span id="css-editor-fps-value">30</span>
|
|
||||||
</label>
|
|
||||||
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
|
||||||
</div>
|
</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>
|
<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>
|
||||||
<div class="slider-row">
|
<input type="number" id="css-editor-led-count" min="0" max="1500" step="1" value="0">
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@@ -236,16 +230,6 @@
|
|||||||
</details>
|
</details>
|
||||||
</div>
|
</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>
|
<div id="css-editor-error" class="error-message" style="display: none;"></div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -32,6 +32,21 @@
|
|||||||
<select id="target-editor-css"></select>
|
<select id="target-editor-css"></select>
|
||||||
</div>
|
</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="form-group" id="target-editor-standby-group">
|
||||||
<div class="label-row">
|
<div class="label-row">
|
||||||
<label for="target-editor-standby-interval">
|
<label for="target-editor-standby-interval">
|
||||||
|
|||||||
Reference in New Issue
Block a user