Add per-target protocol selection (DDP/HTTP) and reorganize target editor

- Add protocol field (ddp/http) to storage, API schemas, routes, processor
- WledTargetProcessor passes protocol to create_led_client(use_ddp=...)
- Target editor: protocol dropdown + keepalive in collapsible Specific Settings
- FPS, brightness threshold, adaptive FPS moved to main form area
- Hide Specific Settings section for serial devices (protocol is WLED-only)
- Card badge: show DDP/HTTP for WLED devices, Serial for serial devices

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-26 20:52:03 +03:00
parent cadef971e7
commit fda040ae18
11 changed files with 87 additions and 21 deletions

View File

@@ -138,6 +138,7 @@ class TargetEditorModal extends Modal {
return {
name: document.getElementById('target-editor-name').value,
device: document.getElementById('target-editor-device').value,
protocol: document.getElementById('target-editor-protocol').value,
css_source: document.getElementById('target-editor-css-source').value,
brightness_vs: document.getElementById('target-editor-brightness-vs').value,
brightness_threshold: document.getElementById('target-editor-brightness-threshold').value,
@@ -200,6 +201,14 @@ function _updateKeepaliveVisibility() {
keepaliveGroup.style.display = caps.includes('standby_required') ? '' : 'none';
}
function _updateSpecificSettingsVisibility() {
const deviceSelect = document.getElementById('target-editor-device');
const selectedDevice = _targetEditorDevices.find(d => d.id === deviceSelect.value);
const isWled = !selectedDevice || selectedDevice.device_type === 'wled';
// Hide entire Specific Settings section for non-WLED devices (protocol + keepalive are WLED-only)
document.getElementById('target-editor-device-settings').style.display = isWled ? '' : 'none';
}
function _updateBrightnessThresholdVisibility() {
// Always visible — threshold considers both brightness source and pixel content
document.getElementById('target-editor-brightness-threshold-group').style.display = '';
@@ -274,6 +283,7 @@ export async function showTargetEditor(targetId = null, cloneData = null) {
document.getElementById('target-editor-brightness-threshold-value').textContent = thresh;
document.getElementById('target-editor-adaptive-fps').checked = target.adaptive_fps ?? false;
document.getElementById('target-editor-protocol').value = target.protocol || 'ddp';
_populateCssDropdown(target.color_strip_source_id || '');
_populateBrightnessVsDropdown(target.brightness_value_source_id || '');
@@ -294,6 +304,7 @@ export async function showTargetEditor(targetId = null, cloneData = null) {
document.getElementById('target-editor-brightness-threshold-value').textContent = cloneThresh;
document.getElementById('target-editor-adaptive-fps').checked = cloneData.adaptive_fps ?? false;
document.getElementById('target-editor-protocol').value = cloneData.protocol || 'ddp';
_populateCssDropdown(cloneData.color_strip_source_id || '');
_populateBrightnessVsDropdown(cloneData.brightness_value_source_id || '');
@@ -311,6 +322,7 @@ export async function showTargetEditor(targetId = null, cloneData = null) {
document.getElementById('target-editor-brightness-threshold-value').textContent = '0';
document.getElementById('target-editor-adaptive-fps').checked = false;
document.getElementById('target-editor-protocol').value = 'ddp';
_populateCssDropdown('');
_populateBrightnessVsDropdown('');
@@ -320,7 +332,7 @@ export async function showTargetEditor(targetId = null, cloneData = null) {
_targetNameManuallyEdited = !!(targetId || cloneData);
document.getElementById('target-editor-name').oninput = () => { _targetNameManuallyEdited = true; };
window._targetAutoName = _autoGenerateTargetName;
deviceSelect.onchange = () => { _updateDeviceInfo(); _updateKeepaliveVisibility(); _updateFpsRecommendation(); _autoGenerateTargetName(); };
deviceSelect.onchange = () => { _updateDeviceInfo(); _updateKeepaliveVisibility(); _updateSpecificSettingsVisibility(); _updateFpsRecommendation(); _autoGenerateTargetName(); };
document.getElementById('target-editor-css-source').onchange = () => { _autoGenerateTargetName(); };
document.getElementById('target-editor-brightness-vs').onchange = () => { _updateBrightnessThresholdVisibility(); };
if (!targetId && !cloneData) _autoGenerateTargetName();
@@ -328,6 +340,7 @@ export async function showTargetEditor(targetId = null, cloneData = null) {
// Show/hide conditional fields
_updateDeviceInfo();
_updateKeepaliveVisibility();
_updateSpecificSettingsVisibility();
_updateFpsRecommendation();
_updateBrightnessThresholdVisibility();
@@ -372,6 +385,7 @@ export async function saveTargetEditor() {
const minBrightnessThreshold = parseInt(document.getElementById('target-editor-brightness-threshold').value) || 0;
const adaptiveFps = document.getElementById('target-editor-adaptive-fps').checked;
const protocol = document.getElementById('target-editor-protocol').value;
const payload = {
name,
@@ -382,6 +396,7 @@ export async function saveTargetEditor() {
fps,
keepalive_interval: standbyInterval,
adaptive_fps: adaptiveFps,
protocol,
};
try {
@@ -858,6 +873,7 @@ export function createTargetCard(target, deviceMap, colorStripSourceMap, valueSo
<div class="stream-card-props">
<span class="stream-card-prop stream-card-link" title="${t('targets.device')}" onclick="event.stopPropagation(); navigateToCard('targets','led','led-devices','data-device-id','${target.device_id}')">${ICON_LED} ${escapeHtml(deviceName)}</span>
<span class="stream-card-prop" title="${t('targets.fps')}">${ICON_FPS} ${target.fps || 30}</span>
${device?.device_type === 'wled' || !device ? `<span class="stream-card-prop" title="${t('targets.protocol')}">${target.protocol === 'http' ? '🌐' : '📡'} ${(target.protocol || 'ddp').toUpperCase()}</span>` : `<span class="stream-card-prop" title="${t('targets.protocol')}">🔌 ${t('targets.protocol.serial')}</span>`}
<span class="stream-card-prop stream-card-prop-full${cssId ? ' stream-card-link' : ''}" title="${t('targets.color_strip_source')}"${cssId ? ` onclick="event.stopPropagation(); navigateToCard('targets','led','led-css','data-css-id','${cssId}')"` : ''}>🎞️ ${cssSummary}</span>
${bvs ? `<span class="stream-card-prop stream-card-prop-full stream-card-link" title="${t('targets.brightness_vs')}" onclick="event.stopPropagation(); navigateToCard('streams','value','value-sources','data-id','${bvsId}')">${getValueSourceIcon(bvs.source_type)} ${escapeHtml(bvs.name)}</span>` : ''}
${target.min_brightness_threshold > 0 ? `<span class="stream-card-prop" title="${t('targets.min_brightness_threshold')}">🔅 &lt;${target.min_brightness_threshold} → off</span>` : ''}

View File

@@ -367,6 +367,7 @@
"targets.section.devices": "💡 Devices",
"targets.section.color_strips": "🎞️ Color Strip Sources",
"targets.section.targets": "⚡ Targets",
"targets.section.specific_settings": "Specific Settings",
"targets.add": "Add Target",
"targets.edit": "Edit Target",
"targets.loading": "Loading targets...",
@@ -915,6 +916,9 @@
"targets.min_brightness_threshold.hint": "Effective output brightness (pixel brightness × device/source brightness) below this value turns LEDs off completely (0 = disabled)",
"targets.adaptive_fps": "Adaptive FPS:",
"targets.adaptive_fps.hint": "Automatically reduce send rate when the device becomes unresponsive, and gradually recover when it stabilizes. Recommended for WiFi devices with weak signal.",
"targets.protocol": "Protocol:",
"targets.protocol.hint": "DDP sends pixels via fast UDP (recommended for most setups). HTTP uses the JSON API — slower but reliable, limited to ~500 LEDs.",
"targets.protocol.serial": "Serial",
"search.open": "Search (Ctrl+K)",
"search.placeholder": "Search entities... (Ctrl+K)",

View File

@@ -367,6 +367,7 @@
"targets.section.devices": "💡 Устройства",
"targets.section.color_strips": "🎞️ Источники цветовых полос",
"targets.section.targets": "⚡ Цели",
"targets.section.specific_settings": "Специальные настройки",
"targets.add": "Добавить Цель",
"targets.edit": "Редактировать Цель",
"targets.loading": "Загрузка целей...",
@@ -915,6 +916,9 @@
"targets.min_brightness_threshold.hint": "Если итоговая яркость (яркость пикселей × яркость устройства/источника) ниже этого значения, светодиоды полностью выключаются (0 = отключено)",
"targets.adaptive_fps": "Адаптивный FPS:",
"targets.adaptive_fps.hint": "Автоматически снижает частоту отправки, когда устройство перестаёт отвечать, и постепенно восстанавливает её при стабилизации. Рекомендуется для WiFi-устройств со слабым сигналом.",
"targets.protocol": "Протокол:",
"targets.protocol.hint": "DDP отправляет пиксели по быстрому UDP (рекомендуется). HTTP использует JSON API — медленнее, но надёжнее, ограничение ~500 LED.",
"targets.protocol.serial": "Serial",
"search.open": "Поиск (Ctrl+K)",
"search.placeholder": "Поиск... (Ctrl+K)",

View File

@@ -367,6 +367,7 @@
"targets.section.devices": "💡 设备",
"targets.section.color_strips": "🎞️ 色带源",
"targets.section.targets": "⚡ 目标",
"targets.section.specific_settings": "特定设置",
"targets.add": "添加目标",
"targets.edit": "编辑目标",
"targets.loading": "正在加载目标...",
@@ -915,6 +916,9 @@
"targets.min_brightness_threshold.hint": "当有效输出亮度(像素亮度 × 设备/源亮度低于此值时LED完全关闭0 = 禁用)",
"targets.adaptive_fps": "自适应FPS",
"targets.adaptive_fps.hint": "当设备无响应时自动降低发送速率稳定后逐步恢复。推荐用于信号较弱的WiFi设备。",
"targets.protocol": "协议:",
"targets.protocol.hint": "DDP通过快速UDP发送像素推荐。HTTP使用JSON API——较慢但可靠限制约500个LED。",
"targets.protocol.serial": "串口",
"search.open": "搜索 (Ctrl+K)",
"search.placeholder": "搜索实体... (Ctrl+K)",