- Sends card ID as plain text with Content-Type: text/plain - Uses 10-digit zero-padded format (e.g., "0983187584") - Tries /api/input first, falls back to /api/play - Updated default URL to 100.94.163.117:8547 - Version bump to 1.2.0
232 lines
7.3 KiB
Python
232 lines
7.3 KiB
Python
#!/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 (10-digit decimal format).
|
|
|
|
When a LEGO Dimensions disc is placed on the portal, it sends the
|
|
10-digit card ID as plain text to the player (like keyboard input).
|
|
|
|
v1.2.0 Changes:
|
|
- Sends 10-digit card ID as plain text (like keyboard entry)
|
|
- No JSON, just the raw card ID string
|
|
- Updated default API URL
|
|
"""
|
|
|
|
import sys
|
|
import time
|
|
import json
|
|
import requests
|
|
from typing import Dict, Optional
|
|
|
|
from lego_dimensions_reader import (
|
|
LegoDimensionsReader,
|
|
TagInfo,
|
|
Pad,
|
|
COLORS
|
|
)
|
|
|
|
__version__ = "1.2.0"
|
|
|
|
|
|
class MoonlightDimensionsClient:
|
|
"""
|
|
Integration client connecting LEGO Dimensions portal to
|
|
Moonlight Drive-In video player.
|
|
|
|
Sends 10-digit NFC card ID as plain text to the player,
|
|
simulating keyboard input.
|
|
"""
|
|
|
|
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://100.94.163.117:8547")
|
|
mapping_file: Optional JSON file mapping card IDs to video paths (for local display only)
|
|
"""
|
|
self.api_url = api_url.rstrip('/')
|
|
self.reader = LegoDimensionsReader()
|
|
|
|
# Optional local mapping for display purposes
|
|
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_card_id: Optional[str] = None
|
|
|
|
def _load_mapping(self, filepath: str):
|
|
"""Load card ID to video mapping from JSON file (optional, for display)."""
|
|
try:
|
|
with open(filepath, 'r') as f:
|
|
data = json.load(f)
|
|
raw_mappings = data.get('mappings', data)
|
|
for key, value in raw_mappings.items():
|
|
self.video_mapping[str(key)] = value
|
|
print(f"Loaded {len(self.video_mapping)} video mappings")
|
|
except FileNotFoundError:
|
|
pass # Mapping file is optional
|
|
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 - send card ID to player."""
|
|
# Get 10-digit zero-padded card ID
|
|
card_id = tag.nfc_card_id_str # "0983187584"
|
|
|
|
print(f"\n{'='*50}")
|
|
print(f"✓ TAG DETECTED on {tag.pad.name} pad")
|
|
print(f" UID: {tag.uid_hex}")
|
|
print(f" Card ID: {card_id}")
|
|
|
|
# Send card ID to player
|
|
self._send_card_id(card_id)
|
|
|
|
# Set pad to green to indicate sent
|
|
self.reader.set_pad_color(tag.pad, COLORS['GREEN'])
|
|
|
|
# Show mapped video if we have local mapping
|
|
video_path = self.video_mapping.get(str(tag.nfc_card_id)) or \
|
|
self.video_mapping.get(card_id)
|
|
if video_path:
|
|
print(f" Mapped to: {video_path}")
|
|
|
|
print(f"{'='*50}")
|
|
|
|
def _on_tag_remove(self, tag: TagInfo):
|
|
"""Handle tag removal."""
|
|
card_id = tag.nfc_card_id_str
|
|
|
|
print(f"\n✗ TAG REMOVED from {tag.pad.name} pad")
|
|
print(f" Card ID: {card_id}")
|
|
|
|
# Optionally send stop command
|
|
self._send_stop()
|
|
|
|
# Turn off pad LED
|
|
self.reader.set_pad_color(tag.pad, COLORS['OFF'])
|
|
self._current_card_id = None
|
|
|
|
def _send_card_id(self, card_id: str):
|
|
"""Send the 10-digit card ID as plain text to the player."""
|
|
try:
|
|
# Send as plain text - like keyboard input
|
|
response = requests.post(
|
|
f"{self.api_url}/api/input",
|
|
data=card_id,
|
|
headers={'Content-Type': 'text/plain'},
|
|
timeout=5
|
|
)
|
|
|
|
if response.ok:
|
|
self._current_card_id = card_id
|
|
print(f" ▶ Sent: {card_id}")
|
|
else:
|
|
# Try alternate endpoint
|
|
response = requests.post(
|
|
f"{self.api_url}/api/play",
|
|
data=card_id,
|
|
headers={'Content-Type': 'text/plain'},
|
|
timeout=5
|
|
)
|
|
if response.ok:
|
|
self._current_card_id = card_id
|
|
print(f" ▶ Sent: {card_id}")
|
|
else:
|
|
print(f" ⚠ API Error: {response.status_code}")
|
|
|
|
except requests.exceptions.ConnectionError:
|
|
print(f" ⚠ Cannot connect to player at {self.api_url}")
|
|
except requests.exceptions.Timeout:
|
|
print(f" ⚠ Request timed out")
|
|
|
|
def _send_stop(self):
|
|
"""Send stop command to player."""
|
|
try:
|
|
requests.post(
|
|
f"{self.api_url}/api/stop",
|
|
timeout=5
|
|
)
|
|
print(f" ⏹ Stop sent")
|
|
except requests.exceptions.RequestException:
|
|
pass # Ignore stop errors
|
|
|
|
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"\nPlayer URL: {self.api_url}")
|
|
print("Sending 10-digit card IDs as text")
|
|
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 - UPDATE THIS TO YOUR PLAYER IP
|
|
MOONLIGHT_API = "http://100.94.163.117:8547"
|
|
MAPPING_FILE = "video_mappings.json" # Optional
|
|
|
|
client = MoonlightDimensionsClient(
|
|
api_url=MOONLIGHT_API,
|
|
mapping_file=MAPPING_FILE
|
|
)
|
|
|
|
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())
|