#!/usr/bin/env python3 """ LEGO Dimensions + Moonlight Drive-In Integration Integrates the LEGO Dimensions portal reader with the Moonlight Drive-In video player system using NFC card IDs (decimal format). When a LEGO Dimensions disc is placed on the portal, it triggers video playback. When removed, playback stops. v1.1.0 Changes: - Now uses nfc_card_id (decimal) for lookups and API calls - Mapping file keys can be decimal strings: "983187584": "path/to/video.mp4" - API sends card_id as decimal integer for compatibility """ import sys import time import json import requests from typing import Dict, Optional from lego_dimensions_reader import ( LegoDimensionsReader, TagInfo, Pad, COLORS ) __version__ = "1.1.0" class MoonlightDimensionsClient: """ Integration client connecting LEGO Dimensions portal to Moonlight Drive-In video player. Uses NFC card ID (decimal) format for compatibility with standard NFC readers and existing mapping systems. """ 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 card IDs to video paths """ self.api_url = api_url.rstrip('/') self.reader = LegoDimensionsReader() # Load card ID to video mapping # Keys can be decimal strings like "983187584" 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 self._current_card_id: Optional[int] = None def _load_mapping(self, filepath: str): """Load card ID to video mapping from JSON file.""" try: with open(filepath, 'r') as f: data = json.load(f) # Support both "mappings" wrapper and direct dict raw_mappings = data.get('mappings', data) # Normalize keys to strings for key, value in raw_mappings.items(): # Accept both string and numeric keys self.video_mapping[str(key)] = value 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}") print(f" NFC Card ID: {tag.nfc_card_id}") # Look up video by decimal card ID card_id_str = str(tag.nfc_card_id) video_path = self.video_mapping.get(card_id_str) # Also try zero-padded format if not video_path: video_path = self.video_mapping.get(tag.nfc_card_id_str) if video_path: print(f" Video: {video_path}") self._play_video(video_path, tag.nfc_card_id) # 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.nfc_card_id}\": \"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") print(f" NFC Card ID: {tag.nfc_card_id}") # 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, card_id: int): """Send play command to Moonlight API with card ID.""" try: response = requests.post( f"{self.api_url}/api/play", json={ "path": video_path, "card_id": card_id # Send decimal card ID }, timeout=5 ) if response.ok: self._current_video = video_path self._current_card_id = card_id 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", json={ "card_id": self._current_card_id } if self._current_card_id else {}, timeout=5 ) if response.ok: print(f" ⏹ Stopped playback") self._current_video = None self._current_card_id = None except requests.exceptions.RequestException: pass # Ignore stop errors def add_mapping(self, card_id: int, video_path: str): """Add a card ID to video mapping at runtime.""" self.video_mapping[str(card_id)] = video_path def start(self): """Start the integration client.""" print("\n" + "="*50) print(" LEGO Dimensions + Moonlight Drive-In") print(f" Integration v{__version__}") print("="*50) print(f"\nMoonlight API: {self.api_url}") print(f"Video mappings: {len(self.video_mapping)}") print("Using NFC Card ID format (decimal)") 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): # { # "983187584": "videos/batman_intro.mp4", # "1234567890": "videos/gandalf_intro.mp4" # } # # Keys are decimal NFC card IDs (what standard NFC readers show) client = MoonlightDimensionsClient( api_url=MOONLIGHT_API, mapping_file=MAPPING_FILE ) # You can also add mappings programmatically: # client.add_mapping(983187584, "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())