Optimize processing pipeline and fix multi-target crash
Performance optimizations across 5 phases: - Saturation filter: float32 → int32 integer math (~2-3x faster) - Frame interpolation: pre-allocated uint16 scratch buffers - Color correction: single-pass cv2.LUT instead of 3 channel lookups - DDP: numpy vectorized color reorder + pre-allocated RGBW buffer - Calibration boundaries: vectorized with np.arange + np.maximum - wled_client: vectorized pixel validation and HTTP pixel list - _fit_to_device: cached linspace arrays (now per-instance) - Diagnostic lists: bounded deque(maxlen=...) instead of unbounded list - Health checks: adaptive intervals (10s streaming, 60s idle) - Profile engine: poll interval 3s → 1s Bug fixes: - Fix deque slicing crash killing targets when multiple run in parallel (deque doesn't support [-1:] or [:5] slice syntax unlike list) - Fix numpy array boolean ambiguity in send_pixels() validation - Persist fatal processing loop errors to metrics for API visibility - Move _fit_to_device cache from class-level to instance-level to prevent cross-target cache thrashing Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -52,6 +52,9 @@ class DDPClient:
|
||||
self._protocol = None
|
||||
self._sequence = 0
|
||||
self._buses: List[BusConfig] = []
|
||||
# Pre-allocated RGBW buffer (resized on demand)
|
||||
self._rgbw_buf: Optional[np.ndarray] = None
|
||||
self._rgbw_buf_n: int = 0
|
||||
|
||||
async def connect(self):
|
||||
"""Establish UDP connection."""
|
||||
@@ -136,26 +139,23 @@ class DDPClient:
|
||||
|
||||
return header + rgb_data
|
||||
|
||||
def _reorder_pixels(
|
||||
self,
|
||||
pixels: List[Tuple[int, int, int]],
|
||||
) -> List[Tuple[int, int, int]]:
|
||||
"""Apply per-bus color order reordering.
|
||||
def _reorder_pixels_numpy(self, pixel_array: np.ndarray) -> np.ndarray:
|
||||
"""Apply per-bus color order reordering using numpy fancy indexing.
|
||||
|
||||
WLED may not apply per-bus color order conversion for DDP data on
|
||||
all buses (observed in multi-bus setups). We reorder pixel channels
|
||||
here so the hardware receives the correct byte order directly.
|
||||
|
||||
Args:
|
||||
pixels: List of (R, G, B) tuples in standard RGB order
|
||||
pixel_array: (N, 3) uint8 numpy array in RGB order
|
||||
|
||||
Returns:
|
||||
List of reordered tuples matching each bus's hardware color order
|
||||
Reordered array (may be a view or copy depending on buses)
|
||||
"""
|
||||
if not self._buses:
|
||||
return pixels
|
||||
return pixel_array
|
||||
|
||||
result = list(pixels)
|
||||
result = pixel_array.copy()
|
||||
for bus in self._buses:
|
||||
order_map = COLOR_ORDER_MAP.get(bus.color_order)
|
||||
if not order_map or order_map == (0, 1, 2):
|
||||
@@ -163,10 +163,7 @@ class DDPClient:
|
||||
|
||||
start = bus.start
|
||||
end = min(bus.start + bus.length, len(result))
|
||||
for i in range(start, end):
|
||||
r, g, b = result[i]
|
||||
rgb = (r, g, b)
|
||||
result[i] = (rgb[order_map[0]], rgb[order_map[1]], rgb[order_map[2]])
|
||||
result[start:end] = result[start:end][:, order_map]
|
||||
|
||||
return result
|
||||
|
||||
@@ -197,8 +194,12 @@ class DDPClient:
|
||||
bpp = 4 if self.rgbw else 3 # bytes per pixel
|
||||
pixel_array = np.array(pixels, dtype=np.uint8)
|
||||
if self.rgbw:
|
||||
white = np.zeros((pixel_array.shape[0], 1), dtype=np.uint8)
|
||||
pixel_array = np.hstack((pixel_array, white))
|
||||
n = pixel_array.shape[0]
|
||||
if n != self._rgbw_buf_n:
|
||||
self._rgbw_buf = np.zeros((n, 4), dtype=np.uint8)
|
||||
self._rgbw_buf_n = n
|
||||
self._rgbw_buf[:, :3] = pixel_array
|
||||
pixel_array = self._rgbw_buf
|
||||
pixel_bytes = pixel_array.tobytes()
|
||||
|
||||
total_bytes = len(pixel_bytes)
|
||||
@@ -256,10 +257,14 @@ class DDPClient:
|
||||
if not self._transport:
|
||||
raise RuntimeError("DDP client not connected")
|
||||
|
||||
# Handle RGBW: insert zero white channel column
|
||||
# Handle RGBW: copy RGB into pre-allocated (N, 4) buffer
|
||||
if self.rgbw:
|
||||
white = np.zeros((pixel_array.shape[0], 1), dtype=np.uint8)
|
||||
pixel_array = np.hstack((pixel_array, white))
|
||||
n = pixel_array.shape[0]
|
||||
if n != self._rgbw_buf_n:
|
||||
self._rgbw_buf = np.zeros((n, 4), dtype=np.uint8)
|
||||
self._rgbw_buf_n = n
|
||||
self._rgbw_buf[:, :3] = pixel_array
|
||||
pixel_array = self._rgbw_buf
|
||||
|
||||
pixel_bytes = pixel_array.tobytes()
|
||||
|
||||
|
||||
@@ -333,18 +333,25 @@ class WLEDClient(LEDClient):
|
||||
RuntimeError: If request fails
|
||||
"""
|
||||
# Validate inputs
|
||||
if not pixels:
|
||||
raise ValueError("Pixels list cannot be empty")
|
||||
if isinstance(pixels, np.ndarray):
|
||||
if pixels.size == 0:
|
||||
raise ValueError("Pixels array cannot be empty")
|
||||
pixel_arr = pixels
|
||||
else:
|
||||
if not pixels:
|
||||
raise ValueError("Pixels list cannot be empty")
|
||||
pixel_arr = np.array(pixels, dtype=np.int16)
|
||||
|
||||
if not 0 <= brightness <= 255:
|
||||
raise ValueError(f"Brightness must be 0-255, got {brightness}")
|
||||
|
||||
# Validate pixel values
|
||||
validated_pixels = []
|
||||
for i, (r, g, b) in enumerate(pixels):
|
||||
if not (0 <= r <= 255 and 0 <= g <= 255 and 0 <= b <= 255):
|
||||
raise ValueError(f"Invalid RGB values at index {i}: ({r}, {g}, {b})")
|
||||
validated_pixels.append((int(r), int(g), int(b)))
|
||||
# Validate pixel values using vectorized bounds check
|
||||
if pixel_arr.dtype != np.uint8:
|
||||
if np.any((pixel_arr < 0) | (pixel_arr > 255)):
|
||||
bad_mask = np.any((pixel_arr < 0) | (pixel_arr > 255), axis=1)
|
||||
idx = int(np.argmax(bad_mask))
|
||||
raise ValueError(f"Invalid RGB values at index {idx}: {tuple(pixel_arr[idx])}")
|
||||
validated_pixels = pixel_arr.astype(np.uint8) if pixel_arr.dtype != np.uint8 else pixel_arr
|
||||
|
||||
# Use DDP protocol if enabled
|
||||
if self.use_ddp and self._ddp_client:
|
||||
@@ -354,33 +361,24 @@ class WLEDClient(LEDClient):
|
||||
|
||||
async def _send_pixels_ddp(
|
||||
self,
|
||||
pixels: List[Tuple[int, int, int]],
|
||||
pixels: np.ndarray,
|
||||
brightness: int = 255,
|
||||
) -> bool:
|
||||
"""Send pixels via DDP protocol.
|
||||
|
||||
Args:
|
||||
pixels: List of (R, G, B) tuples
|
||||
pixels: (N, 3) uint8 numpy array of RGB values
|
||||
brightness: Global brightness (0-255)
|
||||
|
||||
Returns:
|
||||
True if successful
|
||||
"""
|
||||
try:
|
||||
# Apply brightness to pixels
|
||||
if brightness < 255:
|
||||
brightness_factor = brightness / 255.0
|
||||
pixels = [
|
||||
(
|
||||
int(r * brightness_factor),
|
||||
int(g * brightness_factor),
|
||||
int(b * brightness_factor)
|
||||
)
|
||||
for r, g, b in pixels
|
||||
]
|
||||
pixels = (pixels.astype(np.uint16) * brightness >> 8).astype(np.uint8)
|
||||
|
||||
logger.debug(f"Sending {len(pixels)} LEDs via DDP")
|
||||
await self._ddp_client.send_pixels(pixels)
|
||||
self._ddp_client.send_pixels_numpy(pixels)
|
||||
logger.debug(f"Successfully sent pixel colors via DDP")
|
||||
return True
|
||||
|
||||
@@ -390,14 +388,14 @@ class WLEDClient(LEDClient):
|
||||
|
||||
async def _send_pixels_http(
|
||||
self,
|
||||
pixels: List[Tuple[int, int, int]],
|
||||
pixels: np.ndarray,
|
||||
brightness: int = 255,
|
||||
segment_id: int = 0,
|
||||
) -> bool:
|
||||
"""Send pixels via HTTP JSON API.
|
||||
|
||||
Args:
|
||||
pixels: List of (R, G, B) tuples
|
||||
pixels: (N, 3) uint8 numpy array of RGB values
|
||||
brightness: Global brightness (0-255)
|
||||
segment_id: Segment ID to update
|
||||
|
||||
@@ -406,9 +404,8 @@ class WLEDClient(LEDClient):
|
||||
"""
|
||||
try:
|
||||
# Build indexed pixel array: [led_index, r, g, b, ...]
|
||||
indexed_pixels = []
|
||||
for i, (r, g, b) in enumerate(pixels):
|
||||
indexed_pixels.extend([i, int(r), int(g), int(b)])
|
||||
indices = np.arange(len(pixels), dtype=np.int32).reshape(-1, 1)
|
||||
indexed_pixels = np.hstack([indices, pixels.astype(np.int32)]).ravel().tolist()
|
||||
|
||||
# Build WLED JSON state
|
||||
payload = {
|
||||
|
||||
Reference in New Issue
Block a user