Skip to content

Commit

Permalink
Dec 30 daily effect, and multicontrol, and show_text
Browse files Browse the repository at this point in the history
  • Loading branch information
Anders-Holst committed Dec 29, 2021
1 parent c1736c5 commit 1f66efd
Show file tree
Hide file tree
Showing 4 changed files with 248 additions and 1 deletion.
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
__version__ = '0.1.30'
__version__ = '0.1.31'

_classifiers = [
'Development Status :: 4 - Beta',
Expand Down
196 changes: 196 additions & 0 deletions xled_plus/multicontrol.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
# -*- coding: utf-8 -*-

"""
xled_plus.multicontrol
~~~~~~~~~~~~~~~~~~~~~~
Handle multiple connected lights as one unit.
Create a MultiHighControlInterface by giving it a list of ip-addresses to
the devices that are connected, with the master device first. Then, use it
in the same way as a normal HighControlInterface.
"""

from __future__ import absolute_import

import io
import struct
import binascii
import time
import uuid

import struct
import base64

from xled.control import ControlInterface
from xled_plus.highcontrol import HighControlInterface


class MultiHighControlInterface(HighControlInterface):
"""
High level interface to control specific device
"""

def __init__(self, hostlst):
super(MultiHighControlInterface, self).__init__(hostlst[0])
self.ctrlst = [self] + [ControlInterface(ip) for ip in hostlst[1:]]
info = self.get_device_info()
self.family = info["fw_family"] if "fw_family" in info else "D"
self.led_bytes = info["bytes_per_led"] if "bytes_per_led" in info else 3
self.led_profile = info["led_profile"] if "led_profile" in info else "RGB"
self.version = tuple(map(int, self.firmware_version()["version"].split(".")))
self.nledslst = [ctr.get_device_info()["number_of_led"] for ctr in self.ctrlst]
for ctr in self.ctrlst:
ctr._udpclient = self.udpclient
self.num_leds = sum(self.nledslst)
self.string_config = [{'first_led_id': 0, 'length': self.num_leds}]
self.layout = False
self.layout_bounds = False
self.last_mode = None
self.last_rt_time = 0
self.curr_mode = self.get_mode()["mode"]

def split_movie(self, movie):
# return a list of one movie per connected device
lst = [io.BytesIO() for ctr in self.ctrlst]
blens = [nleds * self.led_bytes for nleds in self.nledslst]
totlen = self.num_leds * self.led_bytes
num = movie.seek(0, 2) // totlen
movie.seek(0)
for i in range(num):
for mov, blen in zip(lst, blens):
mov.write(movie.read(blen))
for mov in lst:
mov.seek(0)
return lst

def show_movie(self, movie_or_id, fps=None):
"""
Either starts playing an already uploaded movie with the provided id,
or uploads a new movie and starts playing it at the provided frames-per-second.
Note: if the movie do not fit in the remaining capacity, the old movie list is cleared.
Switches to movie mode if necessary.
The movie is an object suitable created with to_movie or make_func_movie.
:param movie_or_id: either an integer id or a file-like object that points to movie
:param fps: frames per second, or None if a movie id is given
"""
if isinstance(movie_or_id, int) and fps is None:
if self.family == "D" or self.version < (2, 5, 6):
if movie_or_id != 0:
return False
else:
movies = self.get_movies()["movies"]
if movie_or_id in [entry["id"] for entry in movies]:
self.set_movies_current(movie_or_id)
else:
return False
if self.curr_mode != "movie":
self.set_mode("movie")
else:
assert fps
self.set_mode("off")
self.upload_movie(movie_or_id, fps, force=True)
self.set_mode("movie")
return True

def upload_movie(self, movie, fps, force=False):
"""
Uploads a new movie with the provided frames-per-second.
Note: if the movie does not fit in the remaining capacity, and force is
not set to True, the function just returns False, in which case the user
can try clear_movies first.
Does not switch to movie mode, use show_movie instead for that.
The movie is an object suitable created with to_movie or make_func_movie.
Returns the new movie id, which can be used in calls to show_movie or
show_playlist.
:param movie: a file-like object that points to movie
:param fps: frames per second, or None if a movie id is given
:param bool force: if remaining capacity is too low, previous movies will be removed
:rtype: int
"""
numframes = movie.seek(0, 2) // (self.led_bytes * self.num_leds)
movielst = self.split_movie(movie)
if self.family == "D" or self.version < (2, 5, 6):
for ctr, mov, nled in zip(self.ctrlst, movielst, self.nledslst):
ctr.set_led_movie_config(1000 // fps, numframes, nled)
ctr.set_led_movie_full(mov)
return 0
else:
res = self.get_movies()
capacity = res["available_frames"] - 1
if numframes > capacity or len(res["movies"]) > 15:
if force:
if self.curr_mode == "movie" or self.curr_mode == "playlist":
self.set_mode("effect")
self.delete_movies()
else:
return False
if self.curr_mode == "movie":
oldid = self.get_movies_current()["id"]
for ctr, mov, nled in zip(self.ctrlst, movielst, self.nledslst):
res = ctr.set_movies_new(
"",
str(uuid.uuid4()),
self.led_profile.lower() + "_raw",
nled,
numframes,
fps,
)
ctr.set_movies_full(mov)
if self.curr_mode == "movie":
self.set_movies_current(oldid) # Dont change currently shown movie
return res["id"]

def show_rt_frame(self, frame):
"""
Uploads a frame as the next real time frame, and shows it.
Switches to rt mode if necessary.
The frame is either a pattern or a one-frame movie
:param frame: a pattern or file-like object representing the frame
"""
if self.is_pattern(frame):
frame = self.to_movie(frame)
framelst = self.split_movie(frame)
if self.curr_mode != "rt" or self.last_rt_time + 50.0 < time.time():
for ctr in self.ctrlst:
ctr.set_mode("rt")
else:
self.last_rt_time = time.time()
for ctr, mov, nled in zip(self.ctrlst, framelst, self.nledslst):
self.udpclient.destination_host = ctr.host
if self.family == "D":
ctr.set_rt_frame_socket(mov, 1, nled)
elif self.version < (2, 4, 14):
ctr.set_rt_frame_socket(mov, 2)
else:
ctr.set_rt_frame_socket(mov, 3)
self.udpclient.destination_host = self.host

def clear_movies(self):
"""
Removes all uploaded movies and any playlist.
If the current mode is 'movie' or 'playlist' it switches mode to 'effect'
"""
if self.curr_mode == "movie" or self.curr_mode == "playlist":
self.set_mode("effect")
if self.family == "D" or self.version < (2, 5, 6):
# No list of movies to remove in this version,
# but disable movie mode until new movie is uploaded
for i, ctr in enumerate(self.ctrlst):
ctr.set_led_movie_config(1000, 0, self.nledslst[i])
else:
# The playlist is removed automatically when movies are removed
for ctr in self.ctrlst:
ctr.delete_movies()

def get_led_layout(self):
res = super(MultiHighControlInterface, self).get_led_layout()
for ctr in self.ctrlst[1:]:
tmp = ctr.get_led_layout()
res["coordinates"].extend(tmp["coordinates"])
return res


20 changes: 20 additions & 0 deletions xled_plus/samples/day30.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
"""
Day 30 - Random walk in colorspace with random gradient direction
Another variant of the Day 25 effect. Here the colors form a gradient that
moves over the leds at a slowly and randomly changing angle and change rate.
This is again a continuous effect. Aspect ratio is important as usual, so adjust below.
However, dimensionality or density of layout should matter less.
"""

from xled_plus.samples.sample_setup import *

ctr = setup_control()
ctr.adjust_layout_aspect(1.0) # How many times wider than high is the led installation?
eff = MeanderingSequence(ctr)
oldmode = ctr.get_mode()["mode"]
eff.launch_rt()
print("Started continuous effect - press Return to stop it")
input()
eff.stop_rt()
ctr.set_mode(oldmode)
31 changes: 31 additions & 0 deletions xled_plus/show_text.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from xled.discover import discover
from xled_plus.highcontrol import HighControlInterface
from xled_plus.ledcolor import *
from xled_plus.shapes import *
from sys import argv
import re

def isipaddress(txt):
return re.match("([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+)", txt)

def makecolorlist(n):
gs = (1.25**0.5 - 0.5)
return [hsl_color((0.5 + gs * i) % 1.0, 1.0, 0.0) for i in range(n)]

if len(argv) == 3 and isipaddress(argv[1]):
host = argv[1]
txt = argv[2]
elif len(argv) == 3 and isipaddress(argv[2]):
host = argv[2]
txt = argv[1]
elif len(argv) == 2:
txt = argv[1]
host = discover().ip_address
else:
print('Usage: python -m xled_plus.show_text [ip-address] "text"')
quit()

ctr = HighControlInterface(host)
ctr.adjust_layout_aspect(1.0)
eff = RunningText(ctr, txt.upper(), makecolorlist(len(txt)), size=0.6, speed=0.5)
eff.launch_movie()

0 comments on commit 1f66efd

Please sign in to comment.