247 lines
6.1 KiB
Markdown
247 lines
6.1 KiB
Markdown
# 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
|