From c517d78ce65981d5a61aa610867d0b1b8c2ef2d3 Mon Sep 17 00:00:00 2001 From: jessikitty Date: Sat, 24 Jan 2026 10:36:29 +1100 Subject: [PATCH] Add Moonlight Drive-In integration example --- moonlight_integration.py | 220 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 220 insertions(+) create mode 100644 moonlight_integration.py diff --git a/moonlight_integration.py b/moonlight_integration.py new file mode 100644 index 0000000..10c1b0f --- /dev/null +++ b/moonlight_integration.py @@ -0,0 +1,220 @@ +#!/usr/bin/env python3 +""" +LEGO Dimensions + Moonlight Drive-In Integration Example + +This example shows how to integrate the LEGO Dimensions portal reader +with the Moonlight Drive-In video player system. + +When a LEGO Dimensions disc is placed on the portal, it triggers +video playback. When removed, playback stops. +""" + +import sys +import time +import json +import requests +from typing import Dict, Optional + +from lego_dimensions_reader import ( + LegoDimensionsReader, + TagInfo, + Pad, + COLORS +) + + +class MoonlightDimensionsClient: + """ + Integration client connecting LEGO Dimensions portal to + Moonlight Drive-In video player. + """ + + def __init__(self, api_url: str, mapping_file: Optional[str] = None): + """ + Initialize the integration client. + + Args: + api_url: Base URL for Moonlight Drive-In API (e.g., "http://drive-in:8547") + mapping_file: Optional JSON file mapping UIDs to video paths + """ + self.api_url = api_url.rstrip('/') + self.reader = LegoDimensionsReader() + + # Load UID to video mapping + self.video_mapping: Dict[str, str] = {} + if mapping_file: + self._load_mapping(mapping_file) + + # Set up callbacks + self.reader.on_tag_insert = self._on_tag_insert + self.reader.on_tag_remove = self._on_tag_remove + self.reader.on_connect = self._on_connect + self.reader.on_disconnect = self._on_disconnect + self.reader.on_error = self._on_error + + # Track currently playing + self._current_video: Optional[str] = None + + def _load_mapping(self, filepath: str): + """Load UID to video mapping from JSON file.""" + try: + with open(filepath, 'r') as f: + data = json.load(f) + self.video_mapping = data.get('mappings', data) + print(f"Loaded {len(self.video_mapping)} video mappings") + except FileNotFoundError: + print(f"Warning: Mapping file not found: {filepath}") + print("Tags will be logged but no videos will play.") + except json.JSONDecodeError as e: + print(f"Warning: Invalid JSON in mapping file: {e}") + + def _on_connect(self): + """Handle portal connection.""" + print("\n✓ LEGO Dimensions Portal Connected") + print(" Ready to detect discs...") + + # Flash all pads blue to indicate ready + for pad in [Pad.CENTER, Pad.LEFT, Pad.RIGHT]: + self.reader.flash_pad(pad, COLORS['BLUE'], count=2) + + def _on_disconnect(self): + """Handle portal disconnection.""" + print("\n✗ Portal Disconnected") + + def _on_error(self, error: Exception): + """Handle errors.""" + print(f"\n⚠ Error: {error}") + + def _on_tag_insert(self, tag: TagInfo): + """Handle tag placement - trigger video playback.""" + print(f"\n{'='*50}") + print(f"✓ TAG DETECTED on {tag.pad.name} pad") + print(f" UID: {tag.uid_hex}") + + # Check if we have a video mapped for this UID + video_path = self.video_mapping.get(tag.uid_hex) + + if video_path: + print(f" Video: {video_path}") + self._play_video(video_path) + + # Set pad to green to indicate playing + self.reader.set_pad_color(tag.pad, COLORS['GREEN']) + else: + print(f" No video mapped for this tag") + print(f" Add mapping: \"{tag.uid_hex}\": \"path/to/video.mp4\"") + + # Flash yellow to indicate unmapped tag + self.reader.flash_pad(tag.pad, COLORS['YELLOW'], count=3) + + print(f"{'='*50}") + + def _on_tag_remove(self, tag: TagInfo): + """Handle tag removal - stop playback.""" + print(f"\n✗ TAG REMOVED from {tag.pad.name} pad") + + # Stop video if one is playing + if self._current_video: + self._stop_video() + + # Turn off pad LED + self.reader.set_pad_color(tag.pad, COLORS['OFF']) + + def _play_video(self, video_path: str): + """Send play command to Moonlight API.""" + try: + response = requests.post( + f"{self.api_url}/api/play", + json={"path": video_path}, + timeout=5 + ) + + if response.ok: + self._current_video = video_path + print(f" ▶ Playing: {video_path}") + else: + print(f" ⚠ API Error: {response.status_code}") + + except requests.exceptions.ConnectionError: + print(f" ⚠ Cannot connect to Moonlight API at {self.api_url}") + except requests.exceptions.Timeout: + print(f" ⚠ API request timed out") + + def _stop_video(self): + """Send stop command to Moonlight API.""" + try: + response = requests.post( + f"{self.api_url}/api/stop", + timeout=5 + ) + + if response.ok: + print(f" ⏹ Stopped playback") + self._current_video = None + + except requests.exceptions.RequestException: + pass # Ignore stop errors + + def add_mapping(self, uid: str, video_path: str): + """Add a UID to video mapping at runtime.""" + self.video_mapping[uid.upper()] = video_path + + def start(self): + """Start the integration client.""" + print("\n" + "="*50) + print(" LEGO Dimensions + Moonlight Drive-In") + print("="*50) + print(f"\nMoonlight API: {self.api_url}") + print(f"Video mappings: {len(self.video_mapping)}") + print("\nStarting portal connection...") + + self.reader.start() + + def stop(self): + """Stop the integration client.""" + self.reader.disconnect() + print("\nClient stopped.") + + +def main(): + """Main entry point.""" + # Configuration + MOONLIGHT_API = "http://drive-in:8547" # Change to your server + MAPPING_FILE = "video_mappings.json" # Optional mapping file + + # Example mapping file format (video_mappings.json): + # { + # "04A1B2C3D4E5F6": "videos/batman_intro.mp4", + # "04D5E6F7A8B9C0": "videos/gandalf_intro.mp4" + # } + + client = MoonlightDimensionsClient( + api_url=MOONLIGHT_API, + mapping_file=MAPPING_FILE + ) + + # You can also add mappings programmatically: + # client.add_mapping("04AABBCCDDEE00", "videos/custom.mp4") + + try: + client.start() + + print("\nPlace LEGO Dimensions discs on the portal...") + print("Press Ctrl+C to exit\n") + + while True: + time.sleep(1) + + except KeyboardInterrupt: + print("\n\nShutting down...") + except ConnectionError as e: + print(f"\nFailed to connect: {e}") + return 1 + finally: + client.stop() + + return 0 + + +if __name__ == "__main__": + sys.exit(main())