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
|
# Increment sequence number
|
||||||
self._sequence = (self._sequence + 1) % 256
|
self._sequence = (self._sequence + 1) % 256
|
||||||
|
|
||||||
# Build and send packet (no PUSH flag — WLED 0.15.x
|
# Set PUSH flag on the last packet to signal frame completion
|
||||||
# handles DDP without it; adding PUSH broke rendering)
|
|
||||||
packet = self._build_ddp_packet(
|
packet = self._build_ddp_packet(
|
||||||
chunk, offset=start,
|
chunk, offset=start,
|
||||||
sequence=self._sequence, push=False,
|
sequence=self._sequence, push=is_last,
|
||||||
)
|
)
|
||||||
self._transport.sendto(packet)
|
self._transport.sendto(packet)
|
||||||
|
|
||||||
@@ -273,10 +272,11 @@ class DDPClient:
|
|||||||
start = i * bytes_per_packet
|
start = i * bytes_per_packet
|
||||||
end = min(start + bytes_per_packet, total_bytes)
|
end = min(start + bytes_per_packet, total_bytes)
|
||||||
chunk = pixel_bytes[start:end]
|
chunk = pixel_bytes[start:end]
|
||||||
|
is_last = (i == num_packets - 1)
|
||||||
self._sequence = (self._sequence + 1) % 256
|
self._sequence = (self._sequence + 1) % 256
|
||||||
packet = self._build_ddp_packet(
|
packet = self._build_ddp_packet(
|
||||||
chunk, offset=start,
|
chunk, offset=start,
|
||||||
sequence=self._sequence, push=False,
|
sequence=self._sequence, push=is_last,
|
||||||
)
|
)
|
||||||
self._transport.sendto(packet)
|
self._transport.sendto(packet)
|
||||||
|
|
||||||
|
|||||||
@@ -1002,6 +1002,13 @@ class ProcessorManager:
|
|||||||
state.health_task.cancel()
|
state.health_task.cancel()
|
||||||
state.health_task = None
|
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):
|
async def _health_check_loop(self, device_id: str):
|
||||||
"""Background loop that periodically checks a WLED device via GET /json/info."""
|
"""Background loop that periodically checks a WLED device via GET /json/info."""
|
||||||
state = self._devices.get(device_id)
|
state = self._devices.get(device_id)
|
||||||
@@ -1012,7 +1019,14 @@ class ProcessorManager:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
while self._health_monitoring_active:
|
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)
|
await asyncio.sleep(check_interval)
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -4562,7 +4562,21 @@ async function loadTargetsTab() {
|
|||||||
devicesWithState.forEach(device => {
|
devicesWithState.forEach(device => {
|
||||||
attachDeviceListeners(device.id);
|
attachDeviceListeners(device.id);
|
||||||
if ((device.capabilities || []).includes('brightness_control')) {
|
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