# LEGO Dimensions NFC Portal Reader Python module for reading LEGO Dimensions character and vehicle discs using the USB portal (PS3/PS4/Wii U versions) on Windows. Designed to integrate with the [Moonlight Drive-In](https://gitea.hideawaygaming.com.au/jessikitty/moonlight-drive-in) video player system. ## Features - **Event-driven tag detection** - Callbacks for tag insert/remove events - **LED control** - Set colors, flash, and fade effects on portal pads - **Thread-safe** - Background polling thread with proper locking - **Character database** - Built-in lookup for 80+ characters and vehicles - **TEA encryption** - Full encryption/decryption support for character IDs ## Hardware Requirements | Portal Version | Compatible | |----------------|------------| | PS3 | ✅ Yes | | PS4 | ✅ Yes | | Wii U | ✅ Yes | | Xbox 360 | ❌ No | | Xbox One | ❌ No | **USB Identifiers:** - Vendor ID: `0x0e6f` - Product ID: `0x0241` ## Installation ### 1. Install Python Package ```bash pip install pyusb ``` ### 2. Install Windows USB Driver (Required) The portal's default Windows HID driver must be replaced with WinUSB: 1. Download **Zadig** from https://zadig.akeo.ie/ 2. Connect the LEGO Dimensions portal via USB 3. Run Zadig **as Administrator** 4. Select **Options → List All Devices** 5. Choose **"LEGO READER V2.10"** from the dropdown 6. Select **WinUSB** as the target driver 7. Click **"Replace Driver"** 8. Unplug and reconnect the portal ## Quick Start ```python from lego_dimensions_reader import LegoDimensionsReader, COLORS def on_tag_placed(tag): print(f"Tag detected: {tag.uid_hex} on {tag.pad.name}") def on_tag_removed(tag): print(f"Tag removed from {tag.pad.name}") reader = LegoDimensionsReader() reader.on_tag_insert = on_tag_placed reader.on_tag_remove = on_tag_removed reader.start() # Keep running... import time try: while True: time.sleep(1) except KeyboardInterrupt: reader.disconnect() ``` ## API Reference ### LegoDimensionsReader Main class for portal communication. #### Callbacks | Callback | Signature | Description | |----------|-----------|-------------| | `on_tag_insert` | `(TagInfo) -> None` | Called when tag is placed | | `on_tag_remove` | `(TagInfo) -> None` | Called when tag is removed | | `on_connect` | `() -> None` | Called on successful connection | | `on_disconnect` | `() -> None` | Called on disconnection | | `on_error` | `(Exception) -> None` | Called on errors | #### Methods ```python reader.connect() # Connect to portal (called automatically by start()) reader.disconnect() # Disconnect and cleanup reader.start() # Start event polling thread reader.stop() # Stop polling thread # LED Control reader.set_pad_color(Pad.CENTER, COLORS['RED']) reader.flash_pad(Pad.LEFT, COLORS['GREEN'], on_time=10, off_time=10, count=5) reader.fade_pad(Pad.RIGHT, COLORS['BLUE'], speed=10, count=1) # State reader.get_active_tags() # Dict of tags currently on pads reader.is_connected # Boolean reader.is_running # Boolean ``` ### TagInfo Data class returned in callbacks. | Property | Type | Description | |----------|------|-------------| | `uid` | `bytes` | 7-byte tag UID | | `uid_hex` | `str` | UID as hex string | | `pad` | `Pad` | Which pad (CENTER, LEFT, RIGHT) | | `event` | `TagEvent` | INSERTED or REMOVED | ### Pad Enum ```python from lego_dimensions_reader import Pad Pad.ALL # All pads Pad.CENTER # Center pad Pad.LEFT # Left pad Pad.RIGHT # Right pad ``` ### Colors ```python from lego_dimensions_reader import COLORS COLORS['OFF'] # [0, 0, 0] COLORS['RED'] # [255, 0, 0] COLORS['GREEN'] # [0, 255, 0] COLORS['BLUE'] # [0, 0, 255] COLORS['WHITE'] # [255, 255, 255] COLORS['YELLOW'] # [255, 255, 0] COLORS['CYAN'] # [0, 255, 255] COLORS['MAGENTA'] # [255, 0, 255] COLORS['ORANGE'] # [255, 128, 0] COLORS['PURPLE'] # [128, 0, 255] ``` ## Integration with Moonlight Drive-In Example integration for triggering video playback: ```python import requests from lego_dimensions_reader import LegoDimensionsReader # Map tag UIDs to video files VIDEO_MAPPING = { "04A1B2C3D4E5F6": "videos/batman_intro.mp4", "04D5E6F7A8B9C0": "videos/gandalf_intro.mp4", } MOONLIGHT_API = "http://drive-in:8547" def on_tag_insert(tag): video = VIDEO_MAPPING.get(tag.uid_hex) if video: requests.post(f"{MOONLIGHT_API}/api/play", json={"path": video}) def on_tag_remove(tag): requests.post(f"{MOONLIGHT_API}/api/stop") reader = LegoDimensionsReader() reader.on_tag_insert = on_tag_insert reader.on_tag_remove = on_tag_remove reader.start() ``` ## TEA Encryption Functions For advanced tag reading/writing: ```python from lego_dimensions_reader import ( generate_password, generate_tea_key, decrypt_character_id, encrypt_character_id ) uid = bytes.fromhex("04A1B2C3D4E5F6") # Generate tag password password = generate_password(uid) # Decrypt character ID from tag data encrypted_data = bytes.fromhex("...") # 8 bytes from pages 0x24-0x25 character_id = decrypt_character_id(uid, encrypted_data) # Encrypt character ID for writing encrypted = encrypt_character_id(uid, character_id=1) # Batman ``` ## Troubleshooting ### "LEGO Dimensions Portal not found" - Ensure portal is connected via USB - Check you're using PS3/PS4/Wii U portal (NOT Xbox) - Verify Zadig driver installation ### "Access denied" error - Run your script as Administrator - Re-run Zadig driver installation ### Portal detected but no tag events - Try unplugging and reconnecting portal - Check USB cable connection - Ensure tag is placed flat on the pad ## Technical Details ### NFC Tags - Tag type: NTAG213 (Mifare Ultralight C) - Memory: 144 bytes (45 pages × 4 bytes) - Character data: Pages 0x24-0x25 (TEA encrypted) - Vehicle data: Page 0x24 (unencrypted) - Password protection: Pages 0x24+ ### USB Protocol - Packet size: 32 bytes (fixed) - Endpoint OUT: 0x01 - Endpoint IN: 0x81 - Tag events start with byte 0x56 ## Credits Based on reverse-engineering work by: - ags131 (node-ld) - bettse - socram8888 - Ellerbach (LegoDimensions .NET library) ## License MIT License