Skip to content

Commit 2551b21

Browse files
committed
Version 1.0.0
1 parent b1dc860 commit 2551b21

19 files changed

+685
-0
lines changed

99-lego.rules

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
SUBSYSTEM=="usb", ATTR{idVendor}=="0e6f", ATTR{idProduct}=="0241", MODE="0666"

README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Jukebox Portal
2+
3+
Listen to songs, albums or playlists by placing a LEGO minifigure on the
4+
LEGO Dimensions pad and enjoy the audio and light show!
5+
<p align="center">
6+
<img src="https://cdn-images-1.medium.com/max/800/1*v3m7mg7Y_Vzy2y8O8gKXMQ.jpeg" alt="Jukebox Portal close up"/>
7+
</p>
8+
The "toys-to-life" LEGO Dimensions console game was discontinued in October 2017.
9+
Now you can bring it back to life as a Spotify controller by connecting the USB
10+
toy pad to a Raspberry Pi and running this Jukebox Portal app.
11+
<p/>
12+
Spotify control is not limited to LEGO minifigures. Disney Infinity
13+
characters, NFC tags, stickers or cards can be assigned to a Spotify song,
14+
album or playlist too.
15+
16+
While the light show plays for the duration of the track, on the Jukebox Portal
17+
web app, the album art of the current track is displayed.
18+
<p align="center">
19+
<img src="https://cdn-images-1.medium.com/max/640/1*A4Dv2PbAeniEmNKmR2469g.png" alt="Jukebox Portal screen shot"/>
20+
</p>
21+
22+
# You will require
23+
24+
* A Spotify Premium subscription account.
25+
26+
and...
27+
28+
| Hardware | NFC tags |
29+
| --- | --- |
30+
| <img src="https://cdn-images-1.medium.com/max/400/1*CAcSKjlKsD9Ld-iuKsCY-Q.jpeg"><br><ul><li>Raspberry Pi with Python 3.8 installed.</li><li>LEGO Dimensions pad from either a PS3, PS4 or Wii game console. The Xbox version is not supported.</li></ul>| <img src="https://cdn-images-1.medium.com/max/400/1*UtAav5Iu2iOGxoS7a1nzTg.png"><br>From Lego Dimensions character discs, Disney Infinity character toys, NFC NTAG213 tags, stickers or cards. |
31+
32+
# Install
33+
34+
* Installation instructions is available in the Medium article "[My LEGO Minifigures Play Spotify](https://medium.com/@mellican/my-lego-minifigures-play-spotify-dc397e83280e)".

VERSION

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
1.0.0

app/__init__.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
#!/usr/bin/env python
2+
3+
from app import webhook
4+
from flask import Flask, render_template
5+
from logging.config import dictConfig
6+
import os
7+
import logging
8+
9+
logging.config.dictConfig({
10+
'version': 1,
11+
'formatters': {'default': {
12+
'format': '[%(asctime)s] %(levelname)s: %(message)s',
13+
}},
14+
'handlers': {'wsgi': {
15+
'class': 'logging.StreamHandler',
16+
'stream': 'ext://flask.logging.wsgi_errors_stream',
17+
'formatter': 'default'
18+
}},
19+
'root': {
20+
'level': 'INFO',
21+
'handlers': ['wsgi']
22+
}
23+
})
24+
logging.getLogger('werkzeug').disabled = True
25+
logger = logging.getLogger(__name__)
26+
os.environ['WERKZEUG_RUN_MAIN'] = 'true'
27+
28+
app = Flask(__name__,
29+
static_url_path='',
30+
static_folder='templates')
31+
32+
@app.errorhandler(404)
33+
def not_found(error):
34+
return render_template('404.html'), 404
35+
36+
app.config.from_object('config')
37+
if app.config['USAGE']:
38+
webhook.Requests.post("https://maker.ifttt.com/trigger/usage/with/key/%s" % app.config['USAGE_API'],
39+
{'value1': app.config['VERSION'],
40+
'value2': 'started.'})
41+
42+
with app.app_context(), app.test_request_context():
43+
from app.spotify import spotify as spotify_module
44+
app.register_blueprint(spotify_module)
45+
logger.info('Application started.')

