diff --git a/server/src/wled_controller/api/routes/picture_targets.py b/server/src/wled_controller/api/routes/picture_targets.py index c9e4c0d..9907ce8 100644 --- a/server/src/wled_controller/api/routes/picture_targets.py +++ b/server/src/wled_controller/api/routes/picture_targets.py @@ -102,6 +102,7 @@ def _target_to_response(target) -> PictureTargetResponse: state_check_interval=target.state_check_interval, min_brightness_threshold=target.min_brightness_threshold, adaptive_fps=target.adaptive_fps, + protocol=target.protocol, description=target.description, auto_start=target.auto_start, created_at=target.created_at, @@ -163,6 +164,7 @@ async def create_target( state_check_interval=data.state_check_interval, min_brightness_threshold=data.min_brightness_threshold, adaptive_fps=data.adaptive_fps, + protocol=data.protocol, picture_source_id=data.picture_source_id, key_colors_settings=kc_settings, description=data.description, @@ -282,6 +284,7 @@ async def update_target( state_check_interval=data.state_check_interval, min_brightness_threshold=data.min_brightness_threshold, adaptive_fps=data.adaptive_fps, + protocol=data.protocol, key_colors_settings=kc_settings, description=data.description, auto_start=data.auto_start, diff --git a/server/src/wled_controller/api/schemas/picture_targets.py b/server/src/wled_controller/api/schemas/picture_targets.py index 4095842..52d8f65 100644 --- a/server/src/wled_controller/api/schemas/picture_targets.py +++ b/server/src/wled_controller/api/schemas/picture_targets.py @@ -60,6 +60,7 @@ class PictureTargetCreate(BaseModel): state_check_interval: int = Field(default=DEFAULT_STATE_CHECK_INTERVAL, description="Device health check interval (5-600s)", ge=5, le=600) min_brightness_threshold: int = Field(default=0, ge=0, le=254, description="Min brightness threshold (0=disabled); below this → off") adaptive_fps: bool = Field(default=False, description="Auto-reduce FPS when device is unresponsive") + protocol: str = Field(default="ddp", pattern="^(ddp|http)$", description="Send protocol: ddp (UDP) or http (JSON API)") # KC target fields picture_source_id: str = Field(default="", description="Picture source ID (for key_colors targets)") key_colors_settings: Optional[KeyColorsSettingsSchema] = Field(None, description="Key colors settings (for key_colors targets)") @@ -80,6 +81,7 @@ class PictureTargetUpdate(BaseModel): state_check_interval: Optional[int] = Field(None, description="Health check interval (5-600s)", ge=5, le=600) min_brightness_threshold: Optional[int] = Field(None, ge=0, le=254, description="Min brightness threshold (0=disabled); below this → off") adaptive_fps: Optional[bool] = Field(None, description="Auto-reduce FPS when device is unresponsive") + protocol: Optional[str] = Field(None, pattern="^(ddp|http)$", description="Send protocol: ddp (UDP) or http (JSON API)") # KC target fields picture_source_id: Optional[str] = Field(None, description="Picture source ID (for key_colors targets)") key_colors_settings: Optional[KeyColorsSettingsSchema] = Field(None, description="Key colors settings (for key_colors targets)") @@ -102,6 +104,7 @@ class PictureTargetResponse(BaseModel): state_check_interval: int = Field(default=DEFAULT_STATE_CHECK_INTERVAL, description="Health check interval (s)") min_brightness_threshold: int = Field(default=0, description="Min brightness threshold (0=disabled)") adaptive_fps: bool = Field(default=False, description="Auto-reduce FPS when device is unresponsive") + protocol: str = Field(default="ddp", description="Send protocol (ddp or http)") # KC target fields picture_source_id: str = Field(default="", description="Picture source ID (key_colors)") key_colors_settings: Optional[KeyColorsSettingsSchema] = Field(None, description="Key colors settings") diff --git a/server/src/wled_controller/core/processing/processor_manager.py b/server/src/wled_controller/core/processing/processor_manager.py index ca255ca..7008d54 100644 --- a/server/src/wled_controller/core/processing/processor_manager.py +++ b/server/src/wled_controller/core/processing/processor_manager.py @@ -323,6 +323,7 @@ class ProcessorManager: brightness_value_source_id: str = "", min_brightness_threshold: int = 0, adaptive_fps: bool = False, + protocol: str = "ddp", ): """Register a WLED target processor.""" if target_id in self._processors: @@ -340,6 +341,7 @@ class ProcessorManager: brightness_value_source_id=brightness_value_source_id, min_brightness_threshold=min_brightness_threshold, adaptive_fps=adaptive_fps, + protocol=protocol, ctx=self._build_context(), ) self._processors[target_id] = proc diff --git a/server/src/wled_controller/core/processing/wled_target_processor.py b/server/src/wled_controller/core/processing/wled_target_processor.py index f792f6c..f143ef0 100644 --- a/server/src/wled_controller/core/processing/wled_target_processor.py +++ b/server/src/wled_controller/core/processing/wled_target_processor.py @@ -39,6 +39,7 @@ class WledTargetProcessor(TargetProcessor): brightness_value_source_id: str = "", min_brightness_threshold: int = 0, adaptive_fps: bool = False, + protocol: str = "ddp", ctx: TargetContext = None, ): super().__init__(target_id, ctx) @@ -50,6 +51,7 @@ class WledTargetProcessor(TargetProcessor): self._brightness_vs_id = brightness_value_source_id self._min_brightness_threshold = min_brightness_threshold self._adaptive_fps = adaptive_fps + self._protocol = protocol # Adaptive FPS / liveness probe runtime state self._effective_fps: int = self._target_fps @@ -95,7 +97,7 @@ class WledTargetProcessor(TargetProcessor): try: self._led_client = create_led_client( device_info.device_type, device_info.device_url, - use_ddp=True, led_count=device_info.led_count, + use_ddp=(self._protocol == "ddp"), led_count=device_info.led_count, baud_rate=device_info.baud_rate, send_latency_ms=device_info.send_latency_ms, rgbw=device_info.rgbw, @@ -373,6 +375,7 @@ class WledTargetProcessor(TargetProcessor): "errors": [metrics.last_error] if metrics.last_error else [], "device_streaming_reachable": self._device_reachable if self._is_running else None, "fps_effective": self._effective_fps if self._is_running else None, + "protocol": self._protocol, } def get_metrics(self) -> dict: diff --git a/server/src/wled_controller/static/js/features/targets.js b/server/src/wled_controller/static/js/features/targets.js index 44a187d..04d9533 100644 --- a/server/src/wled_controller/static/js/features/targets.js +++ b/server/src/wled_controller/static/js/features/targets.js @@ -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
${ICON_LED} ${escapeHtml(deviceName)} ${ICON_FPS} ${target.fps || 30} + ${device?.device_type === 'wled' || !device ? `${target.protocol === 'http' ? '🌐' : '📡'} ${(target.protocol || 'ddp').toUpperCase()}` : `🔌 ${t('targets.protocol.serial')}`} 🎞️ ${cssSummary} ${bvs ? `${getValueSourceIcon(bvs.source_type)} ${escapeHtml(bvs.name)}` : ''} ${target.min_brightness_threshold > 0 ? `🔅 <${target.min_brightness_threshold} → off` : ''} diff --git a/server/src/wled_controller/static/locales/en.json b/server/src/wled_controller/static/locales/en.json index 3b53aad..853b8ae 100644 --- a/server/src/wled_controller/static/locales/en.json +++ b/server/src/wled_controller/static/locales/en.json @@ -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)", diff --git a/server/src/wled_controller/static/locales/ru.json b/server/src/wled_controller/static/locales/ru.json index ba5b58a..fc49e4e 100644 --- a/server/src/wled_controller/static/locales/ru.json +++ b/server/src/wled_controller/static/locales/ru.json @@ -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)", diff --git a/server/src/wled_controller/static/locales/zh.json b/server/src/wled_controller/static/locales/zh.json index 8fb3b6f..e146415 100644 --- a/server/src/wled_controller/static/locales/zh.json +++ b/server/src/wled_controller/static/locales/zh.json @@ -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)", diff --git a/server/src/wled_controller/storage/picture_target_store.py b/server/src/wled_controller/storage/picture_target_store.py index a545278..ba7274d 100644 --- a/server/src/wled_controller/storage/picture_target_store.py +++ b/server/src/wled_controller/storage/picture_target_store.py @@ -101,6 +101,7 @@ class PictureTargetStore: state_check_interval: int = DEFAULT_STATE_CHECK_INTERVAL, min_brightness_threshold: int = 0, adaptive_fps: bool = False, + protocol: str = "ddp", key_colors_settings: Optional[KeyColorsSettings] = None, description: Optional[str] = None, picture_source_id: str = "", @@ -135,6 +136,7 @@ class PictureTargetStore: state_check_interval=state_check_interval, min_brightness_threshold=min_brightness_threshold, adaptive_fps=adaptive_fps, + protocol=protocol, description=description, auto_start=auto_start, created_at=now, @@ -173,6 +175,7 @@ class PictureTargetStore: state_check_interval: Optional[int] = None, min_brightness_threshold: Optional[int] = None, adaptive_fps: Optional[bool] = None, + protocol: Optional[str] = None, key_colors_settings: Optional[KeyColorsSettings] = None, description: Optional[str] = None, auto_start: Optional[bool] = None, @@ -203,6 +206,7 @@ class PictureTargetStore: state_check_interval=state_check_interval, min_brightness_threshold=min_brightness_threshold, adaptive_fps=adaptive_fps, + protocol=protocol, key_colors_settings=key_colors_settings, description=description, auto_start=auto_start, diff --git a/server/src/wled_controller/storage/wled_picture_target.py b/server/src/wled_controller/storage/wled_picture_target.py index 16c9b4b..45719fb 100644 --- a/server/src/wled_controller/storage/wled_picture_target.py +++ b/server/src/wled_controller/storage/wled_picture_target.py @@ -21,6 +21,7 @@ class WledPictureTarget(PictureTarget): state_check_interval: int = DEFAULT_STATE_CHECK_INTERVAL min_brightness_threshold: int = 0 # brightness below this → 0 (disabled when 0) adaptive_fps: bool = False # auto-reduce FPS when device is unresponsive + protocol: str = "ddp" # "ddp" (UDP) or "http" (JSON API) def register_with_manager(self, manager) -> None: """Register this WLED target with the processor manager.""" @@ -35,6 +36,7 @@ class WledPictureTarget(PictureTarget): brightness_value_source_id=self.brightness_value_source_id, min_brightness_threshold=self.min_brightness_threshold, adaptive_fps=self.adaptive_fps, + protocol=self.protocol, ) def sync_with_manager(self, manager, *, settings_changed: bool, @@ -60,7 +62,7 @@ class WledPictureTarget(PictureTarget): def update_fields(self, *, name=None, device_id=None, color_strip_source_id=None, brightness_value_source_id=None, fps=None, keepalive_interval=None, state_check_interval=None, - min_brightness_threshold=None, adaptive_fps=None, + min_brightness_threshold=None, adaptive_fps=None, protocol=None, description=None, auto_start=None, **_kwargs) -> None: """Apply mutable field updates for WLED targets.""" super().update_fields(name=name, description=description, auto_start=auto_start) @@ -80,6 +82,8 @@ class WledPictureTarget(PictureTarget): self.min_brightness_threshold = min_brightness_threshold if adaptive_fps is not None: self.adaptive_fps = adaptive_fps + if protocol is not None: + self.protocol = protocol @property def has_picture_source(self) -> bool: @@ -96,6 +100,7 @@ class WledPictureTarget(PictureTarget): d["state_check_interval"] = self.state_check_interval d["min_brightness_threshold"] = self.min_brightness_threshold d["adaptive_fps"] = self.adaptive_fps + d["protocol"] = self.protocol return d @classmethod @@ -123,6 +128,7 @@ class WledPictureTarget(PictureTarget): state_check_interval=data.get("state_check_interval", DEFAULT_STATE_CHECK_INTERVAL), min_brightness_threshold=data.get("min_brightness_threshold", 0), adaptive_fps=data.get("adaptive_fps", False), + protocol=data.get("protocol", "ddp"), description=data.get("description"), auto_start=data.get("auto_start", False), created_at=datetime.fromisoformat(data.get("created_at", datetime.utcnow().isoformat())), diff --git a/server/src/wled_controller/templates/modals/target-editor.html b/server/src/wled_controller/templates/modals/target-editor.html index a717ea0..becc8d9 100644 --- a/server/src/wled_controller/templates/modals/target-editor.html +++ b/server/src/wled_controller/templates/modals/target-editor.html @@ -44,18 +44,6 @@
-
-
- - -
- - -
-
-
+
-
- - + +
@@ -96,6 +84,35 @@
+
+ Specific Settings +
+
+
+ + +
+ + +
+ +
+
+ + +
+ + +
+
+
+