diff --git a/README.md b/README.md index 8cf2958..8bb8f8d 100644 --- a/README.md +++ b/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 \ No newline at end of file +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