Fix gamma correction, frame interpolation flicker, and target card redraws
- Fix inverted gamma formula: use (i/255)^gamma instead of (i/255)^(1/gamma) so gamma>1 correctly darkens midtones (standard LED gamma correction) - Fix frame interpolation flicker: move interp buffer update after temporal smoothing so idle-tick output is consistent with new-frame output - Fix target card hover/animation reset: use stable placeholder values in card HTML for volatile metrics (data-tm attributes), patch real values in-place after reconcile instead of replacing entire card DOM element Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -20,7 +20,7 @@ class GammaFilter(PostprocessingFilter):
|
|||||||
super().__init__(options)
|
super().__init__(options)
|
||||||
value = self.options["value"]
|
value = self.options["value"]
|
||||||
lut = np.arange(256, dtype=np.float32) / 255.0
|
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)
|
self._lut = np.clip(lut * 255.0, 0, 255).astype(np.uint8)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
@@ -64,13 +64,13 @@ def _build_gamma_lut(gamma: float) -> np.ndarray:
|
|||||||
"""Build a 256-entry uint8 LUT for gamma correction.
|
"""Build a 256-entry uint8 LUT for gamma correction.
|
||||||
|
|
||||||
gamma=1.0: identity (no correction)
|
gamma=1.0: identity (no correction)
|
||||||
gamma<1.0: brighter midtones
|
gamma<1.0: brighter midtones (gamma < 1 lifts shadows)
|
||||||
gamma>1.0: darker midtones
|
gamma>1.0: darker midtones (standard LED gamma, e.g. 2.2–2.8)
|
||||||
"""
|
"""
|
||||||
if gamma == 1.0:
|
if gamma == 1.0:
|
||||||
return np.arange(256, dtype=np.uint8)
|
return np.arange(256, dtype=np.uint8)
|
||||||
lut = np.array(
|
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,
|
dtype=np.uint8,
|
||||||
)
|
)
|
||||||
return lut
|
return lut
|
||||||
@@ -407,13 +407,6 @@ class PictureColorStripStream(ColorStripStream):
|
|||||||
frame_buf[:] = led_colors
|
frame_buf[:] = led_colors
|
||||||
led_colors = frame_buf
|
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)
|
# Temporal smoothing (pre-allocated uint16 scratch)
|
||||||
smoothing = self._smoothing
|
smoothing = self._smoothing
|
||||||
if (
|
if (
|
||||||
@@ -426,6 +419,15 @@ class PictureColorStripStream(ColorStripStream):
|
|||||||
int(smoothing * 256), led_colors)
|
int(smoothing * 256), led_colors)
|
||||||
t3 = time.perf_counter()
|
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 (pre-allocated int32 scratch)
|
||||||
saturation = self._saturation
|
saturation = self._saturation
|
||||||
if saturation != 1.0:
|
if saturation != 1.0:
|
||||||
|
|||||||
@@ -39,9 +39,55 @@ class KCEditorModal extends Modal {
|
|||||||
|
|
||||||
const kcEditorModal = new KCEditorModal();
|
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 state = target.state || {};
|
||||||
const metrics = target.metrics || {};
|
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 = `
|
||||||
|
<div class="timing-header">
|
||||||
|
<div class="metric-label">${t('device.metrics.timing')}</div>
|
||||||
|
<div class="timing-total"><strong>${state.timing_total_ms}ms</strong></div>
|
||||||
|
</div>
|
||||||
|
<div class="timing-bar">
|
||||||
|
<span class="timing-seg timing-extract" style="flex:${state.timing_calc_colors_ms}" title="calc ${state.timing_calc_colors_ms}ms"></span>
|
||||||
|
<span class="timing-seg timing-smooth" style="flex:${state.timing_smooth_ms || 0.1}" title="smooth ${state.timing_smooth_ms}ms"></span>
|
||||||
|
<span class="timing-seg timing-send" style="flex:${state.timing_broadcast_ms}" title="broadcast ${state.timing_broadcast_ms}ms"></span>
|
||||||
|
</div>
|
||||||
|
<div class="timing-legend">
|
||||||
|
<span class="timing-legend-item"><span class="timing-dot timing-extract"></span>calc ${state.timing_calc_colors_ms}ms</span>
|
||||||
|
<span class="timing-legend-item"><span class="timing-dot timing-smooth"></span>smooth ${state.timing_smooth_ms}ms</span>
|
||||||
|
<span class="timing-legend-item"><span class="timing-dot timing-send"></span>broadcast ${state.timing_broadcast_ms}ms</span>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createKCTargetCard(target, sourceMap, patternTemplateMap, valueSourceMap) {
|
||||||
|
const state = target.state || {};
|
||||||
const kcSettings = target.key_colors_settings || {};
|
const kcSettings = target.key_colors_settings || {};
|
||||||
|
|
||||||
const isProcessing = state.processing || false;
|
const isProcessing = state.processing || false;
|
||||||
@@ -105,50 +151,35 @@ export function createKCTargetCard(target, sourceMap, patternTemplateMap, valueS
|
|||||||
<div class="metrics-grid">
|
<div class="metrics-grid">
|
||||||
<div class="metric">
|
<div class="metric">
|
||||||
<div class="metric-label">${t('device.metrics.actual_fps')}</div>
|
<div class="metric-label">${t('device.metrics.actual_fps')}</div>
|
||||||
<div class="metric-value">${state.fps_actual?.toFixed(1) || '0.0'}</div>
|
<div class="metric-value" data-tm="fps-actual">---</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="metric">
|
<div class="metric">
|
||||||
<div class="metric-label">${t('device.metrics.current_fps')}</div>
|
<div class="metric-label">${t('device.metrics.current_fps')}</div>
|
||||||
<div class="metric-value">${state.fps_current ?? '-'}</div>
|
<div class="metric-value" data-tm="fps-current">---</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="metric">
|
<div class="metric">
|
||||||
<div class="metric-label">${t('device.metrics.target_fps')}</div>
|
<div class="metric-label">${t('device.metrics.target_fps')}</div>
|
||||||
<div class="metric-value">${state.fps_target || 0}</div>
|
<div class="metric-value" data-tm="fps-target">---</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="metric">
|
<div class="metric">
|
||||||
<div class="metric-label">${t('device.metrics.frames')}</div>
|
<div class="metric-label">${t('device.metrics.frames')}</div>
|
||||||
<div class="metric-value">${metrics.frames_processed || 0}</div>
|
<div class="metric-value" data-tm="frames">---</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="metric">
|
<div class="metric">
|
||||||
<div class="metric-label">${t('device.metrics.keepalive')}</div>
|
<div class="metric-label">${t('device.metrics.keepalive')}</div>
|
||||||
<div class="metric-value">${state.frames_keepalive ?? '-'}</div>
|
<div class="metric-value" data-tm="keepalive">---</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="metric">
|
<div class="metric">
|
||||||
<div class="metric-label">${t('device.metrics.errors')}</div>
|
<div class="metric-label">${t('device.metrics.errors')}</div>
|
||||||
<div class="metric-value">${metrics.errors_count || 0}</div>
|
<div class="metric-value" data-tm="errors">---</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="metric">
|
<div class="metric">
|
||||||
<div class="metric-label">${t('device.metrics.uptime')}</div>
|
<div class="metric-label">${t('device.metrics.uptime')}</div>
|
||||||
<div class="metric-value">${formatUptime(metrics.uptime_seconds)}</div>
|
<div class="metric-value" data-tm="uptime">---</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
${state.timing_total_ms != null ? `
|
${state.timing_total_ms != null ? `
|
||||||
<div class="timing-breakdown">
|
<div class="timing-breakdown" data-tm="timing"></div>
|
||||||
<div class="timing-header">
|
|
||||||
<div class="metric-label">${t('device.metrics.timing')}</div>
|
|
||||||
<div class="timing-total"><strong>${state.timing_total_ms}ms</strong></div>
|
|
||||||
</div>
|
|
||||||
<div class="timing-bar">
|
|
||||||
<span class="timing-seg timing-extract" style="flex:${state.timing_calc_colors_ms}" title="calc ${state.timing_calc_colors_ms}ms"></span>
|
|
||||||
<span class="timing-seg timing-smooth" style="flex:${state.timing_smooth_ms || 0.1}" title="smooth ${state.timing_smooth_ms}ms"></span>
|
|
||||||
<span class="timing-seg timing-send" style="flex:${state.timing_broadcast_ms}" title="broadcast ${state.timing_broadcast_ms}ms"></span>
|
|
||||||
</div>
|
|
||||||
<div class="timing-legend">
|
|
||||||
<span class="timing-legend-item"><span class="timing-dot timing-extract"></span>calc ${state.timing_calc_colors_ms}ms</span>
|
|
||||||
<span class="timing-legend-item"><span class="timing-dot timing-smooth"></span>smooth ${state.timing_smooth_ms}ms</span>
|
|
||||||
<span class="timing-legend-item"><span class="timing-dot timing-send"></span>broadcast ${state.timing_broadcast_ms}ms</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
` : ''}
|
` : ''}
|
||||||
</div>
|
</div>
|
||||||
` : ''}
|
` : ''}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { t } from '../core/i18n.js';
|
|||||||
import { showToast, showConfirm, formatUptime, setTabRefreshing } from '../core/ui.js';
|
import { showToast, showConfirm, formatUptime, setTabRefreshing } from '../core/ui.js';
|
||||||
import { Modal } from '../core/modal.js';
|
import { Modal } from '../core/modal.js';
|
||||||
import { createDeviceCard, attachDeviceListeners, fetchDeviceBrightness, _computeMaxFps } from './devices.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 { createColorStripCard } from './color-strips.js';
|
||||||
import {
|
import {
|
||||||
getValueSourceIcon, getTargetTypeIcon,
|
getValueSourceIcon, getTargetTypeIcon,
|
||||||
@@ -555,6 +555,14 @@ export async function loadTargetsTab() {
|
|||||||
CardSection.bindAll([csDevices, csColorStrips, csLedTargets, csKCTargets, csPatternTemplates]);
|
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
|
// Attach event listeners and fetch brightness for device cards
|
||||||
devicesWithState.forEach(device => {
|
devicesWithState.forEach(device => {
|
||||||
attachDeviceListeners(device.id);
|
attachDeviceListeners(device.id);
|
||||||
@@ -665,10 +673,69 @@ function _cssSourceName(cssId, colorStripSourceMap) {
|
|||||||
return css ? escapeHtml(css.name) : escapeHtml(cssId);
|
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 `
|
||||||
|
<div class="timing-header">
|
||||||
|
<div class="metric-label">${t('device.metrics.timing')}</div>
|
||||||
|
<div class="timing-total"><strong>${state.timing_total_ms}ms</strong></div>
|
||||||
|
</div>
|
||||||
|
<div class="timing-bar">
|
||||||
|
${isAudio ? `
|
||||||
|
<span class="timing-seg timing-audio-read" style="flex:${state.timing_audio_read_ms}" title="read ${state.timing_audio_read_ms}ms"></span>
|
||||||
|
<span class="timing-seg timing-audio-fft" style="flex:${state.timing_audio_fft_ms}" title="fft ${state.timing_audio_fft_ms}ms"></span>
|
||||||
|
<span class="timing-seg timing-audio-render" style="flex:${state.timing_audio_render_ms || 0.1}" title="render ${state.timing_audio_render_ms}ms"></span>
|
||||||
|
` : `
|
||||||
|
${state.timing_extract_ms != null ? `<span class="timing-seg timing-extract" style="flex:${state.timing_extract_ms}" title="extract ${state.timing_extract_ms}ms"></span>` : ''}
|
||||||
|
${state.timing_map_leds_ms != null ? `<span class="timing-seg timing-map" style="flex:${state.timing_map_leds_ms}" title="map ${state.timing_map_leds_ms}ms"></span>` : ''}
|
||||||
|
${state.timing_smooth_ms != null ? `<span class="timing-seg timing-smooth" style="flex:${state.timing_smooth_ms || 0.1}" title="smooth ${state.timing_smooth_ms}ms"></span>` : ''}
|
||||||
|
`}
|
||||||
|
<span class="timing-seg timing-send" style="flex:${state.timing_send_ms}" title="send ${state.timing_send_ms}ms"></span>
|
||||||
|
</div>
|
||||||
|
<div class="timing-legend">
|
||||||
|
${isAudio ? `
|
||||||
|
<span class="timing-legend-item"><span class="timing-dot timing-audio-read"></span>read ${state.timing_audio_read_ms}ms</span>
|
||||||
|
<span class="timing-legend-item"><span class="timing-dot timing-audio-fft"></span>fft ${state.timing_audio_fft_ms}ms</span>
|
||||||
|
<span class="timing-legend-item"><span class="timing-dot timing-audio-render"></span>render ${state.timing_audio_render_ms}ms</span>
|
||||||
|
` : `
|
||||||
|
${state.timing_extract_ms != null ? `<span class="timing-legend-item"><span class="timing-dot timing-extract"></span>extract ${state.timing_extract_ms}ms</span>` : ''}
|
||||||
|
${state.timing_map_leds_ms != null ? `<span class="timing-legend-item"><span class="timing-dot timing-map"></span>map ${state.timing_map_leds_ms}ms</span>` : ''}
|
||||||
|
${state.timing_smooth_ms != null ? `<span class="timing-legend-item"><span class="timing-dot timing-smooth"></span>smooth ${state.timing_smooth_ms}ms</span>` : ''}
|
||||||
|
`}
|
||||||
|
<span class="timing-legend-item"><span class="timing-dot timing-send"></span>send ${state.timing_send_ms}ms</span>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _patchTargetMetrics(target) {
|
||||||
|
const card = document.querySelector(`[data-target-id="${target.id}"]`);
|
||||||
|
if (!card) return;
|
||||||
const state = target.state || {};
|
const state = target.state || {};
|
||||||
const metrics = target.metrics || {};
|
const metrics = target.metrics || {};
|
||||||
|
|
||||||
|
const fps = card.querySelector('[data-tm="fps"]');
|
||||||
|
if (fps) fps.innerHTML = `${state.fps_actual?.toFixed(1) || '0.0'}<span class="target-fps-target">/${state.fps_target || 0}</span>`;
|
||||||
|
|
||||||
|
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 isProcessing = state.processing || false;
|
||||||
|
|
||||||
const device = deviceMap[target.device_id];
|
const device = deviceMap[target.device_id];
|
||||||
@@ -720,58 +787,29 @@ export function createTargetCard(target, deviceMap, colorStripSourceMap, valueSo
|
|||||||
<canvas id="target-fps-${target.id}" data-fps-target="${state.fps_target || 30}"></canvas>
|
<canvas id="target-fps-${target.id}" data-fps-target="${state.fps_target || 30}"></canvas>
|
||||||
</div>
|
</div>
|
||||||
<div class="target-fps-label">
|
<div class="target-fps-label">
|
||||||
<span class="metric-value">${state.fps_actual?.toFixed(1) || '0.0'}<span class="target-fps-target">/${state.fps_target || 0}</span></span>
|
<span class="metric-value" data-tm="fps">---</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
${state.timing_total_ms != null ? `
|
${state.timing_total_ms != null ? `
|
||||||
<div class="timing-breakdown" style="grid-column:1/-1">
|
<div class="timing-breakdown" data-tm="timing" style="grid-column:1/-1"></div>
|
||||||
<div class="timing-header">
|
|
||||||
<div class="metric-label">${t('device.metrics.timing')}</div>
|
|
||||||
<div class="timing-total"><strong>${state.timing_total_ms}ms</strong></div>
|
|
||||||
</div>
|
|
||||||
<div class="timing-bar">
|
|
||||||
${state.timing_audio_read_ms != null ? `
|
|
||||||
<span class="timing-seg timing-audio-read" style="flex:${state.timing_audio_read_ms}" title="read ${state.timing_audio_read_ms}ms"></span>
|
|
||||||
<span class="timing-seg timing-audio-fft" style="flex:${state.timing_audio_fft_ms}" title="fft ${state.timing_audio_fft_ms}ms"></span>
|
|
||||||
<span class="timing-seg timing-audio-render" style="flex:${state.timing_audio_render_ms || 0.1}" title="render ${state.timing_audio_render_ms}ms"></span>
|
|
||||||
` : `
|
|
||||||
${state.timing_extract_ms != null ? `<span class="timing-seg timing-extract" style="flex:${state.timing_extract_ms}" title="extract ${state.timing_extract_ms}ms"></span>` : ''}
|
|
||||||
${state.timing_map_leds_ms != null ? `<span class="timing-seg timing-map" style="flex:${state.timing_map_leds_ms}" title="map ${state.timing_map_leds_ms}ms"></span>` : ''}
|
|
||||||
${state.timing_smooth_ms != null ? `<span class="timing-seg timing-smooth" style="flex:${state.timing_smooth_ms || 0.1}" title="smooth ${state.timing_smooth_ms}ms"></span>` : ''}
|
|
||||||
`}
|
|
||||||
<span class="timing-seg timing-send" style="flex:${state.timing_send_ms}" title="send ${state.timing_send_ms}ms"></span>
|
|
||||||
</div>
|
|
||||||
<div class="timing-legend">
|
|
||||||
${state.timing_audio_read_ms != null ? `
|
|
||||||
<span class="timing-legend-item"><span class="timing-dot timing-audio-read"></span>read ${state.timing_audio_read_ms}ms</span>
|
|
||||||
<span class="timing-legend-item"><span class="timing-dot timing-audio-fft"></span>fft ${state.timing_audio_fft_ms}ms</span>
|
|
||||||
<span class="timing-legend-item"><span class="timing-dot timing-audio-render"></span>render ${state.timing_audio_render_ms}ms</span>
|
|
||||||
` : `
|
|
||||||
${state.timing_extract_ms != null ? `<span class="timing-legend-item"><span class="timing-dot timing-extract"></span>extract ${state.timing_extract_ms}ms</span>` : ''}
|
|
||||||
${state.timing_map_leds_ms != null ? `<span class="timing-legend-item"><span class="timing-dot timing-map"></span>map ${state.timing_map_leds_ms}ms</span>` : ''}
|
|
||||||
${state.timing_smooth_ms != null ? `<span class="timing-legend-item"><span class="timing-dot timing-smooth"></span>smooth ${state.timing_smooth_ms}ms</span>` : ''}
|
|
||||||
`}
|
|
||||||
<span class="timing-legend-item"><span class="timing-dot timing-send"></span>send ${state.timing_send_ms}ms</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
` : ''}
|
` : ''}
|
||||||
<div class="metric">
|
<div class="metric">
|
||||||
<div class="metric-label">${t('device.metrics.frames')}</div>
|
<div class="metric-label">${t('device.metrics.frames')}</div>
|
||||||
<div class="metric-value">${metrics.frames_processed || 0}</div>
|
<div class="metric-value" data-tm="frames">---</div>
|
||||||
</div>
|
</div>
|
||||||
${state.needs_keepalive !== false ? `
|
${state.needs_keepalive !== false ? `
|
||||||
<div class="metric">
|
<div class="metric">
|
||||||
<div class="metric-label">${t('device.metrics.keepalive')}</div>
|
<div class="metric-label">${t('device.metrics.keepalive')}</div>
|
||||||
<div class="metric-value">${state.frames_keepalive ?? '-'}</div>
|
<div class="metric-value" data-tm="keepalive">---</div>
|
||||||
</div>
|
</div>
|
||||||
` : ''}
|
` : ''}
|
||||||
<div class="metric">
|
<div class="metric">
|
||||||
<div class="metric-label">${t('device.metrics.errors')}</div>
|
<div class="metric-label">${t('device.metrics.errors')}</div>
|
||||||
<div class="metric-value">${metrics.errors_count || 0}</div>
|
<div class="metric-value" data-tm="errors">---</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="metric">
|
<div class="metric">
|
||||||
<div class="metric-label">${t('device.metrics.uptime')}</div>
|
<div class="metric-label">${t('device.metrics.uptime')}</div>
|
||||||
<div class="metric-value">${formatUptime(metrics.uptime_seconds)}</div>
|
<div class="metric-value" data-tm="uptime">---</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
` : ''}
|
` : ''}
|
||||||
|
|||||||
Reference in New Issue
Block a user