Skip to content

Commit 1f66efd

Browse files
committed
Dec 30 daily effect, and multicontrol, and show_text
1 parent c1736c5 commit 1f66efd

File tree

4 files changed

+248
-1
lines changed

4 files changed

+248
-1
lines changed

setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
__version__ = '0.1.30'
1+
__version__ = '0.1.31'
22

33
_classifiers = [
44
'Development Status :: 4 - Beta',

xled_plus/multicontrol.py

+196
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
# -*- coding: utf-8 -*-
2+
3+
"""
4+
xled_plus.multicontrol
5+
~~~~~~~~~~~~~~~~~~~~~~
6+
7+
Handle multiple connected lights as one unit.
8+
Create a MultiHighControlInterface by giving it a list of ip-addresses to
9+
the devices that are connected, with the master device first. Then, use it
10+
in the same way as a normal HighControlInterface.
11+
12+
"""
13+
14+
from __future__ import absolute_import
15+
16+
import io
17+
import struct
18+
import binascii
19+
import time
20+
import uuid
21+
22+
import struct
23+
import base64
24+
25+
from xled.control import ControlInterface
26+
from xled_plus.highcontrol import HighControlInterface
27+
28+
29+
class MultiHighControlInterface(HighControlInterface):
30+
"""
31+
High level interface to control specific device
32+
"""
33+
34+
def __init__(self, hostlst):
35+
super(MultiHighControlInterface, self).__init__(hostlst[0])
36+
self.ctrlst = [self] + [ControlInterface(ip) for ip in hostlst[1:]]
37+
info = self.get_device_info()
38+
self.family = info["fw_family"] if "fw_family" in info else "D"
39+
self.led_bytes = info["bytes_per_led"] if "bytes_per_led" in info else 3
40+
self.led_profile = info["led_profile"] if "led_profile" in info else "RGB"
41+
self.version = tuple(map(int, self.firmware_version()["version"].split(".")))
42+
self.nledslst = [ctr.get_device_info()["number_of_led"] for ctr in self.ctrlst]
43+
for ctr in self.ctrlst:
44+
ctr._udpclient = self.udpclient
45+
self.num_leds = sum(self.nledslst)
46+
self.string_config = [{'first_led_id': 0, 'length': self.num_leds}]
47+
self.layout = False
48+
self.layout_bounds = False
49+
self.last_mode = None
50+
self.last_rt_time = 0
51+
self.curr_mode = self.get_mode()["mode"]
52+
53+
def split_movie(self, movie):
54+
# return a list of one movie per connected device
55+
lst = [io.BytesIO() for ctr in self.ctrlst]
56+
blens = [nleds * self.led_bytes for nleds in self.nledslst]
57+
totlen = self.num_leds * self.led_bytes
58+
num = movie.seek(0, 2) // totlen
59+
movie.seek(0)
60+
for i in range(num):
61+
for mov, blen in zip(lst, blens):
62+
mov.write(movie.read(blen))
63+
for mov in lst:
64+
mov.seek(0)
65+
return lst
66+
67+
def show_movie(self, movie_or_id, fps=None):
68+
"""
69+
Either starts playing an already uploaded movie with the provided id,
70+
or uploads a new movie and starts playing it at the provided frames-per-second.
71+
Note: if the movie do not fit in the remaining capacity, the old movie list is cleared.
72+
Switches to movie mode if necessary.
73+
The movie is an object suitable created with to_movie or make_func_movie.
74+
75+
:param movie_or_id: either an integer id or a file-like object that points to movie
76+
:param fps: frames per second, or None if a movie id is given
77+
"""
78+
if isinstance(movie_or_id, int) and fps is None:
79+
if self.family == "D" or self.version < (2, 5, 6):
80+
if movie_or_id != 0:
81+
return False
82+
else:
83+
movies = self.get_movies()["movies"]
84+
if movie_or_id in [entry["id"] for entry in movies]:
85+
self.set_movies_current(movie_or_id)
86+
else:
87+
return False
88+
if self.curr_mode != "movie":
89+
self.set_mode("movie")
90+
else:
91+
assert fps
92+
self.set_mode("off")
93+
self.upload_movie(movie_or_id, fps, force=True)
94+
self.set_mode("movie")
95+
return True
96+
97+
def upload_movie(self, movie, fps, force=False):
98+
"""
99+
Uploads a new movie with the provided frames-per-second.
100+
Note: if the movie does not fit in the remaining capacity, and force is
101+
not set to True, the function just returns False, in which case the user
102+
can try clear_movies first.
103+
Does not switch to movie mode, use show_movie instead for that.
104+
The movie is an object suitable created with to_movie or make_func_movie.
105+
Returns the new movie id, which can be used in calls to show_movie or
106+
show_playlist.
107+
108+
:param movie: a file-like object that points to movie
109+
:param fps: frames per second, or None if a movie id is given
110+
:param bool force: if remaining capacity is too low, previous movies will be removed
111+
:rtype: int
112+
"""
113+
numframes = movie.seek(0, 2) // (self.led_bytes * self.num_leds)
114+
movielst = self.split_movie(movie)
115+
if self.family == "D" or self.version < (2, 5, 6):
116+
for ctr, mov, nled in zip(self.ctrlst, movielst, self.nledslst):
117+
ctr.set_led_movie_config(1000 // fps, numframes, nled)
118+
ctr.set_led_movie_full(mov)
119+
return 0
120+
else:
121+
res = self.get_movies()
122+
capacity = res["available_frames"] - 1
123+
if numframes > capacity or len(res["movies"]) > 15:
124+
if force:
125+
if self.curr_mode == "movie" or self.curr_mode == "playlist":
126+
self.set_mode("effect")
127+
self.delete_movies()
128+
else:
129+
return False
130+
if self.curr_mode == "movie":
131+
oldid = self.get_movies_current()["id"]
132+
for ctr, mov, nled in zip(self.ctrlst, movielst, self.nledslst):
133+
res = ctr.set_movies_new(
134+
"",
135+
str(uuid.uuid4()),
136+
self.led_profile.lower() + "_raw",
137+
nled,
138+
numframes,
139+
fps,
140+
)
141+
ctr.set_movies_full(mov)
142+
if self.curr_mode == "movie":
143+
self.set_movies_current(oldid) # Dont change currently shown movie
144+
return res["id"]
145+
146+
def show_rt_frame(self, frame):
147+
"""
148+
Uploads a frame as the next real time frame, and shows it.
149+
Switches to rt mode if necessary.
150+
The frame is either a pattern or a one-frame movie
151+
152+
:param frame: a pattern or file-like object representing the frame
153+
"""
154+
if self.is_pattern(frame):
155+
frame = self.to_movie(frame)
156+
framelst = self.split_movie(frame)
157+
if self.curr_mode != "rt" or self.last_rt_time + 50.0 < time.time():
158+
for ctr in self.ctrlst:
159+
ctr.set_mode("rt")
160+
else:
161+
self.last_rt_time = time.time()
162+
for ctr, mov, nled in zip(self.ctrlst, framelst, self.nledslst):
163+
self.udpclient.destination_host = ctr.host
164+
if self.family == "D":
165+
ctr.set_rt_frame_socket(mov, 1, nled)
166+
elif self.version < (2, 4, 14):
167+
ctr.set_rt_frame_socket(mov, 2)
168+
else:
169+
ctr.set_rt_frame_socket(mov, 3)
170+
self.udpclient.destination_host = self.host
171+
172+
def clear_movies(self):
173+
"""
174+
Removes all uploaded movies and any playlist.
175+
If the current mode is 'movie' or 'playlist' it switches mode to 'effect'
176+
"""
177+
if self.curr_mode == "movie" or self.curr_mode == "playlist":
178+
self.set_mode("effect")
179+
if self.family == "D" or self.version < (2, 5, 6):
180+
# No list of movies to remove in this version,
181+
# but disable movie mode until new movie is uploaded
182+
for i, ctr in enumerate(self.ctrlst):
183+
ctr.set_led_movie_config(1000, 0, self.nledslst[i])
184+
else:
185+
# The playlist is removed automatically when movies are removed
186+
for ctr in self.ctrlst:
187+
ctr.delete_movies()
188+
189+
def get_led_layout(self):
190+
res = super(MultiHighControlInterface, self).get_led_layout()
191+
for ctr in self.ctrlst[1:]:
192+
tmp = ctr.get_led_layout()
193+
res["coordinates"].extend(tmp["coordinates"])
194+
return res
195+
196+

xled_plus/samples/day30.py

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
"""
2+
Day 30 - Random walk in colorspace with random gradient direction
3+
4+
Another variant of the Day 25 effect. Here the colors form a gradient that
5+
moves over the leds at a slowly and randomly changing angle and change rate.
6+
This is again a continuous effect. Aspect ratio is important as usual, so adjust below.
7+
However, dimensionality or density of layout should matter less.
8+
"""
9+
10+
from xled_plus.samples.sample_setup import *
11+
12+
ctr = setup_control()
13+
ctr.adjust_layout_aspect(1.0) # How many times wider than high is the led installation?
14+
eff = MeanderingSequence(ctr)
15+
oldmode = ctr.get_mode()["mode"]
16+
eff.launch_rt()
17+
print("Started continuous effect - press Return to stop it")
18+
input()
19+
eff.stop_rt()
20+
ctr.set_mode(oldmode)

xled_plus/show_text.py

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
from xled.discover import discover
2+
from xled_plus.highcontrol import HighControlInterface
3+
from xled_plus.ledcolor import *
4+
from xled_plus.shapes import *
5+
from sys import argv
6+
import re
7+
8+
def isipaddress(txt):
9+
return re.match("([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+)", txt)
10+
11+
def makecolorlist(n):
12+
gs = (1.25**0.5 - 0.5)
13+
return [hsl_color((0.5 + gs * i) % 1.0, 1.0, 0.0) for i in range(n)]
14+
15+
if len(argv) == 3 and isipaddress(argv[1]):
16+
host = argv[1]
17+
txt = argv[2]
18+
elif len(argv) == 3 and isipaddress(argv[2]):
19+
host = argv[2]
20+
txt = argv[1]
21+
elif len(argv) == 2:
22+
txt = argv[1]
23+
host = discover().ip_address
24+
else:
25+
print('Usage: python -m xled_plus.show_text [ip-address] "text"')
26+
quit()
27+
28+
ctr = HighControlInterface(host)
29+
ctr.adjust_layout_aspect(1.0)
30+
eff = RunningText(ctr, txt.upper(), makecolorlist(len(txt)), size=0.6, speed=0.5)
31+
eff.launch_movie()

0 commit comments

Comments
 (0)