Frame interpolation, FPS hot-update, timing metrics, KC brightness fixes

- CSS: add frame interpolation option — blends between consecutive captured
  frames on idle ticks so LED output runs at full target FPS even when
  capture rate is lower (e.g. capture 30fps, output 60fps)
- WledTargetProcessor: re-read stream.target_fps each loop tick so FPS
  changes to the CSS source take effect without restarting the target
- WledTargetProcessor: restore per-stage timing metrics on target card by
  pulling extract/map/smooth/total from CSS stream get_last_timing()
- TargetProcessingState schema: add missing timing_extract_ms,
  timing_map_leds_ms, timing_smooth_ms, timing_total_ms fields
- KC targets: add extraction FPS badge to target card props row
- KC targets: fix 500 error when changing brightness — update_fields now
  accepts (and ignores) WLED-specific kwargs
- KC targets: fix partial key_colors_settings update wiping pattern_template_id
  — update route merges only explicitly-set fields using model_dump(exclude_unset=True)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-20 20:29:22 +03:00
parent be37df4459
commit 55e25b8860
14 changed files with 138 additions and 6 deletions

View File

@@ -121,6 +121,7 @@ class ColorStripSource:
interpolation_mode=data.get("interpolation_mode") or "average",
calibration=calibration,
led_count=data.get("led_count") or 0,
frame_interpolation=bool(data.get("frame_interpolation", False)),
)
@@ -143,6 +144,7 @@ class PictureColorStripSource(ColorStripSource):
default_factory=lambda: CalibrationConfig(layout="clockwise", start_position="bottom_left")
)
led_count: int = 0 # explicit LED count; 0 = auto (derived from calibration)
frame_interpolation: bool = False # blend between consecutive captured frames
def to_dict(self) -> dict:
d = super().to_dict()
@@ -155,6 +157,7 @@ class PictureColorStripSource(ColorStripSource):
d["interpolation_mode"] = self.interpolation_mode
d["calibration"] = calibration_to_dict(self.calibration)
d["led_count"] = self.led_count
d["frame_interpolation"] = self.frame_interpolation
return d

View File

@@ -104,6 +104,7 @@ class ColorStripStore:
color: Optional[list] = None,
stops: Optional[list] = None,
description: Optional[str] = None,
frame_interpolation: bool = False,
) -> ColorStripSource:
"""Create a new color strip source.
@@ -165,6 +166,7 @@ class ColorStripStore:
interpolation_mode=interpolation_mode,
calibration=calibration,
led_count=led_count,
frame_interpolation=frame_interpolation,
)
self._sources[source_id] = source
@@ -189,6 +191,7 @@ class ColorStripStore:
color: Optional[list] = None,
stops: Optional[list] = None,
description: Optional[str] = None,
frame_interpolation: Optional[bool] = None,
) -> ColorStripSource:
"""Update an existing color strip source.
@@ -228,6 +231,8 @@ class ColorStripStore:
source.calibration = calibration
if led_count is not None:
source.led_count = led_count
if frame_interpolation is not None:
source.frame_interpolation = frame_interpolation
elif isinstance(source, StaticColorStripSource):
if color is not None:
if isinstance(color, list) and len(color) == 3:

View File

@@ -90,7 +90,10 @@ class KeyColorsPictureTarget(PictureTarget):
manager.update_target_source(self.id, self.picture_source_id)
def update_fields(self, *, name=None, device_id=None, picture_source_id=None,
settings=None, key_colors_settings=None, description=None) -> None:
settings=None, key_colors_settings=None, description=None,
# WledPictureTarget-specific params — accepted but ignored:
color_strip_source_id=None, standby_interval=None,
state_check_interval=None) -> None:
"""Apply mutable field updates for KC targets."""
super().update_fields(name=name, description=description)
if picture_source_id is not None: