Send 10-digit card ID as plain text (keyboard-style input)

- 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
This commit is contained in:
2026-02-21 19:08:28 +11:00
parent 93041f0db8
commit a904f89d1d

View File

@@ -3,15 +3,15 @@
LEGO Dimensions + Moonlight Drive-In Integration LEGO Dimensions + Moonlight Drive-In Integration
Integrates the LEGO Dimensions portal reader with the Moonlight Drive-In Integrates the LEGO Dimensions portal reader with the Moonlight Drive-In
video player system using NFC card IDs (decimal format). video player system using NFC card IDs (10-digit decimal format).
When a LEGO Dimensions disc is placed on the portal, it triggers When a LEGO Dimensions disc is placed on the portal, it sends the
video playback. When removed, playback stops. 10-digit card ID as plain text to the player (like keyboard input).
v1.1.0 Changes: v1.2.0 Changes:
- Now uses nfc_card_id (decimal) for lookups and API calls - Sends 10-digit card ID as plain text (like keyboard entry)
- Mapping file keys can be decimal strings: "983187584": "path/to/video.mp4" - No JSON, just the raw card ID string
- API sends card_id as decimal integer for compatibility - Updated default API URL
""" """
import sys import sys
@@ -27,7 +27,7 @@ from lego_dimensions_reader import (
COLORS COLORS
) )
__version__ = "1.1.0" __version__ = "1.2.0"
class MoonlightDimensionsClient: class MoonlightDimensionsClient:
@@ -35,8 +35,8 @@ class MoonlightDimensionsClient:
Integration client connecting LEGO Dimensions portal to Integration client connecting LEGO Dimensions portal to
Moonlight Drive-In video player. Moonlight Drive-In video player.
Uses NFC card ID (decimal) format for compatibility with Sends 10-digit NFC card ID as plain text to the player,
standard NFC readers and existing mapping systems. simulating keyboard input.
""" """
def __init__(self, api_url: str, mapping_file: Optional[str] = None): def __init__(self, api_url: str, mapping_file: Optional[str] = None):
@@ -44,14 +44,13 @@ class MoonlightDimensionsClient:
Initialize the integration client. Initialize the integration client.
Args: Args:
api_url: Base URL for Moonlight Drive-In API (e.g., "http://drive-in:8547") 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 mapping_file: Optional JSON file mapping card IDs to video paths (for local display only)
""" """
self.api_url = api_url.rstrip('/') self.api_url = api_url.rstrip('/')
self.reader = LegoDimensionsReader() self.reader = LegoDimensionsReader()
# Load card ID to video mapping # Optional local mapping for display purposes
# Keys can be decimal strings like "983187584"
self.video_mapping: Dict[str, str] = {} self.video_mapping: Dict[str, str] = {}
if mapping_file: if mapping_file:
self._load_mapping(mapping_file) self._load_mapping(mapping_file)
@@ -64,26 +63,19 @@ class MoonlightDimensionsClient:
self.reader.on_error = self._on_error self.reader.on_error = self._on_error
# Track currently playing # Track currently playing
self._current_video: Optional[str] = None self._current_card_id: Optional[str] = None
self._current_card_id: Optional[int] = None
def _load_mapping(self, filepath: str): def _load_mapping(self, filepath: str):
"""Load card ID to video mapping from JSON file.""" """Load card ID to video mapping from JSON file (optional, for display)."""
try: try:
with open(filepath, 'r') as f: with open(filepath, 'r') as f:
data = json.load(f) data = json.load(f)
# Support both "mappings" wrapper and direct dict
raw_mappings = data.get('mappings', data) raw_mappings = data.get('mappings', data)
# Normalize keys to strings
for key, value in raw_mappings.items(): for key, value in raw_mappings.items():
# Accept both string and numeric keys
self.video_mapping[str(key)] = value self.video_mapping[str(key)] = value
print(f"Loaded {len(self.video_mapping)} video mappings") print(f"Loaded {len(self.video_mapping)} video mappings")
except FileNotFoundError: except FileNotFoundError:
print(f"Warning: Mapping file not found: {filepath}") pass # Mapping file is optional
print("Tags will be logged but no videos will play.")
except json.JSONDecodeError as e: except json.JSONDecodeError as e:
print(f"Warning: Invalid JSON in mapping file: {e}") print(f"Warning: Invalid JSON in mapping file: {e}")
@@ -105,103 +97,95 @@ class MoonlightDimensionsClient:
print(f"\n⚠ Error: {error}") print(f"\n⚠ Error: {error}")
def _on_tag_insert(self, tag: TagInfo): def _on_tag_insert(self, tag: TagInfo):
"""Handle tag placement - trigger video playback.""" """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"\n{'='*50}")
print(f"✓ TAG DETECTED on {tag.pad.name} pad") print(f"✓ TAG DETECTED on {tag.pad.name} pad")
print(f" UID: {tag.uid_hex}") print(f" UID: {tag.uid_hex}")
print(f" NFC Card ID: {tag.nfc_card_id}") print(f" Card ID: {card_id}")
# Look up video by decimal card ID # Send card ID to player
card_id_str = str(tag.nfc_card_id) self._send_card_id(card_id)
video_path = self.video_mapping.get(card_id_str)
# Also try zero-padded format # Set pad to green to indicate sent
if not video_path: self.reader.set_pad_color(tag.pad, COLORS['GREEN'])
video_path = self.video_mapping.get(tag.nfc_card_id_str)
# 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: if video_path:
print(f" Video: {video_path}") print(f" Mapped to: {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}") print(f"{'='*50}")
def _on_tag_remove(self, tag: TagInfo): def _on_tag_remove(self, tag: TagInfo):
"""Handle tag removal - stop playback.""" """Handle tag removal."""
print(f"\n✗ TAG REMOVED from {tag.pad.name} pad") card_id = tag.nfc_card_id_str
print(f" NFC Card ID: {tag.nfc_card_id}")
# Stop video if one is playing print(f"\n✗ TAG REMOVED from {tag.pad.name} pad")
if self._current_video: print(f" Card ID: {card_id}")
self._stop_video()
# Optionally send stop command
self._send_stop()
# Turn off pad LED # Turn off pad LED
self.reader.set_pad_color(tag.pad, COLORS['OFF']) self.reader.set_pad_color(tag.pad, COLORS['OFF'])
self._current_card_id = None
def _play_video(self, video_path: str, card_id: int): def _send_card_id(self, card_id: str):
"""Send play command to Moonlight API with card ID.""" """Send the 10-digit card ID as plain text to the player."""
try: try:
# Send as plain text - like keyboard input
response = requests.post( response = requests.post(
f"{self.api_url}/api/play", f"{self.api_url}/api/input",
json={ data=card_id,
"path": video_path, headers={'Content-Type': 'text/plain'},
"card_id": card_id # Send decimal card ID
},
timeout=5 timeout=5
) )
if response.ok: if response.ok:
self._current_video = video_path
self._current_card_id = card_id self._current_card_id = card_id
print(f"Playing: {video_path}") print(f"Sent: {card_id}")
else: else:
print(f" ⚠ API Error: {response.status_code}") # 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: except requests.exceptions.ConnectionError:
print(f" ⚠ Cannot connect to Moonlight API at {self.api_url}") print(f" ⚠ Cannot connect to player at {self.api_url}")
except requests.exceptions.Timeout: except requests.exceptions.Timeout:
print(f"API request timed out") print(f"Request timed out")
def _stop_video(self): def _send_stop(self):
"""Send stop command to Moonlight API.""" """Send stop command to player."""
try: try:
response = requests.post( requests.post(
f"{self.api_url}/api/stop", f"{self.api_url}/api/stop",
json={
"card_id": self._current_card_id
} if self._current_card_id else {},
timeout=5 timeout=5
) )
print(f" ⏹ Stop sent")
if response.ok:
print(f" ⏹ Stopped playback")
self._current_video = None
self._current_card_id = None
except requests.exceptions.RequestException: except requests.exceptions.RequestException:
pass # Ignore stop errors 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): def start(self):
"""Start the integration client.""" """Start the integration client."""
print("\n" + "="*50) print("\n" + "="*50)
print(" LEGO Dimensions + Moonlight Drive-In") print(" LEGO Dimensions + Moonlight Drive-In")
print(f" Integration v{__version__}") print(f" Integration v{__version__}")
print("="*50) print("="*50)
print(f"\nMoonlight API: {self.api_url}") print(f"\nPlayer URL: {self.api_url}")
print(f"Video mappings: {len(self.video_mapping)}") print("Sending 10-digit card IDs as text")
print("Using NFC Card ID format (decimal)")
print("\nStarting portal connection...") print("\nStarting portal connection...")
self.reader.start() self.reader.start()
@@ -214,26 +198,15 @@ class MoonlightDimensionsClient:
def main(): def main():
"""Main entry point.""" """Main entry point."""
# Configuration # Configuration - UPDATE THIS TO YOUR PLAYER IP
MOONLIGHT_API = "http://drive-in:8547" # Change to your server MOONLIGHT_API = "http://100.94.163.117:8547"
MAPPING_FILE = "video_mappings.json" # Optional mapping file MAPPING_FILE = "video_mappings.json" # Optional
# 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( client = MoonlightDimensionsClient(
api_url=MOONLIGHT_API, api_url=MOONLIGHT_API,
mapping_file=MAPPING_FILE mapping_file=MAPPING_FILE
) )
# You can also add mappings programmatically:
# client.add_mapping(983187584, "videos/custom.mp4")
try: try:
client.start() client.start()