from __future__ import annotations import argparse from pathlib import Path import time from .engine import OutputSettings, RealtimeEngine from .mapping_xml import load_mapping from .models import PatternParams, SceneState def main() -> None: parser = argparse.ArgumentParser(description="Run the Infinity_Vis_1 live DDP sender.") parser.add_argument( "--mapping", type=Path, default=Path(__file__).resolve().parents[2] / "sample_data" / "infinity_mirror_mapping_clean.xml", help="Path to the XML mapping file.", ) parser.add_argument("--pattern", type=str, default="solid", help="Pattern id.") parser.add_argument("--tempo", type=float, default=120.0, help="Tempo in BPM.") parser.add_argument("--fps", type=float, default=40.0, help="Target output FPS.") parser.add_argument("--preview-fps", type=float, default=0.0, help="Preview cadence for snapshot generation.") parser.add_argument("--duration", type=float, default=0.0, help="Optional runtime limit in seconds. 0 means until Ctrl+C.") parser.add_argument("--primary-color", type=str, default="#4D7CFF", help="Primary color in #RRGGBB.") parser.add_argument("--secondary-color", type=str, default="#0E1630", help="Secondary color in #RRGGBB.") parser.add_argument("--palette", type=str, default="Laser Club", help="Palette name.") parser.add_argument("--brightness", type=float, default=1.0, help="Global pattern brightness 0..1.") parser.add_argument("--tempo-multiplier", type=float, default=1.0, help="Pattern tempo multiplier.") parser.add_argument("--color-mode", type=str, default="dual", help="Color mode, for example dual or palette.") parser.add_argument("--strobe-mode", type=str, default="global", help="Strobe mode.") parser.add_argument("--keepalive", type=float, default=0.35, help="Keepalive interval for unchanged payloads.") parser.add_argument("--no-delta", action="store_true", help="Always send every controller every frame.") args = parser.parse_args() mapping = load_mapping(args.mapping) scene = SceneState( pattern_id=args.pattern, tempo_bpm=args.tempo, params=PatternParams( brightness=max(0.0, min(1.0, args.brightness)), tempo_multiplier=args.tempo_multiplier, color_mode=args.color_mode, primary_color=args.primary_color, secondary_color=args.secondary_color, palette=args.palette, strobe_mode=args.strobe_mode, ), ) settings = OutputSettings( output_fps=max(1.0, args.fps), preview_fps=max(0.0, args.preview_fps), keepalive_seconds=max(0.0, args.keepalive), delta_sending=not args.no_delta, preview_enabled=args.preview_fps > 0.0, ) engine = RealtimeEngine(mapping, scene=scene, settings=settings) deadline = time.perf_counter() + args.duration if args.duration > 0 else None last_report = time.perf_counter() print(f"Infinity_Vis_1 live sender started") print(f"Mapping: {mapping.name}") print(f"Pattern: {scene.pattern_id} | Tempo: {scene.tempo_bpm:.1f} BPM | Output FPS: {settings.output_fps:.1f}") print(f"Controllers: {engine.diagnostics.controller_count} | Tiles: {engine.diagnostics.tile_count} | LEDs: {engine.diagnostics.total_leds}") print("Press Ctrl+C to stop.") try: while True: now = time.perf_counter() if deadline is not None and now >= deadline: break engine.send_due(now=now) if now - last_report >= 1.0: diagnostics = engine.diagnostics print( "output={output_frames} preview={preview_frames} packets={packets_sent} " "changed={changed} keepalive={keepalive} stale={stale} render={render_ms:.2f} ms".format( output_frames=diagnostics.output_frames, preview_frames=diagnostics.preview_frames, packets_sent=diagnostics.packets_sent, changed=diagnostics.changed_controller_frames, keepalive=diagnostics.keepalive_controller_frames, stale=diagnostics.stale_output_frames, render_ms=diagnostics.last_render_ms, ) ) last_report = now time.sleep(0.001) except KeyboardInterrupt: print("Stopped by user.") if __name__ == "__main__": main()