Add sync clock entity for synchronized animation timing

Introduces Synchronization Clocks — shared, controllable time bases
that CSS sources can optionally reference for synchronized animation.

Backend:
- New SyncClock dataclass, JSON store, Pydantic schemas, REST API
- Runtime clock with thread-safe pause/resume/reset and speed control
- Ref-counted runtime pool with eager creation for API control
- clock_id field on all ColorStripSource types
- Stream integration: clock time/speed replaces source-local values
- Paused clock skips rendering (saves CPU + stops frame pushes)
- Included in backup/restore via STORE_MAP

Frontend:
- Sync Clocks tab in Streams section with cards and controls
- Clock dropdown in CSS editor (hidden speed slider when clock set)
- Clock crosslink badge on CSS source cards (replaces speed badge)
- Targets tab uses DataCache for picture/audio sources and sync clocks

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-01 21:46:55 +03:00
parent 52ee4bdeb6
commit aa1e4a6afc
32 changed files with 1255 additions and 58 deletions

View File

@@ -13,8 +13,10 @@ from wled_controller.storage.audio_template_store import AudioTemplateStore
from wled_controller.storage.value_source_store import ValueSourceStore
from wled_controller.storage.automation_store import AutomationStore
from wled_controller.storage.scene_preset_store import ScenePresetStore
from wled_controller.storage.sync_clock_store import SyncClockStore
from wled_controller.core.automations.automation_engine import AutomationEngine
from wled_controller.core.backup.auto_backup import AutoBackupEngine
from wled_controller.core.processing.sync_clock_manager import SyncClockManager
# Global instances (initialized in main.py)
_auto_backup_engine: AutoBackupEngine | None = None
@@ -32,6 +34,8 @@ _processor_manager: ProcessorManager | None = None
_automation_store: AutomationStore | None = None
_scene_preset_store: ScenePresetStore | None = None
_automation_engine: AutomationEngine | None = None
_sync_clock_store: SyncClockStore | None = None
_sync_clock_manager: SyncClockManager | None = None
def get_device_store() -> DeviceStore:
@@ -139,6 +143,20 @@ def get_auto_backup_engine() -> AutoBackupEngine:
return _auto_backup_engine
def get_sync_clock_store() -> SyncClockStore:
"""Get sync clock store dependency."""
if _sync_clock_store is None:
raise RuntimeError("Sync clock store not initialized")
return _sync_clock_store
def get_sync_clock_manager() -> SyncClockManager:
"""Get sync clock manager dependency."""
if _sync_clock_manager is None:
raise RuntimeError("Sync clock manager not initialized")
return _sync_clock_manager
def init_dependencies(
device_store: DeviceStore,
template_store: TemplateStore,
@@ -155,12 +173,15 @@ def init_dependencies(
scene_preset_store: ScenePresetStore | None = None,
automation_engine: AutomationEngine | None = None,
auto_backup_engine: AutoBackupEngine | None = None,
sync_clock_store: SyncClockStore | None = None,
sync_clock_manager: SyncClockManager | None = None,
):
"""Initialize global dependencies."""
global _device_store, _template_store, _processor_manager
global _pp_template_store, _pattern_template_store, _picture_source_store, _picture_target_store
global _color_strip_store, _audio_source_store, _audio_template_store
global _value_source_store, _automation_store, _scene_preset_store, _automation_engine, _auto_backup_engine
global _sync_clock_store, _sync_clock_manager
_device_store = device_store
_template_store = template_store
_processor_manager = processor_manager
@@ -176,3 +197,5 @@ def init_dependencies(
_scene_preset_store = scene_preset_store
_automation_engine = automation_engine
_auto_backup_engine = auto_backup_engine
_sync_clock_store = sync_clock_store
_sync_clock_manager = sync_clock_manager