Cache idle LED clients to avoid repeated Arduino resets
Adalight devices trigger a ~3s bootloader reset on every serial connection open. Add a persistent idle client cache in ProcessorManager so calibration test toggles, static color changes, and auto-restore reuse an existing connection instead of creating a new one each time. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -68,6 +68,7 @@ class ProcessorManager:
|
||||
"""Initialize processor manager."""
|
||||
self._devices: Dict[str, DeviceState] = {}
|
||||
self._processors: Dict[str, TargetProcessor] = {}
|
||||
self._idle_clients: Dict[str, object] = {} # device_id -> cached LEDClient
|
||||
self._health_monitoring_active = False
|
||||
self._http_client: Optional[httpx.AsyncClient] = None
|
||||
self._picture_source_store = picture_source_store
|
||||
@@ -181,7 +182,7 @@ class ProcessorManager:
|
||||
|
||||
logger.info(f"Registered device {device_id} with {led_count} LEDs")
|
||||
|
||||
def remove_device(self, device_id: str):
|
||||
async def remove_device(self, device_id: str):
|
||||
"""Unregister a device."""
|
||||
if device_id not in self._devices:
|
||||
raise ValueError(f"Device {device_id} not found")
|
||||
@@ -193,6 +194,7 @@ class ProcessorManager:
|
||||
f"Cannot remove device {device_id}: target {proc.target_id} is using it"
|
||||
)
|
||||
|
||||
await self._close_idle_client(device_id)
|
||||
self._stop_device_health_check(device_id)
|
||||
del self._devices[device_id]
|
||||
logger.info(f"Unregistered device {device_id}")
|
||||
@@ -379,6 +381,10 @@ class ProcessorManager:
|
||||
f"Device {proc.device_id} is already being processed by target {other_id}"
|
||||
)
|
||||
|
||||
# Close cached idle client — processor creates its own connection
|
||||
if isinstance(proc, WledTargetProcessor):
|
||||
await self._close_idle_client(proc.device_id)
|
||||
|
||||
await proc.start()
|
||||
|
||||
async def stop_processing(self, target_id: str):
|
||||
@@ -492,6 +498,42 @@ class ProcessorManager:
|
||||
ds.test_mode_active = False
|
||||
ds.test_mode_edges = {}
|
||||
await self._send_clear_pixels(device_id)
|
||||
await self._close_idle_client(device_id)
|
||||
|
||||
async def _get_idle_client(self, device_id: str):
|
||||
"""Get or create a cached idle LED client for a device.
|
||||
|
||||
Reuses an existing connected client to avoid repeated serial
|
||||
reconnection (which triggers Arduino bootloader reset on Adalight).
|
||||
"""
|
||||
# Prefer a running processor's client (already connected)
|
||||
active = self._find_active_led_client(device_id)
|
||||
if active:
|
||||
return active
|
||||
|
||||
# Reuse cached idle client if still connected
|
||||
cached = self._idle_clients.get(device_id)
|
||||
if cached and cached.is_connected:
|
||||
return cached
|
||||
|
||||
# Create and cache a new client
|
||||
ds = self._devices[device_id]
|
||||
client = create_led_client(
|
||||
ds.device_type, ds.device_url,
|
||||
use_ddp=True, led_count=ds.led_count, baud_rate=ds.baud_rate,
|
||||
)
|
||||
await client.connect()
|
||||
self._idle_clients[device_id] = client
|
||||
return client
|
||||
|
||||
async def _close_idle_client(self, device_id: str) -> None:
|
||||
"""Close and remove the cached idle client for a device."""
|
||||
client = self._idle_clients.pop(device_id, None)
|
||||
if client:
|
||||
try:
|
||||
await client.close()
|
||||
except Exception as e:
|
||||
logger.warning(f"Error closing idle client for {device_id}: {e}")
|
||||
|
||||
async def _send_test_pixels(self, device_id: str) -> None:
|
||||
"""Build and send test pixel array for active test edges."""
|
||||
@@ -513,12 +555,8 @@ class ProcessorManager:
|
||||
pixels = pixels[-offset:] + pixels[:-offset]
|
||||
|
||||
try:
|
||||
active_client = self._find_active_led_client(device_id)
|
||||
if active_client:
|
||||
await active_client.send_pixels(pixels)
|
||||
else:
|
||||
async with create_led_client(ds.device_type, ds.device_url, use_ddp=True, led_count=ds.led_count, baud_rate=ds.baud_rate) as client:
|
||||
await client.send_pixels(pixels)
|
||||
client = await self._get_idle_client(device_id)
|
||||
await client.send_pixels(pixels)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send test pixels for {device_id}: {e}")
|
||||
|
||||
@@ -528,12 +566,8 @@ class ProcessorManager:
|
||||
pixels = [(0, 0, 0)] * ds.led_count
|
||||
|
||||
try:
|
||||
active_client = self._find_active_led_client(device_id)
|
||||
if active_client:
|
||||
await active_client.send_pixels(pixels)
|
||||
else:
|
||||
async with create_led_client(ds.device_type, ds.device_url, use_ddp=True, led_count=ds.led_count, baud_rate=ds.baud_rate) as client:
|
||||
await client.send_pixels(pixels)
|
||||
client = await self._get_idle_client(device_id)
|
||||
await client.send_pixels(pixels)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to clear pixels for {device_id}: {e}")
|
||||
|
||||
@@ -560,6 +594,19 @@ class ProcessorManager:
|
||||
return proc.device_id
|
||||
return None
|
||||
|
||||
async def send_static_color(self, device_id: str, color: Tuple[int, int, int]) -> None:
|
||||
"""Send a solid color to a device via the cached idle client."""
|
||||
import numpy as np
|
||||
ds = self._devices.get(device_id)
|
||||
if not ds:
|
||||
raise ValueError(f"Device {device_id} not found")
|
||||
try:
|
||||
client = await self._get_idle_client(device_id)
|
||||
frame = np.full((ds.led_count, 3), color, dtype=np.uint8)
|
||||
await client.send_pixels(frame)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send static color for {device_id}: {e}")
|
||||
|
||||
async def _restore_device_idle_state(self, device_id: str) -> None:
|
||||
"""Restore a device to its idle state when all targets stop.
|
||||
|
||||
@@ -575,23 +622,15 @@ class ProcessorManager:
|
||||
return
|
||||
|
||||
try:
|
||||
provider = get_provider(ds.device_type)
|
||||
|
||||
if ds.static_color is not None:
|
||||
await provider.set_color(
|
||||
ds.device_url, ds.static_color,
|
||||
led_count=ds.led_count, baud_rate=ds.baud_rate,
|
||||
)
|
||||
await self.send_static_color(device_id, ds.static_color)
|
||||
logger.info(
|
||||
f"Auto-restore: sent static color {ds.static_color} "
|
||||
f"to {ds.device_type} device {device_id}"
|
||||
)
|
||||
elif ds.device_type != "wled":
|
||||
# Non-WLED without static color: power off (send black frame)
|
||||
await provider.set_power(
|
||||
ds.device_url, False,
|
||||
led_count=ds.led_count, baud_rate=ds.baud_rate,
|
||||
)
|
||||
await self._send_clear_pixels(device_id)
|
||||
logger.info(f"Auto-restore: powered off {ds.device_type} device {device_id}")
|
||||
else:
|
||||
# WLED: stop() already called restore_device_state() via snapshot
|
||||
@@ -617,6 +656,10 @@ class ProcessorManager:
|
||||
for device_id in self._devices:
|
||||
await self._restore_device_idle_state(device_id)
|
||||
|
||||
# Close any cached idle LED clients
|
||||
for did in list(self._idle_clients):
|
||||
await self._close_idle_client(did)
|
||||
|
||||
# Safety net: release any remaining managed live streams
|
||||
self._live_stream_manager.release_all()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user