"""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())