Files
hms-mediaengine/apps/control_server/hms_control_server/renderer_link.py
T

91 lines
3.4 KiB
Python
Raw Normal View History

"""RendererStateLink auf der Control-Core-Seite (PLAN.md §6.2, §6.4).
Verbindet ProjectStateStore (autoritativ) mit dem Renderer über IPC:
- connect_and_sync(): Handshake + vollständiger Snapshot (§6.2 Pflicht)
- sync_if_changed(): Delta seit letzter gesendeter Revision
- Reconnect: immer neuer Snapshot, danach erst wieder Deltas (§6.2)
Der Link kennt den letzten beim Renderer angekommenen Zustand und
berechnet Deltas daraus keine halben Zustände (§11.4, §6.4).
"""
from __future__ import annotations
from hms_persistence.state_store import ProjectStateStore
from hms_protocol import Envelope, IpcClient, MessageType
class RendererStateLink:
"""Synchronisiert den Showzustand Control Core → Renderer über IPC."""
def __init__(self, client: IpcClient, store: ProjectStateStore) -> None:
self._client = client
self._store = store
self._last_sent_revision = 0
self._renderer_known_state: dict[str, float] = {}
@property
def last_sent_revision(self) -> int:
return self._last_sent_revision
@property
def in_sync(self) -> bool:
"""True, wenn der Renderer die aktuelle Revision besitzt."""
return self._last_sent_revision == self._store.state_revision
# ---------- Verbindung (§6.2) ----------
async def connect_and_sync(self) -> None:
"""Verbindung aufbauen und vollständigen Snapshot senden.
Nach jedem (Re-)Connect wird immer zuerst der vollständige Snapshot
übertragen; Deltas folgen erst danach (§6.2: „Re-Sync nach
Reconnect", „Deltas erst nach erfolgreichem Re-Sync akzeptiert").
"""
await self._client.connect()
await self.send_full_snapshot()
async def send_full_snapshot(self) -> int:
"""Sendet den vollständigen Zustand; liefert die gesendete Revision."""
snap = self._store.snapshot()
self._last_sent_revision = snap["state_revision"]
self._renderer_known_state = dict(snap["values"])
envelope = Envelope(
type=MessageType.SNAPSHOT,
revision=snap["state_revision"],
payload=snap,
)
await self._client.send(envelope)
return snap["state_revision"]
# ---------- Delta-Versand (§6.4) ----------
async def sync_if_changed(self) -> bool:
"""Sendet ein Delta, falls sich die Revision seit dem letzten Versand
geändert hat. Rückgabe: True, wenn etwas gesendet wurde.
Das Delta wird aus dem zuletzt bekannten Renderer-Zustand berechnet
(neu/geändert/gelöscht); die Semantik folgt StateDelta (§6.4).
"""
if self._store.state_revision == self._last_sent_revision:
return False # nichts Neues
delta = self._store.delta_since(
self._last_sent_revision, self._renderer_known_state
)
if delta is None:
return False
# Buchhaltung: was weiß der Renderer ab jetzt?
for path, value in delta.changes.items():
if value is None:
self._renderer_known_state.pop(path, None)
else:
self._renderer_known_state[path] = value
self._last_sent_revision = delta.state_revision
envelope = Envelope(
type=MessageType.EVENT,
revision=delta.state_revision,
payload=delta.to_dict(),
)
await self._client.send(envelope)
return True