Introduce ColorStripSource as first-class entity
Extracts color processing and calibration out of WledPictureTarget into a new PictureColorStripSource entity, enabling multiple LED targets to share one capture/processing pipeline. New entities & processing: - storage/color_strip_source.py: ColorStripSource + PictureColorStripSource models - storage/color_strip_store.py: JSON-backed CRUD store (prefix css_) - core/processing/color_strip_stream.py: ColorStripStream ABC + PictureColorStripStream (runs border-extract → map → smooth → brightness/sat/gamma in background thread) - core/processing/color_strip_stream_manager.py: ref-counted shared stream manager Modified storage/processing: - WledPictureTarget simplified to device_id + color_strip_source_id + standby_interval + state_check_interval - Device model: calibration field removed - WledTargetProcessor: acquires ColorStripStream from manager instead of running its own pipeline - ProcessorManager: wires ColorStripStreamManager into TargetContext API layer: - New routes: GET/POST/PUT/DELETE /api/v1/color-strip-sources, PUT calibration/test - Removed calibration endpoints from /devices - Updated /picture-targets CRUD for new target structure Frontend: - New color-strips.js module with CSS editor modal and card rendering - Calibration modal extended with CSS mode (css-id hidden field + device picker) - targets.js: Color Strip Sources section added to LED tab; target editor/card updated - app.js: imports and window globals for CSS + showCSSCalibration - en.json / ru.json: color_strip.* and targets.section.color_strips keys added Data migration runs at startup: existing WledPictureTargets are converted to reference a new PictureColorStripSource created from their old settings. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -23,12 +23,18 @@ from .devices import (
|
||||
DeviceStateResponse,
|
||||
DeviceUpdate,
|
||||
)
|
||||
from .color_strip_sources import (
|
||||
ColorStripSourceCreate,
|
||||
ColorStripSourceListResponse,
|
||||
ColorStripSourceResponse,
|
||||
ColorStripSourceUpdate,
|
||||
CSSCalibrationTestRequest,
|
||||
)
|
||||
from .picture_targets import (
|
||||
PictureTargetCreate,
|
||||
PictureTargetListResponse,
|
||||
PictureTargetResponse,
|
||||
PictureTargetUpdate,
|
||||
ProcessingSettings,
|
||||
TargetMetricsResponse,
|
||||
TargetProcessingState,
|
||||
)
|
||||
@@ -89,11 +95,15 @@ __all__ = [
|
||||
"DeviceResponse",
|
||||
"DeviceStateResponse",
|
||||
"DeviceUpdate",
|
||||
"ColorStripSourceCreate",
|
||||
"ColorStripSourceListResponse",
|
||||
"ColorStripSourceResponse",
|
||||
"ColorStripSourceUpdate",
|
||||
"CSSCalibrationTestRequest",
|
||||
"PictureTargetCreate",
|
||||
"PictureTargetListResponse",
|
||||
"PictureTargetResponse",
|
||||
"PictureTargetUpdate",
|
||||
"ProcessingSettings",
|
||||
"TargetMetricsResponse",
|
||||
"TargetProcessingState",
|
||||
"EngineInfo",
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
"""Color strip source schemas (CRUD)."""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import List, Literal, Optional
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from wled_controller.api.schemas.devices import Calibration
|
||||
|
||||
|
||||
class ColorStripSourceCreate(BaseModel):
|
||||
"""Request to create a color strip source."""
|
||||
|
||||
name: str = Field(description="Source name", min_length=1, max_length=100)
|
||||
source_type: Literal["picture"] = Field(default="picture", description="Source type")
|
||||
picture_source_id: str = Field(default="", description="Picture source ID (for picture type)")
|
||||
fps: int = Field(default=30, description="Target frames per second", ge=10, le=90)
|
||||
brightness: float = Field(default=1.0, description="Brightness multiplier (0.0-2.0)", ge=0.0, le=2.0)
|
||||
saturation: float = Field(default=1.0, description="Saturation (0.0=grayscale, 1.0=unchanged, 2.0=double)", ge=0.0, le=2.0)
|
||||
gamma: float = Field(default=1.0, description="Gamma correction (1.0=none, <1=brighter, >1=darker mids)", ge=0.1, le=3.0)
|
||||
smoothing: float = Field(default=0.3, description="Temporal smoothing (0.0=none, 1.0=full)", ge=0.0, le=1.0)
|
||||
interpolation_mode: str = Field(default="average", description="LED color interpolation mode (average, median, dominant)")
|
||||
calibration: Optional[Calibration] = Field(None, description="LED calibration (position and count per edge)")
|
||||
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
||||
|
||||
|
||||
class ColorStripSourceUpdate(BaseModel):
|
||||
"""Request to update a color strip source."""
|
||||
|
||||
name: Optional[str] = Field(None, description="Source name", min_length=1, max_length=100)
|
||||
picture_source_id: Optional[str] = Field(None, description="Picture source ID")
|
||||
fps: Optional[int] = Field(None, description="Target FPS", ge=10, le=90)
|
||||
brightness: Optional[float] = Field(None, description="Brightness multiplier (0.0-2.0)", ge=0.0, le=2.0)
|
||||
saturation: Optional[float] = Field(None, description="Saturation (0.0-2.0)", ge=0.0, le=2.0)
|
||||
gamma: Optional[float] = Field(None, description="Gamma correction (0.1-3.0)", ge=0.1, le=3.0)
|
||||
smoothing: Optional[float] = Field(None, description="Temporal smoothing (0.0-1.0)", ge=0.0, le=1.0)
|
||||
interpolation_mode: Optional[str] = Field(None, description="Interpolation mode (average, median, dominant)")
|
||||
calibration: Optional[Calibration] = Field(None, description="LED calibration")
|
||||
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
||||
|
||||
|
||||
class ColorStripSourceResponse(BaseModel):
|
||||
"""Color strip source response."""
|
||||
|
||||
id: str = Field(description="Source ID")
|
||||
name: str = Field(description="Source name")
|
||||
source_type: str = Field(description="Source type")
|
||||
picture_source_id: Optional[str] = Field(None, description="Picture source ID")
|
||||
fps: Optional[int] = Field(None, description="Target FPS")
|
||||
brightness: Optional[float] = Field(None, description="Brightness multiplier")
|
||||
saturation: Optional[float] = Field(None, description="Saturation")
|
||||
gamma: Optional[float] = Field(None, description="Gamma correction")
|
||||
smoothing: Optional[float] = Field(None, description="Temporal smoothing")
|
||||
interpolation_mode: Optional[str] = Field(None, description="Interpolation mode")
|
||||
calibration: Optional[Calibration] = Field(None, description="LED calibration")
|
||||
description: Optional[str] = Field(None, description="Description")
|
||||
created_at: datetime = Field(description="Creation timestamp")
|
||||
updated_at: datetime = Field(description="Last update timestamp")
|
||||
|
||||
|
||||
class ColorStripSourceListResponse(BaseModel):
|
||||
"""List of color strip sources."""
|
||||
|
||||
sources: List[ColorStripSourceResponse] = Field(description="List of color strip sources")
|
||||
count: int = Field(description="Number of sources")
|
||||
|
||||
|
||||
class CSSCalibrationTestRequest(BaseModel):
|
||||
"""Request to run a calibration test for a color strip source on a specific device."""
|
||||
|
||||
device_id: str = Field(description="Device ID to send test pixels to")
|
||||
edges: dict = Field(
|
||||
default_factory=lambda: {
|
||||
"top": [255, 0, 0],
|
||||
"right": [0, 255, 0],
|
||||
"bottom": [0, 100, 255],
|
||||
"left": [255, 255, 0],
|
||||
},
|
||||
description="Map of edge names to RGB colors. Empty dict = exit test mode.",
|
||||
)
|
||||
@@ -104,7 +104,6 @@ class DeviceResponse(BaseModel):
|
||||
auto_shutdown: bool = Field(default=False, description="Restore device to idle state when targets stop")
|
||||
static_color: Optional[List[int]] = Field(None, description="Static idle color [R, G, B]")
|
||||
capabilities: List[str] = Field(default_factory=list, description="Device type capabilities")
|
||||
calibration: Optional[Calibration] = Field(None, description="Calibration configuration")
|
||||
created_at: datetime = Field(description="Creation timestamp")
|
||||
updated_at: datetime = Field(description="Last update timestamp")
|
||||
|
||||
|
||||
@@ -1,26 +1,11 @@
|
||||
"""Picture target schemas (CRUD, processing state, settings, metrics)."""
|
||||
"""Picture target schemas (CRUD, processing state, metrics)."""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Literal, Optional
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from wled_controller.core.processing.processing_settings import DEFAULT_STATE_CHECK_INTERVAL
|
||||
|
||||
|
||||
class ProcessingSettings(BaseModel):
|
||||
"""Processing settings for a picture target."""
|
||||
|
||||
display_index: int = Field(default=0, description="Display to capture", ge=0)
|
||||
fps: int = Field(default=30, description="Target frames per second", ge=10, le=90)
|
||||
interpolation_mode: str = Field(default="average", description="LED color interpolation mode (average, median, dominant)")
|
||||
brightness: float = Field(default=1.0, description="Global brightness (0.0-1.0)", ge=0.0, le=1.0)
|
||||
smoothing: float = Field(default=0.3, description="Temporal smoothing factor (0.0=none, 1.0=full)", ge=0.0, le=1.0)
|
||||
standby_interval: float = Field(default=1.0, description="Seconds between keepalive sends when screen is static (0.5-5.0)", ge=0.5, le=5.0)
|
||||
state_check_interval: int = Field(
|
||||
default=DEFAULT_STATE_CHECK_INTERVAL, ge=5, le=600,
|
||||
description="Seconds between WLED health checks"
|
||||
)
|
||||
DEFAULT_STATE_CHECK_INTERVAL = 30 # seconds between health checks
|
||||
|
||||
|
||||
class KeyColorRectangleSchema(BaseModel):
|
||||
@@ -65,9 +50,13 @@ class PictureTargetCreate(BaseModel):
|
||||
|
||||
name: str = Field(description="Target name", min_length=1, max_length=100)
|
||||
target_type: str = Field(default="led", description="Target type (led, key_colors)")
|
||||
# LED target fields
|
||||
device_id: str = Field(default="", description="LED device ID")
|
||||
picture_source_id: str = Field(default="", description="Picture source ID")
|
||||
settings: Optional[ProcessingSettings] = Field(None, description="Processing settings (for wled targets)")
|
||||
color_strip_source_id: str = Field(default="", description="Color strip source ID")
|
||||
standby_interval: float = Field(default=1.0, description="Keepalive send interval when screen is static (0.5-5.0s)", ge=0.5, le=5.0)
|
||||
state_check_interval: int = Field(default=DEFAULT_STATE_CHECK_INTERVAL, description="Device health check interval (5-600s)", ge=5, le=600)
|
||||
# KC target fields
|
||||
picture_source_id: str = Field(default="", description="Picture source ID (for key_colors targets)")
|
||||
key_colors_settings: Optional[KeyColorsSettingsSchema] = Field(None, description="Key colors settings (for key_colors targets)")
|
||||
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
||||
|
||||
@@ -76,9 +65,13 @@ class PictureTargetUpdate(BaseModel):
|
||||
"""Request to update a picture target."""
|
||||
|
||||
name: Optional[str] = Field(None, description="Target name", min_length=1, max_length=100)
|
||||
device_id: Optional[str] = Field(None, description="WLED device ID")
|
||||
picture_source_id: Optional[str] = Field(None, description="Picture source ID")
|
||||
settings: Optional[ProcessingSettings] = Field(None, description="Processing settings (for wled targets)")
|
||||
# LED target fields
|
||||
device_id: Optional[str] = Field(None, description="LED device ID")
|
||||
color_strip_source_id: Optional[str] = Field(None, description="Color strip source ID")
|
||||
standby_interval: Optional[float] = Field(None, description="Keepalive interval (0.5-5.0s)", ge=0.5, le=5.0)
|
||||
state_check_interval: Optional[int] = Field(None, description="Health check interval (5-600s)", ge=5, le=600)
|
||||
# KC target fields
|
||||
picture_source_id: Optional[str] = Field(None, description="Picture source ID (for key_colors targets)")
|
||||
key_colors_settings: Optional[KeyColorsSettingsSchema] = Field(None, description="Key colors settings (for key_colors targets)")
|
||||
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
||||
|
||||
@@ -89,10 +82,14 @@ class PictureTargetResponse(BaseModel):
|
||||
id: str = Field(description="Target ID")
|
||||
name: str = Field(description="Target name")
|
||||
target_type: str = Field(description="Target type")
|
||||
device_id: str = Field(default="", description="WLED device ID")
|
||||
picture_source_id: str = Field(default="", description="Picture source ID")
|
||||
settings: Optional[ProcessingSettings] = Field(None, description="Processing settings (wled)")
|
||||
key_colors_settings: Optional[KeyColorsSettingsSchema] = Field(None, description="Key colors settings (key_colors)")
|
||||
# LED target fields
|
||||
device_id: str = Field(default="", description="LED device ID")
|
||||
color_strip_source_id: str = Field(default="", description="Color strip source ID")
|
||||
standby_interval: float = Field(default=1.0, description="Keepalive interval (s)")
|
||||
state_check_interval: int = Field(default=DEFAULT_STATE_CHECK_INTERVAL, description="Health check interval (s)")
|
||||
# KC target fields
|
||||
picture_source_id: str = Field(default="", description="Picture source ID (key_colors)")
|
||||
key_colors_settings: Optional[KeyColorsSettingsSchema] = Field(None, description="Key colors settings")
|
||||
description: Optional[str] = Field(None, description="Description")
|
||||
created_at: datetime = Field(description="Creation timestamp")
|
||||
updated_at: datetime = Field(description="Last update timestamp")
|
||||
@@ -110,21 +107,18 @@ class TargetProcessingState(BaseModel):
|
||||
|
||||
target_id: str = Field(description="Target ID")
|
||||
device_id: Optional[str] = Field(None, description="Device ID")
|
||||
color_strip_source_id: Optional[str] = Field(None, description="Color strip source ID")
|
||||
processing: bool = Field(description="Whether processing is active")
|
||||
fps_actual: Optional[float] = Field(None, description="Actual FPS achieved")
|
||||
fps_potential: Optional[float] = Field(None, description="Potential FPS (processing speed without throttle)")
|
||||
fps_target: int = Field(default=0, description="Target FPS")
|
||||
fps_target: Optional[int] = Field(None, description="Target FPS")
|
||||
frames_skipped: Optional[int] = Field(None, description="Frames skipped (no screen change)")
|
||||
frames_keepalive: Optional[int] = Field(None, description="Keepalive frames sent during standby")
|
||||
fps_current: Optional[int] = Field(None, description="Frames sent in the last second")
|
||||
timing_extract_ms: Optional[float] = Field(None, description="Border extraction time (ms)")
|
||||
timing_map_leds_ms: Optional[float] = Field(None, description="LED mapping time (ms)")
|
||||
timing_smooth_ms: Optional[float] = Field(None, description="Smoothing time (ms)")
|
||||
timing_send_ms: Optional[float] = Field(None, description="DDP send time (ms)")
|
||||
timing_total_ms: Optional[float] = Field(None, description="Total processing time (ms)")
|
||||
timing_calc_colors_ms: Optional[float] = Field(None, description="Color calculation time (ms, KC targets)")
|
||||
timing_broadcast_ms: Optional[float] = Field(None, description="WebSocket broadcast time (ms, KC targets)")
|
||||
display_index: int = Field(default=0, description="Current display index")
|
||||
display_index: Optional[int] = Field(None, description="Current display index")
|
||||
overlay_active: bool = Field(default=False, description="Whether visualization overlay is active")
|
||||
last_update: Optional[datetime] = Field(None, description="Last successful update")
|
||||
errors: List[str] = Field(default_factory=list, description="Recent errors")
|
||||
@@ -147,7 +141,7 @@ class TargetMetricsResponse(BaseModel):
|
||||
device_id: Optional[str] = Field(None, description="Device ID")
|
||||
processing: bool = Field(description="Whether processing is active")
|
||||
fps_actual: Optional[float] = Field(None, description="Actual FPS")
|
||||
fps_target: int = Field(description="Target FPS")
|
||||
fps_target: Optional[int] = Field(None, description="Target FPS")
|
||||
uptime_seconds: float = Field(description="Processing uptime in seconds")
|
||||
frames_processed: int = Field(description="Total frames processed")
|
||||
errors_count: int = Field(description="Total error count")
|
||||
|
||||
Reference in New Issue
Block a user