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 = ` +
+
${t('device.metrics.timing')}
+
${state.timing_total_ms}ms
+
+
+ + + +
+
+ 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 ? ` -
-
-
${t('device.metrics.timing')}
-
${state.timing_total_ms}ms
-
-
- - - -
-
- 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 ` +
+
${t('device.metrics.timing')}
+
${state.timing_total_ms}ms
+
+
+ ${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 ? ` -
-
-
${t('device.metrics.timing')}
-
${state.timing_total_ms}ms
-
-
- ${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)}
+
---
` : ''}