forked from woodenphone/lego_dimensions_protocol
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathlego_dimensions_gateway.py
401 lines (350 loc) · 13.5 KB
/
lego_dimensions_gateway.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
#-------------------------------------------------------------------------------
# Name: library
# Purpose: Python library to control Lego Dimensions gateway/portal peripheral
# Xbox version is unsupported due to likely harware differences.
# Author: User
#
# Created: 21/11/2015
# Copyright: (c) User 2015
# Licence: <your licence>
#-------------------------------------------------------------------------------
# Command to function mapping:
# EP Cmd - func_name() - Description
# 01 0xc0 - switch_pad() - Immediately switch one or all pad(s) to a single value
# 01 0xc2 - fade_pad() - Immediately change the colour of one or all pad(s), fade and flash available
# 01 0xc3 - flash_pad() - set 1 or all pad(s) to a colour with variable flash rates
# 01 0xc8 - switch_pads() - Immediately switch pad(s) to set of colours
# 01 0xc6 - fade_pads() - Fade pad(s) to value(s)
# 01 0xc7 - flash_pads - Flash all 3 pads with individual colours and rates, either change to new or return to old based on pulse count
import time
class Gateway():
"""
Represents a Lego Dimensions gateway/portal peripheral
"""
def __init__(self,verbose=True):
self.verbose = verbose
# Initialise USB connection to the device
self.dev = self._init_usb()
# Reset the state of the device to all pads off
self.blank_pads()
return
def _init_usb(self):
"""
Connect to and initialise the portal
"""
import usb.core
import usb.util
# find our device
dev = usb.core.find(idVendor=0x0e6f)# 0x0e6f Logic3 (made lego dimensions portal hardware)
# was it found?
if dev is None:
raise ValueError('Device not found')
self.reattach = False
if dev.is_kernel_driver_active(0):
self.reattach = True
dev.detach_kernel_driver(0)
# set the active configuration. With no arguments, the first
# configuration will be the active one
dev.set_configuration()
# Initialise portal
if self.verbose:
print "Initialising portal"
dev.write(1, [0x55, 0x0f, 0xb0, 0x01, 0x28, 0x63, 0x29, 0x20, 0x4c, 0x45, 0x47, 0x4f, 0x20, 0x32, 0x30, 0x31, 0x34, 0xf7, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])# Startup
return dev
def generate_checksum_for_command(self,command):
"""
Given a command (without checksum or trailing zeroes),
generate a checksum for it.
"""
assert(len(command) <= 31)# One byte must be left for the checksum
# Add bytes, overflowing at 256
result = 0
for word in command:
result = result + word
if result >= 256:
result -= 256
return result
def __del__(self):
"""
Reattach the device when we are done with the object
"""
if self.reattach:
self.dev.attach_kernel_driver(0)
def pad_message(self,message):
"""Pad a message to 32 bytes"""
assert(len(message) <= 32)# Messages cannot be longer than 32 bytes
while(len(message) < 32):
message.append(0x00)
return message
def convert_command_to_packet(self,command):
"""Take a command and add a checksum and padding"""
assert(len(command) <= 31)# One byte must be left for the checksum
checksum = self.generate_checksum_for_command(command)
message = command+[checksum]
packet = self.pad_message(message)
return packet
def send_command(self,command):
"""Take the command, add checksum and padding, then send it"""
assert(len(command) <= 31)# One byte must be left for the checksum
packet = self.convert_command_to_packet(command)
if self.verbose:
print("packet:"+repr(packet))
self.dev.write(1, packet)
def blank_pads(self):
"""
Clear the pads to all off.
"""
self.switch_pad(
pad = 0, # All pads
colour=(0,0,0)# RGB
)
return
def switch_pad(self, pad, colour):
"""
Change the colour of one or all pad(s) immediately
Pad numbering: 0:All, 1:Center, 2:Left, 3:Right
Colour values are 0-255, with 0 being off and 255 being maximum
Colour should be a tuple of 0-255 values in the format (red, green,blue)
Abstraction for command: 0xc0
"""
red, green, blue = colour[0], colour[1], colour[2]
command = [0x55, 0x06, 0xc0, 0x02, pad, red, green, blue,]
self.send_command(command)
return
def flash_pad(self, pad, on_length, off_length, pulse_count, colour):
"""
Flash one or all pad(s) a given colour
The pad(s) will either revert to old colour or stay on the new one depending on the pulse_count value
Odd: keep new colour, Even: keep previous colour. Exception: 0x00 keeps new colour.
Pulse counts from 0xff will flash forever.
Pad numbering: 0:All, 1:Center, 2:Left, 3:Right
Colour values are 0-255, with 0 being off and 255 being maximum
Colour should be a tuple of 0-255 values in the format (red, green,blue)
Abstraction for command: 0xc3
"""
red, green, blue = colour[0], colour[1], colour[2]
command = [0x55, 0x09, 0xc3, 0x1f, pad, on_length, off_length, pulse_count, red, green, blue]
self.send_command(command)
return
def fade_pad(self, pad, pulse_time, pulse_count, colour):
"""
Fade one or all pad(s) a given colour with optional pulsing effect
The pad(s) will either revert to old colour or stay on the new one depending on the pulse_count value
Odd: keep new colour, Even: keep previous colour. Exception: 0x00 keeps new colour.
pulse_count values of 0x00 and above 0x199 will flash forever.
pulse_time starts fast at 0x01 and continues to 0xff which is very slow, 0x00 causes immediate change.
Pad numbering: 0:All, 1:Center, 2:Left, 3:Right
Colour values are 0-255, with 0 being off and 255 being maximum
Colour should be a tuple of 0-255 values in the format (red, green,blue)
Abstraction for command: 0xc2
"""
red, green, blue = colour[0], colour[1], colour[2]
command = [0x55, 0x08, 0xc2, 0x0f, pad, pulse_time, pulse_count, red, green, blue]
self.send_command(command)
return
def switch_pads(self, *colours):
"""
Requires 3 tuples:
(Center),(Left),(Right)
Each using the format:
(R, G, B)
Empty colour tuples will ignore that pad.
Ignored pads will continue whatever they were doing previously.
Abstraction for command: 0xc8
"""
assert(len(colours) == 3)
command = [0x55, 0x0e, 0xc8, 0x06,]# Start of command
for colour in colours:
if len(colour) != 3:
# Disable command for this pad
enable = 0
red, green, blue = 0, 0, 0
else:
# Send colour values for this pad
enable = 1
red, green, blue = colour[0], colour[1], colour[2]
command += [enable, red, green, blue]# 3 identical segments, one for each colour
self.send_command(command)
return
def fade_pads(self, *pads):# TODO get second opinion on arguments
"""
Fade pad(s) to value(s)
Each pad is represented by a tuple in the format:
(fade_time, pulse_count, (R,G,B) )
Colour values must be from 0-255 (0x00-0xff)
Empty colour tuples will ignore that pad.
TODO investigate time values
TODO investigate count values
Abstraction for command: 0xc6
"""
assert(len(pads) == 3)
command = [0x55, 0x14, 0xc6, 0x26,]
for pad in pads:
if len(pad) != 3:
# Disable command for this pad
enable = 0
fade_time = 0
pulse_count = 0
red, green, blue = 0, 0, 0
elif len(pad[2]) != 3:
# Disable command for this pad
enable = 0
fade_time = 0
pulse_count = 0
red, green, blue = 0, 0, 0
else:
# Enable pad for the command
enable = 1
colour = pad[2]
red, green, blue = colour[0], colour[1], colour[2]
fade_time = pad[0]
pulse_count = pad[1]
command += [enable, fade_time, pulse_count, red, green, blue]
continue
self.send_command(command)
return
def flash_pads(self, *pads):# TODO get second opinion on arguments
"""
Flash all 3 pads with individual colours and rates, either change to new or return to old based on pulse count.
Each pad is represented by a tuple in the format:
(on_length, off_length, pulse_count, (R,G,B) )
Colour values must be from 0-255 (0x00-0xff)
Empty colour tuples will ignore that pad.
Ignored pads will continue whatever they were doing previously.
Empty colour tuples will ignore that pad.
Ignored pads will continue whatever they were doing previously.
On pulse length - 0x00 is almost impersceptible, 0xff is ~10 seconds
Off pulse length - 0x00 is almost impersceptible, 0xff is ~10 seconds
Number of flashes - odd value leaves pad in new colour, even leaves pad in old, except for 0x00, which changes to new. Values above 0xc6 dont stop.
Abstraction for command: 0xc7
"""
assert(len(pads) == 3)
command = [0x55, 0x17, 0xc7, 0x3e,]
for pad in pads:
if len(pad) != 4:
# Disable command for this pad
enable = 0
on_length = 0
off_length = 0
pulse_count = 0
red, green, blue = 0, 0, 0
elif len(pad[3]) != 3:
# Disable command for this pad
enable = 0
on_length = 0
off_length = 0
pulse_count = 0
red, green, blue = 0, 0, 0
else:
# Enable pad for the command
enable = 1
colour = pad[3]
red, green, blue = colour[0], colour[1], colour[2]
on_length = pad[0]
off_length = pad[1]
pulse_count = pad[2]
command += [enable, on_length, off_length, pulse_count, red, green, blue]
continue
self.send_command(command)
return
def demo_switch_pads_skip(gateway):
"""
Show how the previous effect on a pad is preverved with gateway.switch_pads()
"""
print("Demonstrating ignore pad functionality in gateway.switch_pads()")
# Test flash_pad()
gateway.flash_pad(
pad = 0,
on_length = 10,
off_length = 20,
pulse_count = 100,
colour = (255,0,0)# RGB
)
time.sleep(2)
# test switch_pads()
gateway.switch_pads(
(255,0,0),# C:RGB
(0,255,0),# L:RGB
(),# R:skip
)
return
def test_flash_pads(gateway):
# test flash_pads()
gateway.flash_pads(# 3 changing pads
(5, 10, 15, (255,0,0)),# (on_length, off_length, pulse_count, (R,G,B) )
(20, 25, 30, (0,255,0)),# (on_length, off_length, pulse_count, (R,G,B) )
(35, 40, 45, (0,0,255)),# (on_length, off_length, pulse_count, (R,G,B) )
)
pause_between_tests(gateway)
gateway.flash_pads(# Two ignored pads
(5, 10, 15, ()),# On, off, count, (R,G,B)
(),# On, off, count, (R,G,B)
(5, 40, 10, (255,0,255)),# On, off, count, (R,G,B)
)
return
def test_fade_pads(gateway):
# test fade_pads()
gateway.fade_pads(# 3 changing pads
(10, 20, (255, 0, 0)),# (fade_time, pulse_count, (R,G,B) )
(20, 10, (0, 255, 0)),# (fade_time, pulse_count, (R,G,B) )
(15, 15, (0, 0, 255)),# (fade_time, pulse_count, (R,G,B) )
)
pause_between_tests(gateway)
gateway.fade_pads(# Two ignored pads
(),# (fade_time, pulse_count, (R,G,B) )
(20, 10, ()),# (fade_time, pulse_count, (R,G,B) )
(15, 15, (0, 0, 255)),# (fade_time, pulse_count, (R,G,B) )
)
return
def pause_between_tests(gateway):
time.sleep(10)
gateway.blank_pads()
time.sleep(1)
def debug():
"""
For testing and debugging and coding and stuff
"""
# Get gateway object
gateway = Gateway(verbose=True)
# Test functions for library
#test_flash_pads(gateway)
#pause_between_tests(gateway)
test_fade_pads(gateway)
return
# Test switch_pad()
gateway.switch_pad(
pad=0,
colour = (0, 255, 0)# RGB
)
pause_between_tests(gateway)
# Test flash_pad()
gateway.flash_pad(
pad = 0,
on_length = 10,
off_length = 20,
pulse_count = 100,
colour = (255,0,0)# RGB
)
pause_between_tests(gateway)
# Test fade_pad()
gateway.fade_pad(
pad = 1,
pulse_time = 10,
pulse_count = 10,
colour = (255, 0, 255)# RGB
)
pause_between_tests(gateway)
# test switch_pads()
gateway.switch_pads(
(255,0,0),# C:RGB
(0,255,0),# L:RGB
(),# R:skip
)
pause_between_tests(gateway)
demo_switch_pads_skip(gateway)
return
def main():
debug()
pass
if __name__ == '__main__':
main()