Overlay: fix 404, crash on repeat, missing edge test colors, device reset on stop

- Target overlay works without active processing: route pre-loads calibration
  and display info from the CSS store, passes to processor as fallback
- Fix server crash on repeated overlay: replace per-window tk.Tk() with single
  persistent hidden root; each overlay is a Toplevel child dispatched via
  root.after() — eliminates Tcl interpreter crashes on Windows
- Fix edge test colors not lighting up: always call set_test_mode regardless
  of processing state (was guarded by 'not proc.is_running'); pass calibration
  so _send_test_pixels knows which LEDs map to which edges
- Fix device reset on overlay stop: keep idle serial client cached after
  clearing test mode; start_processing() already closes it before connecting

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-20 17:16:10 +03:00
parent a3aeafef13
commit 018bedf9f6
7 changed files with 349 additions and 476 deletions

View File

@@ -435,16 +435,18 @@ class ProcessorManager:
"left": [255, 255, 0],
}
async def start_overlay(self, target_id: str, target_name: str = None) -> None:
async def start_overlay(self, target_id: str, target_name: str = None, calibration=None, display_info=None) -> None:
proc = self._get_processor(target_id)
if not proc.supports_overlay():
raise ValueError(f"Target {target_id} does not support overlays")
await proc.start_overlay(target_name)
await proc.start_overlay(target_name, calibration=calibration, display_info=display_info)
# Light up device LEDs with edge test colors while overlay is visible
if proc.device_id is not None and not proc.is_running:
# Light up device LEDs with edge test colors while overlay is visible.
# Always do this regardless of whether processing is running — the processing
# loop pauses itself when test_mode_active is set.
if proc.device_id is not None:
try:
await self.set_test_mode(proc.device_id, self._OVERLAY_EDGE_COLORS)
await self.set_test_mode(proc.device_id, self._OVERLAY_EDGE_COLORS, calibration)
except Exception as e:
logger.warning(f"Failed to set edge test for overlay on {proc.device_id}: {e}")
@@ -452,8 +454,8 @@ class ProcessorManager:
proc = self._get_processor(target_id)
await proc.stop_overlay()
# Clear device LEDs when overlay is dismissed
if proc.device_id is not None and not proc.is_running:
# Clear device LEDs when overlay is dismissed.
if proc.device_id is not None:
try:
await self.set_test_mode(proc.device_id, {})
except Exception as e:
@@ -462,6 +464,23 @@ class ProcessorManager:
def is_overlay_active(self, target_id: str) -> bool:
return self._get_processor(target_id).is_overlay_active()
# ===== CSS OVERLAY (direct, no target processor required) =====
async def start_css_overlay(self, css_id: str, display_info, calibration, css_name: str = None) -> None:
await asyncio.to_thread(
self._overlay_manager.start_overlay,
css_id, display_info, calibration, css_name,
)
async def stop_css_overlay(self, css_id: str) -> None:
await asyncio.to_thread(
self._overlay_manager.stop_overlay,
css_id,
)
def is_css_overlay_active(self, css_id: str) -> bool:
return self._overlay_manager.is_running(css_id)
# ===== WEBSOCKET (delegates to processor) =====
def add_kc_ws_client(self, target_id: str, ws) -> None:
@@ -508,7 +527,8 @@ class ProcessorManager:
ds.test_mode_edges = {}
ds.test_calibration = None
await self._send_clear_pixels(device_id)
await self._close_idle_client(device_id)
# Keep idle client open — serial reconnect causes device reset.
# start_processing() closes it before connecting its own client.
async def _get_idle_client(self, device_id: str):
"""Get or create a cached idle LED client for a device.

View File

@@ -268,30 +268,34 @@ class WledTargetProcessor(TargetProcessor):
def supports_overlay(self) -> bool:
return True
async def start_overlay(self, target_name: Optional[str] = None) -> None:
async def start_overlay(self, target_name: Optional[str] = None, calibration=None, display_info=None) -> None:
if self._overlay_active:
raise RuntimeError(f"Overlay already active for {self._target_id}")
# Calibration comes from the active color strip stream
if self._color_strip_stream is None:
raise ValueError(
f"Cannot start overlay for {self._target_id}: no color strip stream active. "
f"Start processing first."
)
if calibration is None or display_info is None:
# Calibration comes from the active color strip stream
if self._color_strip_stream is None:
raise ValueError(
f"Cannot start overlay for {self._target_id}: no color strip stream active "
f"and no calibration provided."
)
calibration = self._color_strip_stream.calibration
display_index = self._resolved_display_index
if display_index is None:
display_index = self._color_strip_stream.display_index
if calibration is None:
calibration = self._color_strip_stream.calibration
if display_index is None or display_index < 0:
raise ValueError(f"Invalid display index {display_index} for overlay")
if display_info is None:
display_index = self._resolved_display_index
if display_index is None:
display_index = self._color_strip_stream.display_index
displays = get_available_displays()
if display_index >= len(displays):
raise ValueError(f"Invalid display index {display_index}")
if display_index is None or display_index < 0:
raise ValueError(f"Invalid display index {display_index} for overlay")
display_info = displays[display_index]
displays = get_available_displays()
if display_index >= len(displays):
raise ValueError(f"Invalid display index {display_index}")
display_info = displays[display_index]
await asyncio.to_thread(
self._ctx.overlay_manager.start_overlay,