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:
@@ -5,6 +5,7 @@ from fastapi import APIRouter, Depends, HTTPException
|
||||
from wled_controller.api.auth import AuthRequired
|
||||
from wled_controller.api.dependencies import (
|
||||
get_color_strip_store,
|
||||
get_picture_source_store,
|
||||
get_picture_target_store,
|
||||
get_processor_manager,
|
||||
)
|
||||
@@ -23,9 +24,12 @@ from wled_controller.core.capture.calibration import (
|
||||
calibration_from_dict,
|
||||
calibration_to_dict,
|
||||
)
|
||||
from wled_controller.core.capture.screen_capture import get_available_displays
|
||||
from wled_controller.core.processing.processor_manager import ProcessorManager
|
||||
from wled_controller.storage.color_strip_source import PictureColorStripSource
|
||||
from wled_controller.storage.color_strip_store import ColorStripStore
|
||||
from wled_controller.storage.picture_source import ProcessedPictureSource, ScreenCapturePictureSource
|
||||
from wled_controller.storage.picture_source_store import PictureSourceStore
|
||||
from wled_controller.storage.picture_target_store import PictureTargetStore
|
||||
from wled_controller.utils import get_logger
|
||||
|
||||
@@ -34,7 +38,7 @@ logger = get_logger(__name__)
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def _css_to_response(source) -> ColorStripSourceResponse:
|
||||
def _css_to_response(source, overlay_active: bool = False) -> ColorStripSourceResponse:
|
||||
"""Convert a ColorStripSource to a ColorStripSourceResponse."""
|
||||
calibration = None
|
||||
if isinstance(source, PictureColorStripSource) and source.calibration:
|
||||
@@ -54,21 +58,38 @@ def _css_to_response(source) -> ColorStripSourceResponse:
|
||||
led_count=getattr(source, "led_count", 0),
|
||||
calibration=calibration,
|
||||
description=source.description,
|
||||
overlay_active=overlay_active,
|
||||
created_at=source.created_at,
|
||||
updated_at=source.updated_at,
|
||||
)
|
||||
|
||||
|
||||
def _resolve_display_index(picture_source_id: str, picture_source_store: PictureSourceStore, depth: int = 0) -> int:
|
||||
"""Resolve display index from a picture source, following processed source chains."""
|
||||
if not picture_source_id or depth > 5:
|
||||
return 0
|
||||
try:
|
||||
ps = picture_source_store.get_source(picture_source_id)
|
||||
except Exception:
|
||||
return 0
|
||||
if isinstance(ps, ScreenCapturePictureSource):
|
||||
return ps.display_index
|
||||
if isinstance(ps, ProcessedPictureSource):
|
||||
return _resolve_display_index(ps.source_stream_id, picture_source_store, depth + 1)
|
||||
return 0
|
||||
|
||||
|
||||
# ===== CRUD ENDPOINTS =====
|
||||
|
||||
@router.get("/api/v1/color-strip-sources", response_model=ColorStripSourceListResponse, tags=["Color Strip Sources"])
|
||||
async def list_color_strip_sources(
|
||||
_auth: AuthRequired,
|
||||
store: ColorStripStore = Depends(get_color_strip_store),
|
||||
manager: ProcessorManager = Depends(get_processor_manager),
|
||||
):
|
||||
"""List all color strip sources."""
|
||||
sources = store.get_all_sources()
|
||||
responses = [_css_to_response(s) for s in sources]
|
||||
responses = [_css_to_response(s, manager.is_css_overlay_active(s.id)) for s in sources]
|
||||
return ColorStripSourceListResponse(sources=responses, count=len(responses))
|
||||
|
||||
|
||||
@@ -112,11 +133,12 @@ async def get_color_strip_source(
|
||||
source_id: str,
|
||||
_auth: AuthRequired,
|
||||
store: ColorStripStore = Depends(get_color_strip_store),
|
||||
manager: ProcessorManager = Depends(get_processor_manager),
|
||||
):
|
||||
"""Get a color strip source by ID."""
|
||||
try:
|
||||
source = store.get_source(source_id)
|
||||
return _css_to_response(source)
|
||||
return _css_to_response(source, manager.is_css_overlay_active(source_id))
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
|
||||
@@ -259,3 +281,67 @@ async def test_css_calibration(
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to set CSS calibration test mode: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
# ===== OVERLAY VISUALIZATION =====
|
||||
|
||||
@router.post("/api/v1/color-strip-sources/{source_id}/overlay/start", tags=["Color Strip Sources"])
|
||||
async def start_css_overlay(
|
||||
source_id: str,
|
||||
_auth: AuthRequired,
|
||||
store: ColorStripStore = Depends(get_color_strip_store),
|
||||
picture_source_store: PictureSourceStore = Depends(get_picture_source_store),
|
||||
manager: ProcessorManager = Depends(get_processor_manager),
|
||||
):
|
||||
"""Start screen overlay visualization for a color strip source."""
|
||||
try:
|
||||
source = store.get_source(source_id)
|
||||
if not isinstance(source, PictureColorStripSource):
|
||||
raise HTTPException(status_code=400, detail="Overlay only supported for picture color strip sources")
|
||||
if not source.calibration:
|
||||
raise HTTPException(status_code=400, detail="Color strip source has no calibration configured")
|
||||
|
||||
display_index = _resolve_display_index(source.picture_source_id, picture_source_store)
|
||||
displays = get_available_displays()
|
||||
if not displays:
|
||||
raise HTTPException(status_code=409, detail="No displays available")
|
||||
display_index = min(display_index, len(displays) - 1)
|
||||
display_info = displays[display_index]
|
||||
|
||||
await manager.start_css_overlay(source_id, display_info, source.calibration, source.name)
|
||||
return {"status": "started", "source_id": source_id}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except RuntimeError as e:
|
||||
raise HTTPException(status_code=409, detail=str(e))
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to start CSS overlay: {e}", exc_info=True)
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/api/v1/color-strip-sources/{source_id}/overlay/stop", tags=["Color Strip Sources"])
|
||||
async def stop_css_overlay(
|
||||
source_id: str,
|
||||
_auth: AuthRequired,
|
||||
manager: ProcessorManager = Depends(get_processor_manager),
|
||||
):
|
||||
"""Stop screen overlay visualization for a color strip source."""
|
||||
try:
|
||||
await manager.stop_css_overlay(source_id)
|
||||
return {"status": "stopped", "source_id": source_id}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to stop CSS overlay: {e}", exc_info=True)
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/api/v1/color-strip-sources/{source_id}/overlay/status", tags=["Color Strip Sources"])
|
||||
async def get_css_overlay_status(
|
||||
source_id: str,
|
||||
_auth: AuthRequired,
|
||||
manager: ProcessorManager = Depends(get_processor_manager),
|
||||
):
|
||||
"""Check if overlay is active for a color strip source."""
|
||||
return {"source_id": source_id, "active": manager.is_css_overlay_active(source_id)}
|
||||
|
||||
@@ -11,6 +11,7 @@ from PIL import Image
|
||||
|
||||
from wled_controller.api.auth import AuthRequired
|
||||
from wled_controller.api.dependencies import (
|
||||
get_color_strip_store,
|
||||
get_device_store,
|
||||
get_pattern_template_store,
|
||||
get_picture_source_store,
|
||||
@@ -40,7 +41,10 @@ from wled_controller.core.capture.screen_capture import (
|
||||
calculate_average_color,
|
||||
calculate_dominant_color,
|
||||
calculate_median_color,
|
||||
get_available_displays,
|
||||
)
|
||||
from wled_controller.storage.color_strip_store import ColorStripStore
|
||||
from wled_controller.storage.color_strip_source import PictureColorStripSource
|
||||
from wled_controller.storage import DeviceStore
|
||||
from wled_controller.storage.pattern_template_store import PatternTemplateStore
|
||||
from wled_controller.storage.picture_source import ScreenCapturePictureSource, StaticImagePictureSource
|
||||
@@ -679,6 +683,8 @@ async def start_target_overlay(
|
||||
_auth: AuthRequired,
|
||||
manager: ProcessorManager = Depends(get_processor_manager),
|
||||
target_store: PictureTargetStore = Depends(get_picture_target_store),
|
||||
color_strip_store: ColorStripStore = Depends(get_color_strip_store),
|
||||
picture_source_store: PictureSourceStore = Depends(get_picture_source_store),
|
||||
):
|
||||
"""Start screen overlay visualization for a target.
|
||||
|
||||
@@ -694,7 +700,26 @@ async def start_target_overlay(
|
||||
if not target:
|
||||
raise ValueError(f"Target {target_id} not found")
|
||||
|
||||
await manager.start_overlay(target_id, target.name)
|
||||
# Pre-load calibration and display info from the CSS store so the overlay
|
||||
# can start even when processing is not currently running.
|
||||
calibration = None
|
||||
display_info = None
|
||||
if isinstance(target, WledPictureTarget) and target.color_strip_source_id:
|
||||
try:
|
||||
css = color_strip_store.get_source(target.color_strip_source_id)
|
||||
if isinstance(css, PictureColorStripSource) and css.calibration:
|
||||
calibration = css.calibration
|
||||
# Resolve the display this CSS is capturing
|
||||
from wled_controller.api.routes.color_strip_sources import _resolve_display_index
|
||||
display_index = _resolve_display_index(css.picture_source_id, picture_source_store)
|
||||
displays = get_available_displays()
|
||||
if displays:
|
||||
display_index = min(display_index, len(displays) - 1)
|
||||
display_info = displays[display_index]
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not pre-load CSS calibration for overlay on {target_id}: {e}")
|
||||
|
||||
await manager.start_overlay(target_id, target.name, calibration=calibration, display_info=display_info)
|
||||
return {"status": "started", "target_id": target_id}
|
||||
|
||||
except ValueError as e:
|
||||
|
||||
@@ -57,6 +57,7 @@ class ColorStripSourceResponse(BaseModel):
|
||||
led_count: int = Field(0, description="Total LED count (0 = auto from calibration)")
|
||||
calibration: Optional[Calibration] = Field(None, description="LED calibration")
|
||||
description: Optional[str] = Field(None, description="Description")
|
||||
overlay_active: bool = Field(False, description="Whether the screen overlay is currently active")
|
||||
created_at: datetime = Field(description="Creation timestamp")
|
||||
updated_at: datetime = Field(description="Last update timestamp")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user