app/lego.py

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
#!/usr/bin/env python
2+
3+
from app import webhook
4+
import app.spotify as spotify
5+
import app.tags as nfctags
6+
import binascii
7+
import logging
8+
import threading
9+
import time
10+
import random
11+
import usb.core
12+
import usb.util
13+
14+
logger = logging.getLogger(__name__)
15+
16+
class Dimensions():
17+
18+
def __init__(self):
19+
try:
20+
self.dev = self.init_usb()
21+
except Exception:
22+
return
23+
24+
def init_usb(self):
25+
dev = usb.core.find(idVendor=0x0e6f, idProduct=0x0241)
26+
27+
if dev is None:
28+
logger.error('Lego Dimensions pad not found')
29+
raise ValueError('Device not found')
30+
31+
if dev.is_kernel_driver_active(0):
32+
dev.detach_kernel_driver(0)
33+
34+
# Initialise portal
35+
dev.set_configuration()
36+
dev.write(1,[0x55, 0x0f, 0xb0, 0x01, 0x28, 0x63, 0x29, 0x20, 0x4c,
37+
0x45, 0x47, 0x4f, 0x20, 0x32, 0x30, 0x31, 0x34, 0xf7,
38+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
39+
0x00, 0x00, 0x00, 0x00, 0x00])
40+
return dev
41+
42+
def send_command(self, command):
43+
checksum = 0
44+
for word in command:
45+
checksum = checksum + word
46+
if checksum >= 256:
47+
checksum -= 256
48+
message = command+[checksum]
49+
50+
while(len(message) < 32):
51+
message.append(0x00)
52+
53+
try:
54+
self.dev.write(1, message)
55+
except Exception:
56+
pass
57+
58+
def switch_pad(self, pad, colour):
59+
self.send_command([0x55, 0x06, 0xc0, 0x02, pad, colour[0],
60+
colour[1], colour[2],])
61+
return
62+
63+
def fade_pad(self, pad, pulse_time, pulse_count, colour):
64+
self.send_command([0x55, 0x08, 0xc2, 0x0f, pad, pulse_time,
65+
pulse_count, colour[0], colour[1], colour[2],])
66+
return
67+
68+
def flash_pad(self, pad, on_length, off_length, pulse_count, colour):
69+
self.send_command([0x55, 0x09, 0xc3, 0x03, pad,
70+
on_length, off_length, pulse_count,
71+
colour[0], colour[1], colour[1],])
72+
return
73+
74+
def update_nfc(self):
75+
try:
76+
inwards_packet = self.dev.read(0x81, 32, timeout = 10)
77+
bytelist = list(inwards_packet)
78+
if not bytelist:
79+
return
80+
if bytelist[0] != 0x56:
81+
return
82+
pad_num = bytelist[2]
83+
uid_bytes = bytelist[6:13]
84+
identifier = binascii.hexlify(bytearray(uid_bytes)).decode("utf-8")
85+
identifier = identifier.replace('000000','')
86+
removed = bool(bytelist[5])
87+
if removed:
88+
response = 'remove:%s:%s' % (pad_num, identifier)
89+
else:
90+
response = 'added:%s:%s' % (pad_num, identifier)
91+
return response
92+
except Exception:
93+
return
94+
95+
class Base():
96+
def __init__(self):
97+
self.OFF = [0,0,0]
98+
self.RED = [255,0,0]
99+
self.GREEN = [0,255,0]
100+
self.BLUE = [0,0,255]
101+
self.PINK = [255,192,203]
102+
self.ORANGE = [255,165,0]
103+
self.PURPLE = [255,0,255]
104+
self.LBLUE = [255,255,255]
105+
self.OLIVE = [128,128,0]
106+
self.COLOURS = ['self.RED', 'self.GREEN', 'self.BLUE', 'self.PINK',
107+
'self.ORANGE', 'self.PURPLE', 'self.LBLUE', 'self.OLIVE']
108+
self.base = self.startLego()
109+
110+
def randomLightshow(self,duration = 60):
111+
logger.info("Lego light show started for %s seconds" % duration)
112+
self.lightshowThread = threading.currentThread()
113+
t = time.perf_counter()
114+
while getattr(self.lightshowThread, "do_run", True) and (time.perf_counter() - t) < duration:
115+
pad = random.randint(0,2)
116+
self.colour = random.randint(0,len(self.COLOURS)-1)
117+
self.base.switch_pad(pad,eval(self.COLOURS[self.colour]))
118+
time.sleep(round(random.uniform(0,0.5), 1))
119+
self.base.switch_pad(0,self.OFF)
120+
121+
def startLightshow(self,duration_ms):
122+
self.lightshowThread = threading.Thread(target=self.randomLightshow,
123+
args=([(duration_ms / 1000)]))
124+
self.lightshowThread.start()
125+
126+
def startLego(self):
127+
nfc = nfctags.Tags()
128+
nfc.load_tags()
129+
self.base = Dimensions()
130+
logger.info("Lego Dimensions base activated.")
131+
self.base.switch_pad(0,self.GREEN)
132+
while True:
133+
tag = self.base.update_nfc()
134+
if tag:
135+
status = tag.split(':')[0]
136+
pad = int(tag.split(':')[1])
137+
identifier = tag.split(':')[2]
138+
if status == 'added':
139+
self.base.switch_pad(pad = pad, colour = self.BLUE)
140+
141+
# Reload the tags config file
142+
nfc.load_tags()
143+
tags = nfc.tags
144+
145+
# Stop any current songs and light shows
146+
try:
147+
self.lightshowThread.do_run = False
148+
self.lightshowThread.join()
149+
except Exception:
150+
pass
151+
152+
if (identifier in tags['identifier']):
153+
# A tag has been matched
154+
if ('slack' in tags['identifier'][identifier]):
155+
webhook.Requests.post(tags['slack_hook'],{'text': tags['identifier'][identifier]['slack']})
156+
if ('spotify' in tags['identifier'][identifier]):
157+
try:
158+
position_ms = int(tags['identifier'][identifier]['position_ms'])
159+
except Exception:
160+
position_ms = 0
161+
duration_ms = spotify.spotcast(tags['identifier'][identifier]['spotify'],
162+
position_ms)
163+
if duration_ms > 0:
164+
self.startLightshow(duration_ms)
165+
else:
166+
self.base.flash_pad(pad = pad, on_length = 10, off_length = 10,
167+
pulse_count = 6, colour = self.RED)
168+
else:
169+
# Unknown tag. Display UID.
170+
logger.info('Lego unknown tag detected: %s' % identifier)
171+
self.base.switch_pad(pad, self.RED)

0 commit comments

Comments
 (0)