Add screen overlay visualization for LED target testing

Implements transparent on-screen overlay that displays LED calibration data
directly on the target display for easier setup and debugging. Overlay shows
border zones, LED position axes with tick labels, and calibration details.

Features:
- Tkinter-based transparent overlay window with click-through support
- Border zones highlighting pixel sampling areas (colored rectangles)
- LED position axes with numbered tick marks at regular intervals
- Calibration info box showing target name, LED counts, and configuration
- Toggle button (eye icon) in target cards for show/hide
- Localized UI strings (English and Russian)

Implementation:
- New screen_overlay.py module with OverlayWindow and OverlayManager classes
- Overlay runs in background thread with proper asyncio integration
- API endpoints for start/stop/status overlay control
- overlay_active state tracking in processor manager

Known limitation: tkinter threading cleanup causes server restart when overlay
is closed, but functionality works correctly while overlay is active.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-16 22:33:16 +03:00
parent ac5c1d0c82
commit 4f4d17c44d
7 changed files with 850 additions and 2 deletions

View File

@@ -789,3 +789,71 @@ async def target_colors_ws(
pass
finally:
manager.remove_kc_ws_client(target_id, websocket)
# ===== OVERLAY VISUALIZATION =====
@router.post("/api/v1/picture-targets/{target_id}/overlay/start", tags=["Visualization"])
async def start_target_overlay(
target_id: str,
_auth: AuthRequired,
manager: ProcessorManager = Depends(get_processor_manager),
target_store: PictureTargetStore = Depends(get_picture_target_store),
):
"""Start screen overlay visualization for a target.
Displays a transparent overlay on the target display showing:
- Border sampling zones (colored rectangles)
- LED position markers (numbered dots)
- Pixel-to-LED mapping ranges (colored segments)
- Calibration info text
"""
try:
# Get target name from store
target = target_store.get_target(target_id)
if not target:
raise ValueError(f"Target {target_id} not found")
await manager.start_overlay(target_id, target.name)
return {"status": "started", "target_id": target_id}
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))
except RuntimeError as e:
raise HTTPException(status_code=409, detail=str(e))
except Exception as e:
logger.error(f"Failed to start overlay: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=str(e))
@router.post("/api/v1/picture-targets/{target_id}/overlay/stop", tags=["Visualization"])
async def stop_target_overlay(
target_id: str,
_auth: AuthRequired,
manager: ProcessorManager = Depends(get_processor_manager),
):
"""Stop screen overlay visualization for a target."""
try:
await manager.stop_overlay(target_id)
return {"status": "stopped", "target_id": target_id}
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))
except Exception as e:
logger.error(f"Failed to stop overlay: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=str(e))
@router.get("/api/v1/picture-targets/{target_id}/overlay/status", tags=["Visualization"])
async def get_overlay_status(
target_id: str,
_auth: AuthRequired,
manager: ProcessorManager = Depends(get_processor_manager),
):
"""Check if overlay is active for a target."""
try:
active = manager.is_overlay_active(target_id)
return {"target_id": target_id, "active": active}
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))