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

@@ -70,6 +70,7 @@ def _css_to_response(source, overlay_active: bool = False) -> ColorStripSourceRe
color=getattr(source, "color", None),
stops=stops,
description=source.description,
frame_interpolation=getattr(source, "frame_interpolation", None),
overlay_active=overlay_active,
created_at=source.created_at,
updated_at=source.updated_at,
@@ -134,6 +135,7 @@ async def create_color_strip_source(
color=data.color,
stops=stops,
description=data.description,
frame_interpolation=data.frame_interpolation,
)
return _css_to_response(source)
@@ -190,6 +192,7 @@ async def update_color_strip_source(
color=data.color,
stops=stops,
description=data.description,
frame_interpolation=data.frame_interpolation,
)
# Hot-reload running stream (no restart needed for in-place param changes)

View File

@@ -214,7 +214,28 @@ async def update_target(
if not device:
raise HTTPException(status_code=422, detail=f"Device {data.device_id} not found")
kc_settings = _kc_schema_to_settings(data.key_colors_settings) if data.key_colors_settings else None
# Build KC settings with partial-update support: only apply fields that were
# explicitly provided in the request body, merging with the existing settings.
kc_settings = None
if data.key_colors_settings is not None:
incoming = data.key_colors_settings.model_dump(exclude_unset=True)
try:
existing_target = target_store.get_target(target_id)
except ValueError:
existing_target = None
if isinstance(existing_target, KeyColorsPictureTarget):
ex = existing_target.settings
merged = KeyColorsSettingsSchema(
fps=incoming.get("fps", ex.fps),
interpolation_mode=incoming.get("interpolation_mode", ex.interpolation_mode),
smoothing=incoming.get("smoothing", ex.smoothing),
pattern_template_id=incoming.get("pattern_template_id", ex.pattern_template_id),
brightness=incoming.get("brightness", ex.brightness),
)
kc_settings = _kc_schema_to_settings(merged)
else:
kc_settings = _kc_schema_to_settings(data.key_colors_settings)
# Update in store
target = target_store.update_target(