Update README with comprehensive documentation
This commit is contained in:
247
README.md
247
README.md
@@ -1,3 +1,246 @@
|
|||||||
# lego-dimensions-reader
|
# LEGO Dimensions NFC Portal Reader
|
||||||
|
|
||||||
LEGO Dimensions NFC Portal reader module for Python on Windows - integrates with Moonlight Drive-In video player system
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user