Add frame-change detection, keepalive, current FPS, and compact metrics UI

Skip redundant processing/DDP sends when screen is static using object
identity comparison. Add configurable standby interval to periodically
resend last frame keeping WLED in live mode. Track frames skipped,
keepalive count, and current FPS (rolling 1-second send count). Always
use DDP regardless of LED count. Compact metrics grid with label-value
rows and remove Skipped from UI display.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-13 15:17:14 +03:00
parent 8d5ebc92ee
commit 3100b0d979
9 changed files with 154 additions and 50 deletions

View File

@@ -3821,6 +3821,8 @@ async function showTargetEditor(targetId = null) {
document.getElementById('target-editor-interpolation').value = target.settings?.interpolation_mode ?? 'average';
document.getElementById('target-editor-smoothing').value = target.settings?.smoothing ?? 0.3;
document.getElementById('target-editor-smoothing-value').textContent = target.settings?.smoothing ?? 0.3;
document.getElementById('target-editor-standby-interval').value = target.settings?.standby_interval ?? 1.0;
document.getElementById('target-editor-standby-interval-value').textContent = target.settings?.standby_interval ?? 1.0;
document.getElementById('target-editor-title').textContent = t('targets.edit');
} else {
// Creating new target
@@ -3834,6 +3836,8 @@ async function showTargetEditor(targetId = null) {
document.getElementById('target-editor-interpolation').value = 'average';
document.getElementById('target-editor-smoothing').value = 0.3;
document.getElementById('target-editor-smoothing-value').textContent = '0.3';
document.getElementById('target-editor-standby-interval').value = 1.0;
document.getElementById('target-editor-standby-interval-value').textContent = '1.0';
document.getElementById('target-editor-title').textContent = t('targets.add');
}
@@ -3845,6 +3849,7 @@ async function showTargetEditor(targetId = null) {
border_width: document.getElementById('target-editor-border-width').value,
interpolation: document.getElementById('target-editor-interpolation').value,
smoothing: document.getElementById('target-editor-smoothing').value,
standby_interval: document.getElementById('target-editor-standby-interval').value,
};
const modal = document.getElementById('target-editor-modal');
@@ -3868,7 +3873,8 @@ function isTargetEditorDirty() {
document.getElementById('target-editor-fps').value !== targetEditorInitialValues.fps ||
document.getElementById('target-editor-border-width').value !== targetEditorInitialValues.border_width ||
document.getElementById('target-editor-interpolation').value !== targetEditorInitialValues.interpolation ||
document.getElementById('target-editor-smoothing').value !== targetEditorInitialValues.smoothing
document.getElementById('target-editor-smoothing').value !== targetEditorInitialValues.smoothing ||
document.getElementById('target-editor-standby-interval').value !== targetEditorInitialValues.standby_interval
);
}
@@ -3896,6 +3902,7 @@ async function saveTargetEditor() {
const borderWidth = parseInt(document.getElementById('target-editor-border-width').value) || 10;
const interpolation = document.getElementById('target-editor-interpolation').value;
const smoothing = parseFloat(document.getElementById('target-editor-smoothing').value);
const standbyInterval = parseFloat(document.getElementById('target-editor-standby-interval').value);
const errorEl = document.getElementById('target-editor-error');
if (!name) {
@@ -3913,6 +3920,7 @@ async function saveTargetEditor() {
border_width: borderWidth,
interpolation_mode: interpolation,
smoothing: smoothing,
standby_interval: standbyInterval,
},
};
@@ -4175,24 +4183,32 @@ function createTargetCard(target, deviceMap, sourceMap) {
${isProcessing ? `
<div class="metrics-grid">
<div class="metric">
<div class="metric-value">${state.fps_actual?.toFixed(1) || '0.0'}</div>
<div class="metric-label">${t('device.metrics.actual_fps')}</div>
<div class="metric-value">${state.fps_actual?.toFixed(1) || '0.0'}</div>
</div>
<div class="metric">
<div class="metric-label">${t('device.metrics.current_fps')}</div>
<div class="metric-value">${state.fps_current ?? '-'}</div>
</div>
<div class="metric">
<div class="metric-value">${state.fps_target || 0}</div>
<div class="metric-label">${t('device.metrics.target_fps')}</div>
<div class="metric-value">${state.fps_target || 0}</div>
</div>
<div class="metric">
<div class="metric-value">${state.fps_potential?.toFixed(0) || '-'}</div>
<div class="metric-label">${t('device.metrics.potential_fps')}</div>
<div class="metric-value">${state.fps_potential?.toFixed(0) || '-'}</div>
</div>
<div class="metric">
<div class="metric-value">${metrics.frames_processed || 0}</div>
<div class="metric-label">${t('device.metrics.frames')}</div>
<div class="metric-value">${metrics.frames_processed || 0}</div>
</div>
<div class="metric">
<div class="metric-label">${t('device.metrics.keepalive')}</div>
<div class="metric-value">${state.frames_keepalive ?? '-'}</div>
</div>
<div class="metric">
<div class="metric-value">${metrics.errors_count || 0}</div>
<div class="metric-label">${t('device.metrics.errors')}</div>
<div class="metric-value">${metrics.errors_count || 0}</div>
</div>
</div>
` : ''}

View File

@@ -317,6 +317,18 @@
<input type="range" id="target-editor-smoothing" min="0.0" max="1.0" step="0.05" value="0.3" oninput="document.getElementById('target-editor-smoothing-value').textContent = this.value">
</div>
<div class="form-group">
<div class="label-row">
<label for="target-editor-standby-interval">
<span data-i18n="targets.standby_interval">Standby Interval:</span>
<span id="target-editor-standby-interval-value">1.0</span><span>s</span>
</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="targets.standby_interval.hint">How often to resend the last frame when the screen is static, to keep WLED in live mode (0.5-5.0s)</small>
<input type="range" id="target-editor-standby-interval" min="0.5" max="5.0" step="0.5" value="1.0" oninput="document.getElementById('target-editor-standby-interval-value').textContent = this.value">
</div>
<div id="target-editor-error" class="error-message" style="display: none;"></div>
</form>
</div>

View File

@@ -129,9 +129,12 @@
"device.started": "Processing started",
"device.stopped": "Processing stopped",
"device.metrics.actual_fps": "Actual FPS",
"device.metrics.current_fps": "Current FPS",
"device.metrics.target_fps": "Target FPS",
"device.metrics.potential_fps": "Potential FPS",
"device.metrics.frames": "Frames",
"device.metrics.frames_skipped": "Skipped",
"device.metrics.keepalive": "Keepalive",
"device.metrics.errors": "Errors",
"device.health.online": "WLED Online",
"device.health.offline": "WLED Offline",
@@ -330,6 +333,8 @@
"targets.interpolation.dominant": "Dominant",
"targets.smoothing": "Smoothing:",
"targets.smoothing.hint": "Temporal blending between frames (0=none, 1=full). Reduces flicker.",
"targets.standby_interval": "Standby Interval:",
"targets.standby_interval.hint": "How often to resend the last frame when the screen is static, keeping WLED in live mode (0.5-5.0s)",
"targets.created": "Target created successfully",
"targets.updated": "Target updated successfully",
"targets.deleted": "Target deleted successfully",

View File

@@ -129,9 +129,12 @@
"device.started": "Обработка запущена",
"device.stopped": "Обработка остановлена",
"device.metrics.actual_fps": "Факт. FPS",
"device.metrics.current_fps": "Текущ. FPS",
"device.metrics.target_fps": "Целев. FPS",
"device.metrics.potential_fps": "Потенц. FPS",
"device.metrics.frames": "Кадры",
"device.metrics.frames_skipped": "Пропущено",
"device.metrics.keepalive": "Keepalive",
"device.metrics.errors": "Ошибки",
"device.health.online": "WLED Онлайн",
"device.health.offline": "WLED Недоступен",
@@ -330,6 +333,8 @@
"targets.interpolation.dominant": "Доминантный",
"targets.smoothing": "Сглаживание:",
"targets.smoothing.hint": "Временное смешивание между кадрами (0=нет, 1=полное). Уменьшает мерцание.",
"targets.standby_interval": "Интервал ожидания:",
"targets.standby_interval.hint": "Как часто повторно отправлять последний кадр при статичном экране для удержания WLED в режиме live (0.5-5.0с)",
"targets.created": "Цель успешно создана",
"targets.updated": "Цель успешно обновлена",
"targets.deleted": "Цель успешно удалена",

View File

@@ -844,28 +844,29 @@ input:-webkit-autofill:focus {
.metrics-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
margin-top: 10px;
grid-template-columns: 1fr 1fr;
gap: 4px 12px;
margin-top: 8px;
}
.metric {
text-align: center;
padding: 10px;
display: flex;
justify-content: space-between;
align-items: center;
padding: 3px 8px;
background: var(--bg-color);
border-radius: 4px;
}
.metric-value {
font-size: 1.5rem;
font-size: 0.9rem;
font-weight: 700;
color: var(--primary-color);
}
.metric-label {
font-size: 0.85rem;
font-size: 0.8rem;
color: #999;
margin-top: 5px;
}
/* Modal Styles */