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:
@@ -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)
|
||||
|
||||
|
||||
@@ -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:
|
||||
await self._check_device_health(device_id)
|
||||
# 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
|
||||
|
||||
@@ -4562,7 +4562,21 @@ async function loadTargetsTab() {
|
||||
devicesWithState.forEach(device => {
|
||||
attachDeviceListeners(device.id);
|
||||
if ((device.capabilities || []).includes('brightness_control')) {
|
||||
fetchDeviceBrightness(device.id);
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user