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).
This commit is contained in:
HMS MediaEngine Agent
2026-09-11 00:36:59 +02:00
commit 0922cc1d68
133 changed files with 7939 additions and 0 deletions
View File
+85
View File
@@ -0,0 +1,85 @@
"""Art-Net-Emulator (PLAN.md §16, §29.2, §36 Nr. 78).
Simuliert ein Lichtpult für Gate-0-Tests:
- poll: sendet ArtPoll und zeigt die ArtPollReply des Nodes (Media Server)
- sweep: fährt einen Fader über Layer-Opacity (16 Bit, Kanäle 23) und
sendet ArtDMX mit Sequenznummern
Nur für Testnetze bestimmt; kein Ersatz für den Hardwarepulttest (§29.7).
"""
from __future__ import annotations
import argparse
import socket
import time
from hms_artnet.packets import build_dmx, build_poll, parse_poll_reply
def cmd_poll(host: str, port: int) -> int:
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
sock.settimeout(2.0)
sock.bind(("0.0.0.0", 0))
local_port = sock.getsockname()[1]
sock.sendto(build_poll(talk_to_me=0x00), (host, port))
try:
data, addr = sock.recvfrom(2048)
except TimeoutError:
print("keine ArtPollReply erhalten (Timeout)")
return 1
info = parse_poll_reply(data)
if info is None:
print(f"ungültige Antwort von {addr}")
return 1
print(f"ArtPollReply von {addr[0]} → lokal gebunden auf Port {local_port}")
print(f" IP: {info.ip}")
print(f" ShortName: {info.short_name}")
print(f" LongName: {info.long_name}")
print(f" NodeReport: {info.node_report}")
print(f" Style: 0x{info.style:02X} (0x02 = StMedia/Media Server)")
print(f" Ports: {info.num_ports}")
return 0
def cmd_sweep(host: str, port: int, universe: int, rate_hz: float, cycles: int) -> int:
sequence = 0
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
for _ in range(cycles):
for step in range(0, 256, 8):
# Layer64-Layout: Kanal 1 Enable, Kanal 2-3 Opacity 16 Bit (MSB zuerst)
data = bytearray(16)
data[0] = 255
data[1] = step
data[2] = 0
sequence = (sequence % 255) + 1
sock.sendto(build_dmx(universe, bytes(data), sequence=sequence), (host, port))
time.sleep(1.0 / rate_hz)
for step in range(255, -1, -8):
data = bytearray(16)
data[0] = 255
data[1] = step
data[2] = 0
sequence = (sequence % 255) + 1
sock.sendto(build_dmx(universe, bytes(data), sequence=sequence), (host, port))
time.sleep(1.0 / rate_hz)
print(f"sweep abgeschlossen: {cycles} Zyklen auf Universe {universe}")
return 0
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(prog="artnet_emulator")
parser.add_argument("--host", default="127.0.0.1")
parser.add_argument("--port", type=int, default=6454)
parser.add_argument("--mode", choices=["poll", "sweep"], required=True)
parser.add_argument("--universe", type=int, default=0)
parser.add_argument("--rate", type=float, default=40.0, help="Pakete pro Sekunde")
parser.add_argument("--cycles", type=int, default=1)
args = parser.parse_args(argv)
if args.mode == "poll":
return cmd_poll(args.host, args.port)
return cmd_sweep(args.host, args.port, args.universe, args.rate, args.cycles)
if __name__ == "__main__":
raise SystemExit(main())
View File
View File
View File
@@ -0,0 +1,148 @@
"""Fixture-Generator (PLAN.md §16.3, §16.4, §16.7).
Erzeugt menschenlesbare CSV-Kanallisten für die V1-Personalities:
- HMS MediaEngine Master 32ch
- HMS MediaEngine Layer 64ch
GDTF-/pultspezifische Personality-Dateien folgen in Phase 4; die hier
erzeugten Listen tragen bereits dieselbe Fixture-Schema-Version (§16.7).
"""
from __future__ import annotations
import argparse
import csv
from pathlib import Path
FIXTURE_SCHEMA_VERSION = 1
MASTER32: list[tuple[int, str, str]] = [
(1, "Master Intensity (MSB)", "16 Bit (mit Kanal 2)"),
(2, "Master Intensity (LSB)", "16 Bit"),
(3, "Blackout", "Trigger/Schalter, höchste Priorität"),
(4, "Freeze Output", "Schalter"),
(5, "Preset Bank", "8 Bit"),
(6, "Preset Index (MSB)", "16 Bit (mit Kanal 7)"),
(7, "Preset Index (LSB)", "16 Bit"),
(8, "Preset Recall", "steigende Flanke, direkter Abruf ohne Cue-GO-Logik"),
(9, "Transition Type", "Enum"),
(10, "Transition Duration (MSB)", "16 Bit, konfigurierter Maximalwert"),
(11, "Transition Duration (LSB)", "16 Bit"),
(12, "Global Speed (MSB)", "16 Bit"),
(13, "Global Speed (LSB)", "16 Bit"),
(14, "BPM (MSB)", "16 Bit"),
(15, "BPM (LSB)", "16 Bit"),
(16, "Tap Tempo", "steigende Flanke"),
(17, "reserviert (Cue/Timeline-Erweiterung)", "im MVP neutral ignorieren"),
(18, "reserviert (Cue/Timeline-Erweiterung)", "im MVP neutral ignorieren"),
(19, "reserviert (Cue/Timeline-Erweiterung)", "im MVP neutral ignorieren"),
(20, "reserviert (Cue/Timeline-Erweiterung)", "im MVP neutral ignorieren"),
(21, "reserviert (Cue/Timeline-Erweiterung)", "im MVP neutral ignorieren"),
(22, "Audio Reactive Enable", "Schalter"),
(23, "Audio Master Gain", "8 Bit"),
(24, "Automation/AI Enable", "nur Freigabe, keine Sicherheitsumgehung"),
(25, "Output Test Pattern", "Enum"),
(26, "Preview Enable", "Schalter"),
(27, "Global Hue", "8 Bit"),
(28, "Global Saturation", "8 Bit"),
(29, "Fallback Preset", "8 Bit"),
(30, "Release Manual Overrides", "Trigger mit Schutzlogik"),
(31, "reserviert", "muss neutral ignoriert werden"),
(32, "reserviert", "muss neutral ignoriert werden"),
]
def _layer64() -> list[tuple[int, str, str]]:
rows: list[tuple[int, str, str]] = [
(1, "Layer Enable", "Schalter"),
(2, "Opacity (MSB)", "16 Bit"),
(3, "Opacity (LSB)", "16 Bit"),
(4, "Source Type", "Enum: Media/Generator/Live/Solid"),
(5, "Media Bank", "8 Bit"),
(6, "Media Folder", "8 Bit"),
(7, "Media/Plugin Index (MSB)", "16 Bit"),
(8, "Media/Plugin Index (LSB)", "16 Bit"),
(9, "Load/Commit Selection", "steigende Flanke"),
(10, "Transport", "Enum: Stop/Play/Pause/Retrigger"),
(11, "Loop Mode", "Enum"),
(12, "Playback Direction/Mode", "Enum"),
(13, "Playback Speed (MSB)", "16 Bit, signed Mapping"),
(14, "Playback Speed (LSB)", "16 Bit, signed Mapping"),
(15, "Playback Position (MSB)", "16 Bit, normalisiert"),
(16, "Playback Position (LSB)", "16 Bit, normalisiert"),
(17, "In Point (MSB)", "16 Bit, normalisiert"),
(18, "In Point (LSB)", "16 Bit, normalisiert"),
(19, "Out Point (MSB)", "16 Bit, normalisiert"),
(20, "Out Point (LSB)", "16 Bit, normalisiert"),
(21, "Blend Mode", "Enum"),
(22, "Transform Mode/Anchor", "Enum"),
(23, "Position X (MSB)", "16 Bit, signed"),
(24, "Position X (LSB)", "16 Bit, signed"),
(25, "Position Y (MSB)", "16 Bit, signed"),
(26, "Position Y (LSB)", "16 Bit, signed"),
(27, "Scale X (MSB)", "16 Bit"),
(28, "Scale X (LSB)", "16 Bit"),
(29, "Scale Y (MSB)", "16 Bit"),
(30, "Scale Y (LSB)", "16 Bit"),
(31, "Rotation (MSB)", "16 Bit"),
(32, "Rotation (LSB)", "16 Bit"),
(33, "Crop Left", "8 Bit"),
(34, "Crop Right", "8 Bit"),
(35, "Crop Top", "8 Bit"),
(36, "Crop Bottom", "8 Bit"),
(37, "Hue", "8 Bit"),
(38, "Saturation", "8 Bit"),
(39, "Brightness", "8 Bit"),
(40, "Contrast", "8 Bit"),
(41, "FX1 Enable", "Schalter"),
(42, "FX1 Plugin Select", "8 Bit, Show-Registry"),
(43, "FX1 Mix", "8 Bit"),
]
for slot in range(1, 9):
rows.append((43 + slot, f"FX1 Parameter P{slot}", "8 Bit oder manifestgebundene Paare"))
rows.extend(
[
(52, "FX2 Enable", "Schalter"),
(53, "FX2 Plugin Select", "8 Bit, Show-Registry"),
(54, "FX2 Mix", "8 Bit"),
]
)
for slot in range(1, 9):
rows.append((54 + slot, f"FX2 Parameter P{slot}", "8 Bit oder manifestgebundene Paare"))
rows.append((63, "Layer Retrigger/Reset", "steigende Flanke"))
rows.append((64, "reserviert", "muss neutral ignoriert werden"))
return rows
LAYER64: list[tuple[int, str, str]] = _layer64()
_HEADER = ["channel", "parameter", "resolution_behavior"]
def write_csv(rows: list[tuple[int, str, str]], path: Path) -> Path:
"""Schreibt eine Kanalliste als CSV; Elternordner werden angelegt."""
if [r[0] for r in rows] != list(range(1, len(rows) + 1)):
raise ValueError("channel numbers must be contiguous starting at 1")
path.parent.mkdir(parents=True, exist_ok=True)
with open(path, "w", newline="", encoding="utf-8") as fh:
writer = csv.writer(fh)
writer.writerow(_HEADER)
writer.writerows(rows)
return path
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(prog="fixture_generator")
parser.add_argument("--out-dir", default="fixture_profiles", help="Zielordner")
args = parser.parse_args(argv)
out = Path(args.out_dir)
master = write_csv(MASTER32, out / "master32" / "master32.csv")
layer = write_csv(LAYER64, out / "layer64" / "layer64.csv")
print(f"master32: {master} ({len(MASTER32)} Kanäle)")
print(f"layer64: {layer} ({len(LAYER64)} Kanäle)")
print(f"fixture_schema_version: {FIXTURE_SCHEMA_VERSION}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
View File
View File