362e089be0
Der Nutzer hat recht: Der Ordner war voller Entwicklungs-Muell. Jetzt ist sauber getrennt: ROOT (was der Nutzer sieht und braucht): - run.py = das Programm - hms_app/ = der Anwendungscode - HMS MediaEngine.app = macOS Doppelklick-Starter - HMS-Start.vbs = Windows Doppelklick-Starter - HMS-Install.vbs = Windows Erst-Installation - HMS-Mac-Install.command = macOS Homebrew-Installation - HMS-Portable-Install.command = macOS Portable-Installation (16GB-Fix) - installer_gui.py = grafischer Installer - launcher.pyw + launcher_core.py = interne Start-Logik - LIESMICH.txt = 10-Zeilen-Kurzanleitung - .gitignore _entwicklung/ (alles andere, NICHT benoetigt): - packages/ apps/ native/ plugins/ tools/ schemas/ tests/ docs/ build/ fixture_profiles/ - PLAN.md STATUS.md ERRORS.md TEST_REPORT.md CHANGELOG.md README.md - pyproject.toml uv.lock setup_*.sh/ps1 make_mac_app.py Diese Trennung gilt ab sofort fuer alle Commits. Der Nutzer kann _entwicklung/ loeschen wenn er Platz braucht - die App laeuft ohne. Verifiziert: App startet nach Aufraeumen unveraendert (Health 200).
97 lines
3.5 KiB
Python
97 lines
3.5 KiB
Python
"""Remote-State-Mirror auf der Renderer-Seite (PLAN.md §6.2, §6.4).
|
|
|
|
Der Renderer hält den zuletzt übermittelten Showzustand als lokales
|
|
Spiegelbild:
|
|
- nach Verbindung: vollständiger Snapshot (§6.2 Pflicht)
|
|
- danach inkrementelle Deltas mit monotoner Revision
|
|
- Deltas werden erst nach erfolgtem Snapshot akzeptiert (§6.2:
|
|
„Deltas erst nach erfolgreichem Re-Sync")
|
|
- veraltete/doppelte Deltas sind idempotente No-Ops (§6.5)
|
|
|
|
Der Mirror ist reine Zustandslogik ohne Pixelbezug (§33); der Rendergraph
|
|
liest Werte über get_value() je Frame.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
class RemoteStateMirror:
|
|
"""Spiegel des autoritativen Showzustands im Renderer."""
|
|
|
|
def __init__(self) -> None:
|
|
self._revision = 0
|
|
self._values: dict[str, float] = {}
|
|
self._has_snapshot = False
|
|
|
|
@property
|
|
def revision(self) -> int:
|
|
return self._revision
|
|
|
|
@property
|
|
def has_snapshot(self) -> bool:
|
|
"""True, nachdem ein vollständiger Snapshot empfangen wurde."""
|
|
return self._has_snapshot
|
|
|
|
def get_value(self, path: str, default: float | None = None) -> float | None:
|
|
"""Wirksamer Wert für einen Parameterpfad (§10.2)."""
|
|
return self._values.get(path, default)
|
|
|
|
def all_values(self) -> dict[str, float]:
|
|
return dict(self._values)
|
|
|
|
# ---------- Snapshot / Delta (§6.2, §6.4) ----------
|
|
|
|
def apply_snapshot(self, payload: dict) -> None:
|
|
"""Übernimmt einen vollständigen Snapshot (nach Verbindung/Reconnect)."""
|
|
values = payload.get("values", {})
|
|
if not isinstance(values, dict):
|
|
raise ValueError("Snapshot ohne Werte-Objekt")
|
|
self._revision = int(payload["state_revision"])
|
|
self._values = {str(k): float(v) for k, v in values.items()}
|
|
self._has_snapshot = True
|
|
|
|
def apply_delta(self, payload: dict) -> bool:
|
|
"""Wendet ein Delta an.
|
|
|
|
Rückgabe:
|
|
- True: Delta angewendet ODER als veraltetes Duplikat ignoriert
|
|
- False: Re-Sync nötig (noch kein Snapshot empfangen, §6.2)
|
|
|
|
Gelöschte Pfade sind als None kodiert (StateDelta-Vertrag).
|
|
"""
|
|
if not self._has_snapshot:
|
|
return False # §6.2: Deltas erst nach Snapshot
|
|
delta_revision = int(payload["state_revision"])
|
|
if delta_revision <= self._revision:
|
|
return True # idempotent: Duplikat/veraltet, kein Handlungsbedarf
|
|
changes = payload.get("changes", {})
|
|
if not isinstance(changes, dict):
|
|
raise ValueError("Delta ohne Changes-Objekt")
|
|
for path, value in changes.items():
|
|
if value is None:
|
|
self._values.pop(str(path), None) # gelöscht
|
|
else:
|
|
self._values[str(path)] = float(value)
|
|
self._revision = delta_revision
|
|
return True
|
|
|
|
|
|
def apply_envelope(mirror: RemoteStateMirror, envelope) -> bool:
|
|
"""Verarbeitet ein IPC-Envelope in den Mirror.
|
|
|
|
- SNAPSHOT: vollständige Übernahme
|
|
- EVENT mit changes: Delta-Anwendung
|
|
- alles andere (Heartbeat, Ack, …): keine Zustandswirkung
|
|
|
|
Rückgabe: True, wenn der Zustand dadurch (re-)synchronisiert wurde;
|
|
False, wenn ein Re-Sync (neuer Snapshot) angefordert werden muss.
|
|
"""
|
|
from hms_protocol import MessageType
|
|
|
|
if envelope.type is MessageType.SNAPSHOT:
|
|
mirror.apply_snapshot(envelope.payload)
|
|
return True
|
|
if envelope.type is MessageType.EVENT and "changes" in envelope.payload:
|
|
return mirror.apply_delta(envelope.payload)
|
|
return True # keine Zustandsnachricht: nichts zu tun
|