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:
@@ -291,7 +291,7 @@ async def delete_device(
|
|||||||
|
|
||||||
# Remove from manager
|
# Remove from manager
|
||||||
try:
|
try:
|
||||||
manager.remove_device(device_id)
|
await manager.remove_device(device_id)
|
||||||
except (ValueError, RuntimeError):
|
except (ValueError, RuntimeError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -497,11 +497,7 @@ async def set_device_color(
|
|||||||
# If device is idle, apply the color immediately
|
# If device is idle, apply the color immediately
|
||||||
if color is not None and not manager.is_device_processing(device_id):
|
if color is not None and not manager.is_device_processing(device_id):
|
||||||
try:
|
try:
|
||||||
provider = get_provider(device.device_type)
|
await manager.send_static_color(device_id, color)
|
||||||
await provider.set_color(
|
|
||||||
device.url, color,
|
|
||||||
led_count=device.led_count, baud_rate=device.baud_rate,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Failed to apply static color immediately: {e}")
|
logger.warning(f"Failed to apply static color immediately: {e}")
|
||||||
|
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ class ProcessorManager:
|
|||||||
"""Initialize processor manager."""
|
"""Initialize processor manager."""
|
||||||
self._devices: Dict[str, DeviceState] = {}
|
self._devices: Dict[str, DeviceState] = {}
|
||||||
self._processors: Dict[str, TargetProcessor] = {}
|
self._processors: Dict[str, TargetProcessor] = {}
|
||||||
|
self._idle_clients: Dict[str, object] = {} # device_id -> cached LEDClient
|
||||||
self._health_monitoring_active = False
|
self._health_monitoring_active = False
|
||||||
self._http_client: Optional[httpx.AsyncClient] = None
|
self._http_client: Optional[httpx.AsyncClient] = None
|
||||||
self._picture_source_store = picture_source_store
|
self._picture_source_store = picture_source_store
|
||||||
@@ -181,7 +182,7 @@ class ProcessorManager:
|
|||||||
|
|
||||||
logger.info(f"Registered device {device_id} with {led_count} LEDs")
|
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."""
|
"""Unregister a device."""
|
||||||
if device_id not in self._devices:
|
if device_id not in self._devices:
|
||||||
raise ValueError(f"Device {device_id} not found")
|
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"
|
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)
|
self._stop_device_health_check(device_id)
|
||||||
del self._devices[device_id]
|
del self._devices[device_id]
|
||||||
logger.info(f"Unregistered device {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}"
|
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()
|
await proc.start()
|
||||||
|
|
||||||
async def stop_processing(self, target_id: str):
|
async def stop_processing(self, target_id: str):
|
||||||
@@ -492,6 +498,42 @@ class ProcessorManager:
|
|||||||
ds.test_mode_active = False
|
ds.test_mode_active = False
|
||||||
ds.test_mode_edges = {}
|
ds.test_mode_edges = {}
|
||||||
await self._send_clear_pixels(device_id)
|
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:
|
async def _send_test_pixels(self, device_id: str) -> None:
|
||||||
"""Build and send test pixel array for active test edges."""
|
"""Build and send test pixel array for active test edges."""
|
||||||
@@ -513,11 +555,7 @@ class ProcessorManager:
|
|||||||
pixels = pixels[-offset:] + pixels[:-offset]
|
pixels = pixels[-offset:] + pixels[:-offset]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
active_client = self._find_active_led_client(device_id)
|
client = await self._get_idle_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)
|
await client.send_pixels(pixels)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to send test pixels for {device_id}: {e}")
|
logger.error(f"Failed to send test pixels for {device_id}: {e}")
|
||||||
@@ -528,11 +566,7 @@ class ProcessorManager:
|
|||||||
pixels = [(0, 0, 0)] * ds.led_count
|
pixels = [(0, 0, 0)] * ds.led_count
|
||||||
|
|
||||||
try:
|
try:
|
||||||
active_client = self._find_active_led_client(device_id)
|
client = await self._get_idle_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)
|
await client.send_pixels(pixels)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to clear pixels for {device_id}: {e}")
|
logger.error(f"Failed to clear pixels for {device_id}: {e}")
|
||||||
@@ -560,6 +594,19 @@ class ProcessorManager:
|
|||||||
return proc.device_id
|
return proc.device_id
|
||||||
return None
|
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:
|
async def _restore_device_idle_state(self, device_id: str) -> None:
|
||||||
"""Restore a device to its idle state when all targets stop.
|
"""Restore a device to its idle state when all targets stop.
|
||||||
|
|
||||||
@@ -575,23 +622,15 @@ class ProcessorManager:
|
|||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
provider = get_provider(ds.device_type)
|
|
||||||
|
|
||||||
if ds.static_color is not None:
|
if ds.static_color is not None:
|
||||||
await provider.set_color(
|
await self.send_static_color(device_id, ds.static_color)
|
||||||
ds.device_url, ds.static_color,
|
|
||||||
led_count=ds.led_count, baud_rate=ds.baud_rate,
|
|
||||||
)
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Auto-restore: sent static color {ds.static_color} "
|
f"Auto-restore: sent static color {ds.static_color} "
|
||||||
f"to {ds.device_type} device {device_id}"
|
f"to {ds.device_type} device {device_id}"
|
||||||
)
|
)
|
||||||
elif ds.device_type != "wled":
|
elif ds.device_type != "wled":
|
||||||
# Non-WLED without static color: power off (send black frame)
|
# Non-WLED without static color: power off (send black frame)
|
||||||
await provider.set_power(
|
await self._send_clear_pixels(device_id)
|
||||||
ds.device_url, False,
|
|
||||||
led_count=ds.led_count, baud_rate=ds.baud_rate,
|
|
||||||
)
|
|
||||||
logger.info(f"Auto-restore: powered off {ds.device_type} device {device_id}")
|
logger.info(f"Auto-restore: powered off {ds.device_type} device {device_id}")
|
||||||
else:
|
else:
|
||||||
# WLED: stop() already called restore_device_state() via snapshot
|
# WLED: stop() already called restore_device_state() via snapshot
|
||||||
@@ -617,6 +656,10 @@ class ProcessorManager:
|
|||||||
for device_id in self._devices:
|
for device_id in self._devices:
|
||||||
await self._restore_device_idle_state(device_id)
|
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
|
# Safety net: release any remaining managed live streams
|
||||||
self._live_stream_manager.release_all()
|
self._live_stream_manager.release_all()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user