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