Fix WLED LED stutters: restore DDP PUSH flag, skip HTTP during streaming

Three changes to eliminate periodic LED stutters on high-LED-count
WLED devices:

1. DDP PUSH flag: re-enable on the last packet of each frame so WLED
   waits for the complete frame before rendering (prevents tearing
   from partial multi-packet frames).

2. Health check: skip HTTP probe while a target is actively streaming
   to the device — the device is clearly online and the HTTP request
   to the ESP causes LED output to stutter.

3. Brightness polling: cache the value after first fetch and reuse it
   on subsequent 2-second UI refreshes instead of hitting the ESP
   every cycle.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-16 16:48:08 +03:00
parent afb20f2dac
commit 350dafb1e8
3 changed files with 34 additions and 6 deletions

View File

@@ -223,11 +223,10 @@ class DDPClient:
# Increment sequence number
self._sequence = (self._sequence + 1) % 256
# Build and send packet (no PUSH flag — WLED 0.15.x
# handles DDP without it; adding PUSH broke rendering)
# Set PUSH flag on the last packet to signal frame completion
packet = self._build_ddp_packet(
chunk, offset=start,
sequence=self._sequence, push=False,
sequence=self._sequence, push=is_last,
)
self._transport.sendto(packet)
@@ -273,10 +272,11 @@ class DDPClient:
start = i * bytes_per_packet
end = min(start + bytes_per_packet, total_bytes)
chunk = pixel_bytes[start:end]
is_last = (i == num_packets - 1)
self._sequence = (self._sequence + 1) % 256
packet = self._build_ddp_packet(
chunk, offset=start,
sequence=self._sequence, push=False,
sequence=self._sequence, push=is_last,
)
self._transport.sendto(packet)

View File

@@ -1002,6 +1002,13 @@ class ProcessorManager:
state.health_task.cancel()
state.health_task = None
def _device_is_processing(self, device_id: str) -> bool:
"""Check if any target is actively streaming to this device."""
return any(
t.is_running for t in self._targets.values()
if t.device_id == device_id
)
async def _health_check_loop(self, device_id: str):
"""Background loop that periodically checks a WLED device via GET /json/info."""
state = self._devices.get(device_id)
@@ -1012,7 +1019,14 @@ class ProcessorManager:
try:
while self._health_monitoring_active:
# Skip health check while actively streaming — the device is
# clearly online and the HTTP request causes LED stutters
if not self._device_is_processing(device_id):
await self._check_device_health(device_id)
else:
# Mark as online since we're successfully sending frames
if state.health:
state.health.online = True
await asyncio.sleep(check_interval)
except asyncio.CancelledError:
pass

View File

@@ -4562,8 +4562,22 @@ async function loadTargetsTab() {
devicesWithState.forEach(device => {
attachDeviceListeners(device.id);
if ((device.capabilities || []).includes('brightness_control')) {
// Only fetch from device if we don't have a cached value yet —
// avoids HTTP requests to the ESP every 2s which cause LED stutters
if (device.id in _deviceBrightnessCache) {
const bri = _deviceBrightnessCache[device.id];
const slider = document.querySelector(`[data-device-brightness="${device.id}"]`);
if (slider) {
slider.value = bri;
slider.title = Math.round(bri / 255 * 100) + '%';
slider.disabled = false;
}
const wrap = document.querySelector(`[data-brightness-wrap="${device.id}"]`);
if (wrap) wrap.classList.remove('brightness-loading');
} else {
fetchDeviceBrightness(device.id);
}
}
});
// Manage KC WebSockets: connect for processing, disconnect for stopped