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:
@@ -16,13 +16,13 @@ from wled_controller.api import router
|
||||
from wled_controller.api.dependencies import init_dependencies
|
||||
from wled_controller.config import get_config
|
||||
from wled_controller.core.processing.processor_manager import ProcessorManager
|
||||
from wled_controller.core.processing.processing_settings import ProcessingSettings
|
||||
from wled_controller.storage import DeviceStore
|
||||
from wled_controller.storage.template_store import TemplateStore
|
||||
from wled_controller.storage.postprocessing_template_store import PostprocessingTemplateStore
|
||||
from wled_controller.storage.pattern_template_store import PatternTemplateStore
|
||||
from wled_controller.storage.picture_source_store import PictureSourceStore
|
||||
from wled_controller.storage.picture_target_store import PictureTargetStore
|
||||
from wled_controller.storage.color_strip_store import ColorStripStore
|
||||
from wled_controller.storage.profile_store import ProfileStore
|
||||
from wled_controller.core.profiles.profile_engine import ProfileEngine
|
||||
from wled_controller.utils import setup_logging, get_logger
|
||||
@@ -41,6 +41,7 @@ pp_template_store = PostprocessingTemplateStore(config.storage.postprocessing_te
|
||||
picture_source_store = PictureSourceStore(config.storage.picture_sources_file)
|
||||
picture_target_store = PictureTargetStore(config.storage.picture_targets_file)
|
||||
pattern_template_store = PatternTemplateStore(config.storage.pattern_templates_file)
|
||||
color_strip_store = ColorStripStore(config.storage.color_strip_sources_file)
|
||||
profile_store = ProfileStore(config.storage.profiles_file)
|
||||
|
||||
processor_manager = ProcessorManager(
|
||||
@@ -49,6 +50,7 @@ processor_manager = ProcessorManager(
|
||||
pp_template_store=pp_template_store,
|
||||
pattern_template_store=pattern_template_store,
|
||||
device_store=device_store,
|
||||
color_strip_store=color_strip_store,
|
||||
)
|
||||
|
||||
|
||||
@@ -69,22 +71,10 @@ def _migrate_devices_to_targets():
|
||||
migrated = 0
|
||||
for device_id, device_data in devices_raw.items():
|
||||
legacy_source_id = device_data.get("picture_source_id", "")
|
||||
legacy_settings = device_data.get("settings", {})
|
||||
|
||||
if not legacy_source_id and not legacy_settings:
|
||||
if not legacy_source_id:
|
||||
continue
|
||||
|
||||
# Build ProcessingSettings from legacy data
|
||||
from wled_controller.core.processing.processing_settings import DEFAULT_STATE_CHECK_INTERVAL
|
||||
settings = ProcessingSettings(
|
||||
display_index=legacy_settings.get("display_index", 0),
|
||||
fps=legacy_settings.get("fps", 30),
|
||||
brightness=legacy_settings.get("brightness", 1.0),
|
||||
smoothing=legacy_settings.get("smoothing", 0.3),
|
||||
interpolation_mode=legacy_settings.get("interpolation_mode", "average"),
|
||||
state_check_interval=legacy_settings.get("state_check_interval", DEFAULT_STATE_CHECK_INTERVAL),
|
||||
)
|
||||
|
||||
device_name = device_data.get("name", device_id)
|
||||
target_name = f"{device_name} Target"
|
||||
|
||||
@@ -93,8 +83,6 @@ def _migrate_devices_to_targets():
|
||||
name=target_name,
|
||||
target_type="wled",
|
||||
device_id=device_id,
|
||||
picture_source_id=legacy_source_id,
|
||||
settings=settings,
|
||||
description=f"Auto-migrated from device {device_name}",
|
||||
)
|
||||
migrated += 1
|
||||
@@ -106,6 +94,65 @@ def _migrate_devices_to_targets():
|
||||
logger.info(f"Migration complete: created {migrated} picture target(s) from legacy device settings")
|
||||
|
||||
|
||||
def _migrate_targets_to_color_strips():
|
||||
"""One-time migration: create ColorStripSources from legacy WledPictureTarget data.
|
||||
|
||||
For each WledPictureTarget that has a legacy _legacy_picture_source_id (from old JSON)
|
||||
but no color_strip_source_id, create a ColorStripSource and link it.
|
||||
"""
|
||||
from wled_controller.storage.wled_picture_target import WledPictureTarget
|
||||
from wled_controller.core.capture.calibration import create_default_calibration
|
||||
|
||||
migrated = 0
|
||||
for target in picture_target_store.get_all_targets():
|
||||
if not isinstance(target, WledPictureTarget):
|
||||
continue
|
||||
if target.color_strip_source_id:
|
||||
continue # already migrated
|
||||
if not target._legacy_picture_source_id:
|
||||
continue # no legacy source to migrate
|
||||
|
||||
legacy_settings = target._legacy_settings or {}
|
||||
|
||||
# Try to get calibration from device (old location)
|
||||
device = device_store.get_device(target.device_id) if target.device_id else None
|
||||
calibration = getattr(device, "_legacy_calibration", None) if device else None
|
||||
if calibration is None:
|
||||
calibration = create_default_calibration(0)
|
||||
|
||||
css_name = f"{target.name} Strip"
|
||||
# Ensure unique name
|
||||
existing_names = {s.name for s in color_strip_store.get_all_sources()}
|
||||
if css_name in existing_names:
|
||||
css_name = f"{target.name} Strip (migrated)"
|
||||
|
||||
try:
|
||||
css = color_strip_store.create_source(
|
||||
name=css_name,
|
||||
source_type="picture",
|
||||
picture_source_id=target._legacy_picture_source_id,
|
||||
fps=legacy_settings.get("fps", 30),
|
||||
brightness=legacy_settings.get("brightness", 1.0),
|
||||
smoothing=legacy_settings.get("smoothing", 0.3),
|
||||
interpolation_mode=legacy_settings.get("interpolation_mode", "average"),
|
||||
calibration=calibration,
|
||||
)
|
||||
|
||||
# Update target to reference the new CSS
|
||||
target.color_strip_source_id = css.id
|
||||
target.standby_interval = legacy_settings.get("standby_interval", 1.0)
|
||||
target.state_check_interval = legacy_settings.get("state_check_interval", 30)
|
||||
picture_target_store._save()
|
||||
|
||||
migrated += 1
|
||||
logger.info(f"Migrated target {target.id} -> CSS {css.id} ({css_name})")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to migrate target {target.id} to CSS: {e}")
|
||||
|
||||
if migrated > 0:
|
||||
logger.info(f"CSS migration complete: created {migrated} color strip source(s) from legacy targets")
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
"""Application lifespan manager.
|
||||
@@ -137,6 +184,7 @@ async def lifespan(app: FastAPI):
|
||||
|
||||
# Run migrations
|
||||
_migrate_devices_to_targets()
|
||||
_migrate_targets_to_color_strips()
|
||||
|
||||
# Create profile engine (needs processor_manager)
|
||||
profile_engine = ProfileEngine(profile_store, processor_manager)
|
||||
@@ -148,6 +196,7 @@ async def lifespan(app: FastAPI):
|
||||
pattern_template_store=pattern_template_store,
|
||||
picture_source_store=picture_source_store,
|
||||
picture_target_store=picture_target_store,
|
||||
color_strip_store=color_strip_store,
|
||||
profile_store=profile_store,
|
||||
profile_engine=profile_engine,
|
||||
)
|
||||
@@ -160,7 +209,6 @@ async def lifespan(app: FastAPI):
|
||||
device_id=device.id,
|
||||
device_url=device.url,
|
||||
led_count=device.led_count,
|
||||
calibration=device.calibration,
|
||||
device_type=device.device_type,
|
||||
baud_rate=device.baud_rate,
|
||||
software_brightness=device.software_brightness,
|
||||
|
||||
Reference in New Issue
Block a user