diff --git a/server/src/wled_controller/core/filters/gamma.py b/server/src/wled_controller/core/filters/gamma.py
index e1ff7f2..7e83101 100644
--- a/server/src/wled_controller/core/filters/gamma.py
+++ b/server/src/wled_controller/core/filters/gamma.py
@@ -20,7 +20,7 @@ class GammaFilter(PostprocessingFilter):
super().__init__(options)
value = self.options["value"]
lut = np.arange(256, dtype=np.float32) / 255.0
- np.power(lut, 1.0 / value, out=lut)
+ np.power(lut, value, out=lut)
self._lut = np.clip(lut * 255.0, 0, 255).astype(np.uint8)
@classmethod
diff --git a/server/src/wled_controller/core/processing/color_strip_stream.py b/server/src/wled_controller/core/processing/color_strip_stream.py
index 00960be..c8dc021 100644
--- a/server/src/wled_controller/core/processing/color_strip_stream.py
+++ b/server/src/wled_controller/core/processing/color_strip_stream.py
@@ -64,13 +64,13 @@ def _build_gamma_lut(gamma: float) -> np.ndarray:
"""Build a 256-entry uint8 LUT for gamma correction.
gamma=1.0: identity (no correction)
- gamma<1.0: brighter midtones
- gamma>1.0: darker midtones
+ gamma<1.0: brighter midtones (gamma < 1 lifts shadows)
+ gamma>1.0: darker midtones (standard LED gamma, e.g. 2.2–2.8)
"""
if gamma == 1.0:
return np.arange(256, dtype=np.uint8)
lut = np.array(
- [min(255, int(((i / 255.0) ** (1.0 / gamma)) * 255 + 0.5)) for i in range(256)],
+ [min(255, int(((i / 255.0) ** gamma) * 255 + 0.5)) for i in range(256)],
dtype=np.uint8,
)
return lut
@@ -407,13 +407,6 @@ class PictureColorStripStream(ColorStripStream):
frame_buf[:] = led_colors
led_colors = frame_buf
- # Update interpolation buffers (raw colors, before corrections)
- if self._frame_interpolation:
- self._interp_from = self._interp_to
- self._interp_to = led_colors.copy()
- self._interp_start = loop_start
- self._interp_duration = max(interval, 0.001)
-
# Temporal smoothing (pre-allocated uint16 scratch)
smoothing = self._smoothing
if (
@@ -426,6 +419,15 @@ class PictureColorStripStream(ColorStripStream):
int(smoothing * 256), led_colors)
t3 = time.perf_counter()
+ # Update interpolation buffers (smoothed colors, before corrections)
+ # Must be AFTER smoothing so idle-tick interpolation produces
+ # output consistent with new-frame ticks (both smoothed).
+ if self._frame_interpolation:
+ self._interp_from = self._interp_to
+ self._interp_to = led_colors.copy()
+ self._interp_start = loop_start
+ self._interp_duration = max(interval, 0.001)
+
# Saturation (pre-allocated int32 scratch)
saturation = self._saturation
if saturation != 1.0:
diff --git a/server/src/wled_controller/static/js/features/kc-targets.js b/server/src/wled_controller/static/js/features/kc-targets.js
index ed37bcb..ec80106 100644
--- a/server/src/wled_controller/static/js/features/kc-targets.js
+++ b/server/src/wled_controller/static/js/features/kc-targets.js
@@ -39,9 +39,55 @@ class KCEditorModal extends Modal {
const kcEditorModal = new KCEditorModal();
-export function createKCTargetCard(target, sourceMap, patternTemplateMap, valueSourceMap) {
+export function patchKCTargetMetrics(target) {
+ const card = document.querySelector(`[data-kc-target-id="${target.id}"]`);
+ if (!card) return;
const state = target.state || {};
const metrics = target.metrics || {};
+
+ const fpsActual = card.querySelector('[data-tm="fps-actual"]');
+ if (fpsActual) fpsActual.textContent = state.fps_actual?.toFixed(1) || '0.0';
+
+ const fpsCurrent = card.querySelector('[data-tm="fps-current"]');
+ if (fpsCurrent) fpsCurrent.textContent = state.fps_current ?? '-';
+
+ const fpsTarget = card.querySelector('[data-tm="fps-target"]');
+ if (fpsTarget) fpsTarget.textContent = state.fps_target || 0;
+
+ const frames = card.querySelector('[data-tm="frames"]');
+ if (frames) frames.textContent = metrics.frames_processed || 0;
+
+ const keepalive = card.querySelector('[data-tm="keepalive"]');
+ if (keepalive) keepalive.textContent = state.frames_keepalive ?? '-';
+
+ const errors = card.querySelector('[data-tm="errors"]');
+ if (errors) errors.textContent = metrics.errors_count || 0;
+
+ const uptime = card.querySelector('[data-tm="uptime"]');
+ if (uptime) uptime.textContent = formatUptime(metrics.uptime_seconds);
+
+ const timing = card.querySelector('[data-tm="timing"]');
+ if (timing && state.timing_total_ms != null) {
+ timing.innerHTML = `
+
+
+
+
+
+
+
+ calc ${state.timing_calc_colors_ms}ms
+ smooth ${state.timing_smooth_ms}ms
+ broadcast ${state.timing_broadcast_ms}ms
+
`;
+ }
+}
+
+export function createKCTargetCard(target, sourceMap, patternTemplateMap, valueSourceMap) {
+ const state = target.state || {};
const kcSettings = target.key_colors_settings || {};
const isProcessing = state.processing || false;
@@ -105,50 +151,35 @@ export function createKCTargetCard(target, sourceMap, patternTemplateMap, valueS
${t('device.metrics.actual_fps')}
-
${state.fps_actual?.toFixed(1) || '0.0'}
+
---
${t('device.metrics.current_fps')}
-
${state.fps_current ?? '-'}
+
---
${t('device.metrics.target_fps')}
-
${state.fps_target || 0}
+
---
${t('device.metrics.frames')}
-
${metrics.frames_processed || 0}
+
---
${t('device.metrics.keepalive')}
-
${state.frames_keepalive ?? '-'}
+
---
${t('device.metrics.errors')}
-
${metrics.errors_count || 0}
+
---
${t('device.metrics.uptime')}
-
${formatUptime(metrics.uptime_seconds)}
+
---
${state.timing_total_ms != null ? `
-
-
-
-
-
-
-
-
- calc ${state.timing_calc_colors_ms}ms
- smooth ${state.timing_smooth_ms}ms
- broadcast ${state.timing_broadcast_ms}ms
-
-
+
` : ''}
` : ''}
diff --git a/server/src/wled_controller/static/js/features/targets.js b/server/src/wled_controller/static/js/features/targets.js
index 0795079..86e0286 100644
--- a/server/src/wled_controller/static/js/features/targets.js
+++ b/server/src/wled_controller/static/js/features/targets.js
@@ -15,7 +15,7 @@ import { t } from '../core/i18n.js';
import { showToast, showConfirm, formatUptime, setTabRefreshing } from '../core/ui.js';
import { Modal } from '../core/modal.js';
import { createDeviceCard, attachDeviceListeners, fetchDeviceBrightness, _computeMaxFps } from './devices.js';
-import { createKCTargetCard, connectKCWebSocket, disconnectKCWebSocket } from './kc-targets.js';
+import { createKCTargetCard, patchKCTargetMetrics, connectKCWebSocket, disconnectKCWebSocket } from './kc-targets.js';
import { createColorStripCard } from './color-strips.js';
import {
getValueSourceIcon, getTargetTypeIcon,
@@ -555,6 +555,14 @@ export async function loadTargetsTab() {
CardSection.bindAll([csDevices, csColorStrips, csLedTargets, csKCTargets, csPatternTemplates]);
}
+ // Patch volatile metrics in-place (avoids full card replacement on polls)
+ for (const tgt of ledTargets) {
+ if (tgt.state && tgt.state.processing) _patchTargetMetrics(tgt);
+ }
+ for (const tgt of kcTargets) {
+ if (tgt.state && tgt.state.processing) patchKCTargetMetrics(tgt);
+ }
+
// Attach event listeners and fetch brightness for device cards
devicesWithState.forEach(device => {
attachDeviceListeners(device.id);
@@ -665,10 +673,69 @@ function _cssSourceName(cssId, colorStripSourceMap) {
return css ? escapeHtml(css.name) : escapeHtml(cssId);
}
-export function createTargetCard(target, deviceMap, colorStripSourceMap, valueSourceMap) {
+// ─── In-place metric patching (avoids full card replacement on polls) ───
+
+function _buildLedTimingHTML(state) {
+ const isAudio = state.timing_audio_read_ms != null;
+ return `
+
+
+ ${isAudio ? `
+
+
+
+ ` : `
+ ${state.timing_extract_ms != null ? `` : ''}
+ ${state.timing_map_leds_ms != null ? `` : ''}
+ ${state.timing_smooth_ms != null ? `` : ''}
+ `}
+
+
+
+ ${isAudio ? `
+ read ${state.timing_audio_read_ms}ms
+ fft ${state.timing_audio_fft_ms}ms
+ render ${state.timing_audio_render_ms}ms
+ ` : `
+ ${state.timing_extract_ms != null ? `extract ${state.timing_extract_ms}ms` : ''}
+ ${state.timing_map_leds_ms != null ? `map ${state.timing_map_leds_ms}ms` : ''}
+ ${state.timing_smooth_ms != null ? `smooth ${state.timing_smooth_ms}ms` : ''}
+ `}
+ send ${state.timing_send_ms}ms
+
`;
+}
+
+function _patchTargetMetrics(target) {
+ const card = document.querySelector(`[data-target-id="${target.id}"]`);
+ if (!card) return;
const state = target.state || {};
const metrics = target.metrics || {};
+ const fps = card.querySelector('[data-tm="fps"]');
+ if (fps) fps.innerHTML = `${state.fps_actual?.toFixed(1) || '0.0'}/${state.fps_target || 0}`;
+
+ const timing = card.querySelector('[data-tm="timing"]');
+ if (timing && state.timing_total_ms != null) timing.innerHTML = _buildLedTimingHTML(state);
+
+ const frames = card.querySelector('[data-tm="frames"]');
+ if (frames) frames.textContent = metrics.frames_processed || 0;
+
+ const keepalive = card.querySelector('[data-tm="keepalive"]');
+ if (keepalive) keepalive.textContent = state.frames_keepalive ?? '-';
+
+ const errors = card.querySelector('[data-tm="errors"]');
+ if (errors) errors.textContent = metrics.errors_count || 0;
+
+ const uptime = card.querySelector('[data-tm="uptime"]');
+ if (uptime) uptime.textContent = formatUptime(metrics.uptime_seconds);
+}
+
+export function createTargetCard(target, deviceMap, colorStripSourceMap, valueSourceMap) {
+ const state = target.state || {};
+
const isProcessing = state.processing || false;
const device = deviceMap[target.device_id];
@@ -720,58 +787,29 @@ export function createTargetCard(target, deviceMap, colorStripSourceMap, valueSo
- ${state.fps_actual?.toFixed(1) || '0.0'}/${state.fps_target || 0}
+ ---
${state.timing_total_ms != null ? `
-
-
-
- ${state.timing_audio_read_ms != null ? `
-
-
-
- ` : `
- ${state.timing_extract_ms != null ? `` : ''}
- ${state.timing_map_leds_ms != null ? `` : ''}
- ${state.timing_smooth_ms != null ? `` : ''}
- `}
-
-
-
- ${state.timing_audio_read_ms != null ? `
- read ${state.timing_audio_read_ms}ms
- fft ${state.timing_audio_fft_ms}ms
- render ${state.timing_audio_render_ms}ms
- ` : `
- ${state.timing_extract_ms != null ? `extract ${state.timing_extract_ms}ms` : ''}
- ${state.timing_map_leds_ms != null ? `map ${state.timing_map_leds_ms}ms` : ''}
- ${state.timing_smooth_ms != null ? `smooth ${state.timing_smooth_ms}ms` : ''}
- `}
- send ${state.timing_send_ms}ms
-
-
+
` : ''}
${t('device.metrics.frames')}
-
${metrics.frames_processed || 0}
+
---
${state.needs_keepalive !== false ? `
${t('device.metrics.keepalive')}
-
${state.frames_keepalive ?? '-'}
+
---
` : ''}
${t('device.metrics.errors')}
-
${metrics.errors_count || 0}
+
---
${t('device.metrics.uptime')}
-
${formatUptime(metrics.uptime_seconds)}
+
---
` : ''}