Files
hms-mediaengine/packages/artnet/hms_artnet/mapping.py
T
HMS MediaEngine Agent 0922cc1d68 Phase 0: Repository-Initialisierung nach Bauplan v1.2
- Struktur gemäß §8 (Eigentumsgrenzen), PLAN.md als normative Basis
- Pflichtdokumente: STATUS.md, ERRORS.md, TEST_REPORT.md, CHANGELOG.md, ADRs
- ADR-0001 Python 3.13-Pin, ADR-0002 GStreamer 1.28.6-Pin (Windows),
  ADR-0003 IPC TCP+MessagePack v1
- Kernpakete: hms_protocol, hms_domain, hms_parameter, hms_artnet,
  hms_adaptive, hms_capabilities, hms_plugin_sdk
- Renderer-Spike: D3D11-Primärpfad + Dev-GL-Pfad (§36 Nr. 4-5)
- Control Core: FastAPI REST + WebSocket (§36 Nr. 9)
- Beispielplugins: Passthrough + Gaussian Blur (3 Adaptive-Quality-
  Varianten, HLSL/GLSL/GLES)
- Tools: Art-Net-Emulator, Fixture-Generator (Master32/Layer64-CSV),
  Capability-Probe
- JSON-Schemas: IPC, Plugin, Projekt, Cluster
- 121 Unit-/Integrationstests grün, Ruff grün

Gate 0 bleibt offen: Hardwaremessungen nur auf echter Windows-Referenz-
hardware gültig (§29.7, §33).
2026-09-11 00:36:59 +02:00

112 lines
4.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""DMX→Parameter-Mapping (PLAN.md §16.416.6, §36 Nr. 8).
Bildet DMX-Kanäle eines Universes auf stabile Parameterpfade ab:
- 8-Bit: Byte / 255
- 16-Bit: (MSB << 8 | LSB) / 65535, MSB zuerst (DMX-Konvention)
- Flankenerkennung für Trigger (steigende Flanke, §16.3/§16.5)
- Signalverlust je konfigurierter Policy; HOLD ist V1-Standard (§11.3)
Alle Werte fließen ausschließlich über die Parameter-Engine in den Control
Core (§11); der Renderer wird nie direkt berührt.
"""
from __future__ import annotations
from dataclasses import dataclass
from hms_parameter.engine import ControlSource, ParameterEngine
from hms_parameter.paths import layer_opacity_path
from hms_artnet.receiver import DmxUpdate, LossBehavior
class RisingEdge:
"""Erkennt steigende Flanken über einer Schwelle (§16.5).
Ein Trigger ist ein Ereignis, kein Dauerzustand: derselbe gehaltene
Faderwert löst genau einmal aus; erst nach Rückkehr unter die Schwelle
kann erneut getriggert werden.
"""
def __init__(self, threshold: int = 64) -> None:
if not 0 <= threshold <= 255:
raise ValueError("threshold must be 0..255")
self.threshold = threshold
self._was_active = False
def feed(self, value: int) -> bool:
active = value >= self.threshold
triggered = active and not self._was_active
self._was_active = active
return triggered
@dataclass(frozen=True)
class LayerDmxMapping:
"""Layer-Fixture-Belegung (Auszug Layer64, §16.4).
base_address: 1-basierte DMX-Startadresse des Layer-Fixtures
Kanäle relativ: 1 = Enable, 23 = Opacity (16 Bit, MSB zuerst)
"""
universe: int
base_address: int
composition_id: str
layer_id: str
loss_behavior: LossBehavior = LossBehavior.HOLD
def __post_init__(self) -> None:
if not 0 <= self.universe < 0x8000:
raise ValueError("universe must be 0..0x7FFF")
if not 1 <= self.base_address <= 512 - 2:
raise ValueError("base_address must leave room for channels 1..3")
@property
def enable_path(self) -> str:
return f"composition/{self.composition_id}/layer/{self.layer_id}/enabled"
@property
def opacity_path(self) -> str:
return layer_opacity_path(self.composition_id, self.layer_id)
class DmxLayerMapper:
"""Wandelt DmxUpdates eines Universes in Parameter-Engine-Werte.
Phase-0-Umfang (§36 Nr. 8): DMX-Kanal auf Layer-Opacity mappen.
Media-Auswahl mit Load/Commit-Semantik (§16.5) folgt in Phase 2;
RisingEdge ist bereits getestet verfügbar.
"""
def __init__(
self,
mapping: LayerDmxMapping,
engine: ParameterEngine,
source: ControlSource = ControlSource.CONSOLE,
) -> None:
self._mapping = mapping
self._engine = engine
self._source = source
def _channel(self, data: bytes, relative: int) -> int:
"""Liest Kanal relativ zur Base-Adresse (1-basiert); 0 wenn zu kurz."""
idx = self._mapping.base_address - 1 + (relative - 1)
if 0 <= idx < len(data):
return data[idx]
return 0
def handle(self, update: DmxUpdate) -> None:
if update.universe != self._mapping.universe:
return
if update.sequence == -1 and not update.data:
# Signalverlust (§11.3, §16.1): Policy anwenden, niemals still
if self._mapping.loss_behavior is LossBehavior.FADE_TO_BLACK:
self._engine.release(self._mapping.opacity_path, self._source)
self._engine.release(self._mapping.enable_path, self._source)
# HOLD: letzten Zustand behalten keine Aktion
return
enable = 1.0 if self._channel(update.data, 1) >= 128 else 0.0
opacity = ((self._channel(update.data, 2) << 8) | self._channel(update.data, 3)) / 65535.0
self._engine.set_value(self._mapping.enable_path, enable, self._source)
self._engine.set_value(self._mapping.opacity_path, opacity, self._source)