From c91b4ad36b07edcded69fe43cd1233cc43aee57e Mon Sep 17 00:00:00 2001 From: Justin Warren Date: Wed, 14 Aug 2013 14:46:35 +1000 Subject: [PATCH 01/50] * Added example, lights the string one globe at a time until all are lit, and then repeats, like the floor evac lights in Phloston Paradise from the movie The Fifth Element. * Added set_pattern() to holiday.py, to set all globes at once by passing in a pattern. --- examples/holiday.py | 8 ++++ examples/phloston.py | 90 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+) create mode 100644 examples/phloston.py diff --git a/examples/holiday.py b/examples/holiday.py index 4e41a3d..988d614 100644 --- a/examples/holiday.py +++ b/examples/holiday.py @@ -68,6 +68,14 @@ def getglobe(self, globenum): return False return (self.globes[globenum][0], self.globes[globenum][1], self.globes[globenum][2]) + def set_pattern(self, pattern): + """ + Set the entire string in one go + """ + if len(pattern) != self.NUM_GLOBES: + raise ValueError("pattern length incorrect: %d != %d" % ( len(pattern), self.NUM_GLOBES) ) + self.globes = pattern[:] + def chase(self, direction="True"): """Rotate all of the globes around - up if TRUE, down if FALSE""" return diff --git a/examples/phloston.py b/examples/phloston.py new file mode 100644 index 0000000..a3780d6 --- /dev/null +++ b/examples/phloston.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python +""" +Phloston Paradise Bomb-in-the-hotel style display for MooresCloud Holiday + +Copyright (c) 2013 Justin Warren +License: MIT (see LICENSE for details) +""" + +__author__ = "Justin Warren" +__version__ = '0.01-dev' +__license__ = "MIT" + +import sys +import time +import logging + +import holiday + +# Simulator default address +SIM_ADDR = "localhost:8080" + +log = logging.getLogger(sys.argv[0]) +handler = logging.StreamHandler() +handler.setFormatter(logging.Formatter("%(asctime)s: %(name)s [%(levelname)s]: %(message)s")) +log.addHandler(handler) +log.setLevel(logging.DEBUG) + +class PhlostonString(object): + """ + A PhlostonString is a Holiday turned into a Phloston Paradise visual alarm light. + + Turns a Holiday into Phloston Paradise hotel evacuation lighting, from the movie + The Fifth Element. + """ + + def __init__(self, addr, + color=(0xff, 0xff, 0xff), + delay=0.02): + """ + Controls a single Holiday at addr + + @param addr: Address of the remote Holiday IoTAS controller + @param color: The color of the lights + @param delay: Time between lighting each globe + """ + self.hol = holiday.Holiday(addr=addr, + remote=True) + self.color = color + self.delay = 0.02 + + self.base_pattern = [ + (0x00, 0x00, 0x00), + ] * self.hol.NUM_GLOBES + + # Make a copy so we don't clobber the original + self.globe_pattern = self.base_pattern[:] + + def animate(self): + # Animation sequence is to start blank, then light each + # globe in sequence until all are lit, then start again. + while True: + for step in range(0, self.hol.NUM_GLOBES): + #log.debug("Step %d of animation", step) + if step == 0: + # Reset to beginning + self.globe_pattern = self.base_pattern[:] + else: + for i in range(0, step): + self.globe_pattern[i] = self.color + pass + pass + #log.debug("Pattern is: %s", self.globe_pattern) + self.hol.set_pattern(self.globe_pattern) + self.hol.render() + time.sleep(0.02) + pass + +if __name__ == '__main__': + if len(sys.argv) > 1: + hostname = sys.argv[1] + log.debug("hostname: %s", hostname) + else: + # Assume we're on the simulator + log.info("Using simulator: %s", SIM_ADDR) + hostname = SIM_ADDR + pass + + ps = PhlostonString(hostname, + color=(0x33, 0x88, 0x33),) + ps.animate() From 3d6968784339143f312a67414e22854377ffd083 Mon Sep 17 00:00:00 2001 From: Justin Warren Date: Wed, 14 Aug 2013 16:23:21 +1000 Subject: [PATCH 02/50] * Added emacs backup file pattern (*~) to .gitignore * Added a simple chaser demo animation --- .gitignore | 2 +- examples/chase_demo.py | 96 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 examples/chase_demo.py diff --git a/.gitignore b/.gitignore index d4a80c8..18415e5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ .DS_Store *.pyc - +*~ diff --git a/examples/chase_demo.py b/examples/chase_demo.py new file mode 100644 index 0000000..3ff6b4a --- /dev/null +++ b/examples/chase_demo.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python +""" +Demonstrate the chase function from holiday.py + +Copyright (c) 2013 Justin Warren +License: MIT (see LICENSE for details) +""" + +__author__ = "Justin Warren" +__version__ = '0.01-dev' +__license__ = "MIT" + +import sys +import time +import logging + +import holiday + +# Simulator default address +SIM_ADDR = "localhost:8080" + +log = logging.getLogger(sys.argv[0]) +handler = logging.StreamHandler() +handler.setFormatter(logging.Formatter("%(asctime)s: %(name)s [%(levelname)s]: %(message)s")) +log.addHandler(handler) +log.setLevel(logging.DEBUG) + +class Chaser(object): + """ + Implements a chaser + """ + + def __init__(self, addr, + pattern=None, + reverse=False, + delay=0.1): + """ + Controls a single Holiday at addr + + @param pattern: The starting pattern to chase around + """ + self.hol = holiday.Holiday(addr=addr, + remote=True) + + if pattern is None: + pattern = [ + (0x00, 0x00, 0x00), + ] * self.hol.NUM_GLOBES + pattern[0] = (0xff, 0xff, 0xff) + pattern[10] = (0xff, 0x00, 0x00) + pattern[20] = (0x00, 0xff, 0x00) + pattern[30] = (0x00, 0x00, 0xff) + pattern[40] = (0xff, 0xff, 0x00) + + # Make a copy so we don't clobber the original + self.globe_pattern = pattern[:] + + self.reverse = reverse + self.delay = delay + + def animate(self): + # Animation sequence is to start blank, then light each + # globe in sequence until all are lit, then start again. + while True: + + # Move lights one step forwards or backwards, + # depending on the setting passed in + if self.reverse: + new_pattern = self.globe_pattern[1:] + new_pattern.append(self.globe_pattern[0]) + pass + + else: + new_pattern = self.globe_pattern[:-1] + new_pattern.insert(0, self.globe_pattern[-1]) + pass + + self.globe_pattern = new_pattern + self.hol.set_pattern(self.globe_pattern) + self.hol.render() + time.sleep(self.delay) + pass + +if __name__ == '__main__': + if len(sys.argv) > 1: + hostname = sys.argv[1] + log.debug("hostname: %s", hostname) + else: + # Assume we're on the simulator + log.info("Using simulator: %s", SIM_ADDR) + hostname = SIM_ADDR + pass + + obj = Chaser(hostname, + reverse=False) + obj.animate() From 99ee8011e7bf0c8c510521cb6b1e109732628a02 Mon Sep 17 00:00:00 2001 From: Justin Warren Date: Tue, 29 Oct 2013 21:31:53 +1100 Subject: [PATCH 03/50] * Added multi-Holiday simulator using PyGame and SecretLabs UDP API * Added holibeats.py, a graphical audio frequency analyser that uses Holidays as frequency band displays like on a graphic equialiser. * Added holiscreen.py, a simple image to Holiday array converter that can also handled animated GIFs (in a simplistic way) * Updated phloston.py to support multiple Holidays using WebAPI or UDP API. --- examples/holibeats.py | 293 +++++++++++++++++++++++++++++++++++++++++ examples/holiscreen.py | 191 +++++++++++++++++++++++++++ examples/phloston.py | 158 ++++++++++++++++++---- simgame/__init__.py | 0 simgame/holiday.py | 97 ++++++++++++++ simgame/simgame.py | 244 ++++++++++++++++++++++++++++++++++ 6 files changed, 954 insertions(+), 29 deletions(-) create mode 100755 examples/holibeats.py create mode 100755 examples/holiscreen.py create mode 100644 simgame/__init__.py create mode 100644 simgame/holiday.py create mode 100755 simgame/simgame.py diff --git a/examples/holibeats.py b/examples/holibeats.py new file mode 100755 index 0000000..205ac1d --- /dev/null +++ b/examples/holibeats.py @@ -0,0 +1,293 @@ +#!/usr/bin/python +# +# A SoundLevel monitor on a Holiday for sound playing on your PC +# +import sys +import pyaudio +import audioop +import numpy +import scipy +import time +#from scipy.signal import kaiserord, lfilter, firwin, freqz + +from scipy import fft + +import optparse + +from secretapi.holidaysecretapi import HolidaySecretAPI + +FORMAT = pyaudio.paInt16 +CHANNELS = 2 +RATE = 44100 +BUFSIZE = 2048 + +def list_devices(): + # List all audio input devices + p = pyaudio.PyAudio() + i = 0 + n = p.get_device_count() + + for i in range(0, n): + dev = p.get_device_info_by_index(i) + if dev['maxInputChannels'] > 0: + print "Found device: %d: %s" % (i, dev['name']) + i += 1 + pass + pass + pass + +def get_default_device_id(): + """ + Find the default audio device we want to monitor + """ + for i in range(0, n): + dev = p.get_device_info_by_index(i) + if dev['maxInputChannels'] > 0: + print "Found device: %d: %s" % (i, dev['name']) + i += 1 + pass + pass + pass + +def calc_samp_amp(sample): + """ + Figure out the sample value to use for our spectrum + analyser output for the given pre-filtered sample + """ + SCALE_FACTOR = 10e4 + #result = numpy.std(sample) * SCALE_FACTOR + result = max(abs(sample)) * SCALE_FACTOR + + return result + +def send_blinken(hol, value, maxval=None): + """ + Create a light pattern for a remote Holiday + based on the value we receive. + + If maxval is supplied, scale value as proportion of + maxval, so the display auto-ranges. + """ + # Normalize value based on maxval == 48 globes + if maxval: + value = value/maxval * 48 + pass + + value = int(value) + + # Clip values greater than 50 + if value > 50: + value = 50 + pass + + for i in range(0, value): + if i < 25: + hol.setglobe(i, 50, 180, 50) + elif i < 40: + hol.setglobe(i, 222, 215, 26) + else: + hol.setglobe(i, 200, 50, 50) + pass + + for i in range(value, 51): + hol.setglobe(i, 0, 0, 0) + pass + + hol.render() + +class HolibeatOptions(optparse.OptionParser): + """ + Command-line options parser + """ + def __init__(self, *args, **kwargs): + optparse.OptionParser.__init__(self, **kwargs) + self.addOptions() + + def addOptions(self): + self.add_option('-n', '--numstrings', dest='numstrings', + help="Number of Holiday strings to simulate [%default]", + type="int", default=1) + + # Listen on multiple TCP/UDP ports, one for each Holiday we simulate + self.add_option('-p', '--portstart', dest='portstart', + help="Port number to start at for UDP listeners [%default]", + type="int", default=9988) + + self.add_option('-m', '--mode', dest='mode', + help="Frequency mode: amplitude or power [%default]", + type="choice", choices=['amp', 'power'], default='amp') + + self.add_option('-f', '--fps', dest='fps', + help="Frames per second, used to slow down data sending", + type="int", default=30) + + def parseOptions(self): + """ + Emulate twistedmatrix options parser API + """ + options, args = self.parse_args() + self.options = options + self.args = args + + self.postOptions() + + return self.options, self.args + + def postOptions(self): + pass + +def chunks(l, n): + """ + Generator for n-sized chunks from list l + """ + for i in xrange(0, len(l), n): + yield l[i:i+n] + pass + +if __name__ == '__main__': + #list_devices() + optparse = HolibeatOptions() + options, args = optparse.parseOptions() + + pa = pyaudio.PyAudio() + + stream = pa.open(format=FORMAT, + channels=CHANNELS, + rate=RATE, + input=True, + frames_per_buffer=BUFSIZE) + + # Sample rate in Hertz + sample_rate = float(RATE) + # The Nyquist rate of the signal + nyq_rate = sample_rate / 2.0 + + # A Hamming window, to help with putting frequencies into + # different buckets. + window = numpy.hamming(BUFSIZE*2) + + hols = [] + for i in range(0, options.numstrings): + hols.append(HolidaySecretAPI(port=options.portstart+i)) + pass + + # Build the list of cutoff frequences as powers of 10 + cutoffs = [] + exp = [2, 3, 4] + for x in exp: + for i in range(1,10): + for sub in [0, 1, 2, 3]: + cutoffs.append( i*(10**x) + sub*(10**x/4) ) + pass + pass + pass + + # ignore anything higher than 10kHz because it's boring + cutoffs = [ x for x in cutoffs if x <= 10000 ] + #print cutoffs + + # chunk cutoffs based on number of strings, + # and use the max freq val of the chunk for our new cutoff + cutoffs = [ max(c) for c in chunks(cutoffs, len(cutoffs)/options.numstrings) ] + #print cutoffs + + # Used for auto-ranging of display + maxval = 0 + + # Time limiting bits to slow down the UDP firehose + # This is stupid, but functional. Should be event driven, probably. + + sleeptime = 1.0/options.fps + while True: + data = stream.read(BUFSIZE) + numdata = numpy.fromstring(data, dtype=numpy.short) + + # Normalize data with our sample rate + normdata = numdata / sample_rate + + # Window the data, using a Hamming window, to better capture + normdata = normdata * window + + # Use a Fast Fourier Transform to convert to frequency domain + # Take the absolute value using numpy so the complex number + # returned by fft() is correctly converted to magnitude information + # rather than just chopping off the imaginary portion of the complex number + # discard the second half of the data because it's just aliased from + # the first half (i.e. repeated because of Nyquist reasons) + # For more detailed example, see: + # https://svn.enthought.com/enthought/browser/Chaco/trunk/examples/advanced/spectrum.py + # See also the doco for Numpy FFT, which explains how to get amplitude and + # power of the FFT output using different operations + # http://docs.scipy.org/doc/numpy/reference/routines.fft.html + fft_data = numpy.fft.fft(normdata) + freq = numpy.fft.fftfreq(fft_data.size) + + # Ignore the second half of the data + fft_data = fft_data[:len(fft_data)/2] + + # Convert frequency information into Hz based on the sample_rate + freq = freq[:len(freq)/2] * sample_rate + + # amplitude at each frequency + fft_amp = numpy.abs(fft_data) + + # power at each frequency + fft_power = numpy.abs(fft_data)**2 + + # Select the values we want to use + if options.mode == 'amp': + fft_vals = fft_amp[1:] + elif options.mode == 'power': + fft_vals = fft_power[1:] + else: + raise ValueError("Invalid mode selected: %s" % options.mode) + + # Average data into the number of buckets we want to display + # We use log-scale for buckets, because most of the interesting + # information (to humans) is in the lower frequencies. + visdata = [] + + #sys.exit(1) + vals = [] + + cutoff_idx = 0 + for val, f in zip(fft_vals, freq): + if f < cutoffs[cutoff_idx]: + vals.append(val) + else: + # average values for this bucket and save + if len(vals) == 0: + visdata.append(0) + else: + visdata.append( numpy.average(vals) ) + pass + vals = [] + # Update the cutoff value + cutoff_idx += 1 + + # Ignore anything higher than last cutoff + if cutoff_idx > len(cutoffs)-1: + break + + pass + pass + + #print visdata + #print ["%03d" % x for x in visdata] + + # scale bucket 0 down, because bass always seems to dominate the spectrum + # particularly for anything not classical music. + visdata[0] = visdata[0] / 1.5 + + for i in range(0, options.numstrings): + # Update auto-ranging information + if visdata[i] > maxval: + maxval = visdata[i] + pass + send_blinken(hols[i], visdata[i], maxval) + pass + pass + + # Wait for next timetick + time.sleep(sleeptime) + pass diff --git a/examples/holiscreen.py b/examples/holiscreen.py new file mode 100755 index 0000000..fc7df92 --- /dev/null +++ b/examples/holiscreen.py @@ -0,0 +1,191 @@ +#!/usr/bin/python +# +# Use a series of Holidays as a lo-res, flexible display screen that you +# can walk through like a beaded curtain. +# + +# FIXME: Update to be able to do video. + +import optparse +import Image +import math +import time + +from secretapi.holidaysecretapi import HolidaySecretAPI + +# FIXME: replace with OptionParser options +# Height of display == number of globes in Holiday strings +DISPLAY_HEIGHT = HolidaySecretAPI.NUM_GLOBES + +# Width of display == number of strings we want to use +DEFAULT_WIDTH = 10 + +def image_to_globes(img, numstrings=DEFAULT_WIDTH): + """ + Convert an image file into a Holiday string display + + @param imgfile: the image file to use + @param numstring: the number of Holiday strings in the display + """ + img = img.convert('RGB') + + # Ensure we will the 'screen' by fixing the display height + new_width = int(img.size[0] * (float(DISPLAY_HEIGHT) / img.size[1] )) + img.thumbnail( (new_width, DISPLAY_HEIGHT), Image.ANTIALIAS) + + # Now we want to sample display_width times across the width of + # the image + stringlist = [] + width, height = img.size + + slice_width = float(width)/float(numstrings) + for piece in range(numstrings): + # top left is (piece * slice_width, 0) + # bottom right is (piece+1 * offset, height) + bbox = ( int(piece * slice_width), 0, + int(math.ceil((piece+1) * slice_width)), height) + region = img.crop(bbox) + pixeldata = region.load() + + # For this region, average all the points at each Y, + # and save them as a list of colour values for the string + globelist = [] + for y in xrange(region.size[1]): + r = g = b = 0 + for x in xrange(region.size[0]): + # This works even for PNG images with alpha values + tr, tg, tb = pixeldata[x, y] + r += tr + g += tg + b += tb + pass + + # Take average of pixels + #print "r, g, b, rg", r, g, b, region.size[0] + globelist.append((int(r/float(region.size[0])), + int(g/float(region.size[0])), + int(b/float(region.size[0])), + )) + pass + + # save the list of globe colours + # We reverse, because otherwise the display is upside-down + globelist.reverse() + stringlist.append(globelist) + pass + return stringlist + +class HoliscreenOptions(optparse.OptionParser): + """ + Command-line options parser + """ + def __init__(self, *args, **kwargs): + optparse.OptionParser.__init__(self, **kwargs) + self.addOptions() + + def addOptions(self): + self.add_option('-n', '--numstrings', dest='numstrings', + help="Number of Holiday strings to simulate [%default]", + type="int", default=25) + + self.add_option('-f', '--file', dest='imgfile', + help="Image file to process. Overrides arguments.", + type="string" ) + + self.add_option('-a', '--animate', dest='animate', + help="Run in animation mode. Required animated GIF.", + action="store_true" ) + + self.add_option('-s', '--sleeptime', dest='anim_sleep', + help="Sleep between animation frames, in seconds [%default]", + type="float", default=0.1 ) + + + # Listen on multiple TCP/UDP ports, one for each Holiday we simulate + self.add_option('-p', '--portstart', dest='portstart', + help="Port number to start at for UDP listeners [%default]", + type="int", default=9988) + + def parseOptions(self): + """ + Emulate twistedmatrix options parser API + """ + options, args = self.parse_args() + self.options = options + self.args = args + + self.postOptions() + + return self.options, self.args + + def postOptions(self): + if not self.options.imgfile and len(self.args) == 0: + self.error("Image filename not given.") + pass + + if not self.options.imgfile: + self.options.imgfile = self.args[0] + pass + pass + +def render_image(img, hols): + """ + Render an image to a set of remote Holidays + """ + globelists = image_to_globes(img, + options.numstrings) + + for hol, globelist in zip(hols, globelists): + for i in range(len(globelist)): + r, g, b = globelist[i] + hol.setglobe(i, r, g, b) + pass + hol.render() + pass + pass + + +if __name__ == '__main__': + + usage = "Usage: %prog [options] " + optparse = HoliscreenOptions(usage=usage) + options, args = optparse.parseOptions() + hols = [] + for i in range(options.numstrings): + hols.append(HolidaySecretAPI(port=options.portstart+i)) + pass + + img = Image.open(options.imgfile) + + isanimated = False + + # Detect animated GIFs + if img.format == 'GIF': + try: + img.seek(img.tell()+1) + isanimated = True + except EOFError: + img.seek(0) + pass + pass + + if isanimated and options.animate: + # render first frame + render_image(img, hols) + while True: + # get the next frame after a short delay + time.sleep(options.anim_sleep) + + try: + img.seek(img.tell()+1) + render_image(img, hols) + except EOFError: + img.seek(0) + render_image(img, hols) + pass + pass + pass + else: + render_image(img, hols) + pass + diff --git a/examples/phloston.py b/examples/phloston.py index a3780d6..07df302 100644 --- a/examples/phloston.py +++ b/examples/phloston.py @@ -7,14 +7,16 @@ """ __author__ = "Justin Warren" -__version__ = '0.01-dev' +__version__ = '0.02-dev' __license__ = "MIT" import sys import time import logging +import optparse import holiday +from secretapi.holidaysecretapi import HolidaySecretAPI # Simulator default address SIM_ADDR = "localhost:8080" @@ -25,6 +27,61 @@ log.addHandler(handler) log.setLevel(logging.DEBUG) +class PhlostonOptions(optparse.OptionParser): + """ + Command-line options parser + """ + def __init__(self, *args, **kwargs): + optparse.OptionParser.__init__(self, **kwargs) + self.addOptions() + + def addOptions(self): + self.add_option('-n', '--numstrings', dest='numstrings', + help="Number of Holiday strings to use [%default]", + type="int", default=1) + + # Comms mode, TCP or UDP + self.add_option('-m', '--mode', dest='mode', + help="Communications mode, UDP or TCP [%default]", + type="choice", choices=['udp', 'tcp'], default='tcp') + + # Port to start at if we use multiple Holidays + self.add_option('-p', '--portstart', dest='portstart', + help="Port number to start at for strings [%default]", + type="int", default=8080) + + self.add_option('-f', '--fps', dest='fps', + help="Frames per second, used to slow down data sending", + type="int", default=30) + + + def parseOptions(self): + """ + Emulate twistedmatrix options parser API + """ + options, args = self.parse_args() + self.options = options + self.args = args + + self.postOptions() + + return self.options, self.args + + def postOptions(self): + if len(self.args) < 1: + self.args = [ SIM_ADDR, ] + pass + + # If we want more strings than we have arguments, + # then use the first argument as the base addr:port combo + # for the set, and increment port numbers. This behaviour + # is used with the simulator. If we specify all of them + # individually, then the arguments length is how many + # strings we have, and overrides the -n setting + if self.options.numstrings < len(self.args): + self.options.numstrings = len(self.args) + pass + class PhlostonString(object): """ A PhlostonString is a Holiday turned into a Phloston Paradise visual alarm light. @@ -35,6 +92,7 @@ class PhlostonString(object): def __init__(self, addr, color=(0xff, 0xff, 0xff), + mode='tcp', delay=0.02): """ Controls a single Holiday at addr @@ -43,10 +101,16 @@ def __init__(self, addr, @param color: The color of the lights @param delay: Time between lighting each globe """ - self.hol = holiday.Holiday(addr=addr, - remote=True) + if mode == 'tcp': + self.hol = holiday.Holiday(addr=addr, + remote=True) + elif mode == 'udp': + addr, port = addr.split(':') + self.hol = HolidaySecretAPI(addr, int(port)) + self.color = color - self.delay = 0.02 + + self.numlit = 0 self.base_pattern = [ (0x00, 0x00, 0x00), @@ -58,33 +122,69 @@ def __init__(self, addr, def animate(self): # Animation sequence is to start blank, then light each # globe in sequence until all are lit, then start again. - while True: - for step in range(0, self.hol.NUM_GLOBES): - #log.debug("Step %d of animation", step) - if step == 0: - # Reset to beginning - self.globe_pattern = self.base_pattern[:] - else: - for i in range(0, step): - self.globe_pattern[i] = self.color - pass - pass - #log.debug("Pattern is: %s", self.globe_pattern) - self.hol.set_pattern(self.globe_pattern) - self.hol.render() - time.sleep(0.02) + + self.numlit += 1 + if self.numlit > self.hol.NUM_GLOBES: + self.numlit = 0 + self.globe_pattern = self.base_pattern[:] + pass + else: + for i in range(self.numlit): + self.globe_pattern[i] = self.color pass + #log.debug("Pattern is: %s", self.globe_pattern) + self.hol.set_pattern(self.globe_pattern) + self.hol.render() + pass + pass if __name__ == '__main__': - if len(sys.argv) > 1: - hostname = sys.argv[1] - log.debug("hostname: %s", hostname) - else: - # Assume we're on the simulator - log.info("Using simulator: %s", SIM_ADDR) - hostname = SIM_ADDR + + usage = "Usage: %prog [options] []" + optparse = PhlostonOptions() + + options, args = optparse.parseOptions() + + phlostons = [] + + colorset = [ + (0x33, 0x88, 0x33), + (0x88, 0x33, 0x33), + (0x00, 0x33, 0x88), + (0x88, 0x88, 0x33), + (0x33, 0x88, 0x88), + ] + if options.numstrings > len(colorset): + colorset = colorset * ( int(options.numstrings/len(colorset))+1) + pass + + for i in range(options.numstrings): + if options.numstrings > len(args): + addr, port = args[0].split(':') + port = int(port) + i + ps_addr = "%s:%s" % (addr, port) + pass + else: + ps_addr = args[i] + pass + + ps = PhlostonString(ps_addr, + mode=options.mode, + color=colorset[i],) + phlostons.append(ps) + + pass + + # Time limiting bits to slow down the UDP firehose + # This is stupid, but functional. Should be event driven, probably. + sleeptime = 1.0/options.fps + + while True: + for i in range(options.numstrings): + phlostons[i].animate() + pass + # Wait for next timetick + time.sleep(sleeptime) pass + pass - ps = PhlostonString(hostname, - color=(0x33, 0x88, 0x33),) - ps.animate() diff --git a/simgame/__init__.py b/simgame/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/simgame/holiday.py b/simgame/holiday.py new file mode 100644 index 0000000..26279cb --- /dev/null +++ b/simgame/holiday.py @@ -0,0 +1,97 @@ +""" +Simulated Holiday Server +""" +import select, socket +import array + +UDP_HEADER_LENGTH = 10 +UDP_DATA_LENGTH = 150 +UDP_MSG_LENGTH = UDP_HEADER_LENGTH + UDP_DATA_LENGTH + +class HolidayRemote(object): + """ + A simulated remote Holiday. + + This object pretends to be a physical Holiday light, which + listens for instructions via both TCP (for the WebAPI) and + also via UDP (using the SecretLabsAPI). + + FIXME: Only the UDP interface is implemented for now. + """ + # FIXME: The core code should all be in one place, not duplicated + # in every subdirectory the way it is now. + NUM_GLOBES = 50 + def __init__(self, remote=False, addr='', + tcpport=None, + udpport=None): + + # Initialise globes to zero + self.globes = [ [0x00, 0x00, 0x00] ] * self.NUM_GLOBES + + if not remote: + self.addr = addr + self.tcpport = tcpport + if udpport is None: + bound_port = False + for port in range(9988, 10100): + try: + self.bind_udp(port) + bound_port = True + break + except socket.error, e: + num, s = e + # Ignore Address already in use + if num != 98: + raise + pass + pass + # If we get this far, bail out + if not bound_port: + raise ValueError("Can't find available UDP port in range 9988 to 10100") + + else: + self.bind_udp(udpport) + print "UDP listening on (%s, %s)" % (self.addr, self.udpport) + else: + raise NotImplementedError("Listening Simulator only. Does not send to remote devices.") + + def bind_udp(self, udpport): + """ + Try to bind to a UDP port, retrying a range if one is already in use + """ + self.udpport = udpport + self.udpsock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.udpsock.setblocking(0) + self.udpsock.bind((self.addr, self.udpport)) + + def recv_udp(self): + """ + Receive data on the UDP port and process it + """ + try: + data, addr = self.udpsock.recvfrom(512) # 512 byte buffer. Should be heaps + except socket.error, e: + num, msg = e + # This is EWOULDBLOCK, which we ignore + if num == 11: + return + + # Basic check that data is in the right format + # All SecretLabsAPI packets should be 160 bytes: + # 10 bytes of header (unused atm) and 150 bytes of data + if len(data) != UDP_MSG_LENGTH: + # Just ignore it for now + print "Incorrect msg length: %d" % len(data) + return + + header = data[:10] + globedata = array.array('B', data[10:]) + + # Iterate over globedata 3 at a time + # FIXME: replace with something from itertools? + for i in range(0, self.NUM_GLOBES): + coldata = globedata[:3] + globedata = globedata[3:] + self.globes[i] = [ coldata[0], coldata[1], coldata[2] ] + pass + diff --git a/simgame/simgame.py b/simgame/simgame.py new file mode 100755 index 0000000..6b9b7d9 --- /dev/null +++ b/simgame/simgame.py @@ -0,0 +1,244 @@ +#!/usr/bin/python +# +""" +Holiday simulator with a PyGame display for local development. + +Not as portable as the simpype in-browser display, but is more +responsive for things that need closer to hardware responsiveness. + +Also allows you to simulate a multi-Holiday display by aggregating +the display of multiple Holiday instances if required. +""" + +import optparse +import pygame +from holiday import HolidayRemote + +class BaseOptParser(optparse.OptionParser): + """ + Command-line options parser + """ + def __init__(self, *args, **kwargs): + optparse.OptionParser.__init__(self, **kwargs) + self.addOptions() + + def addOptions(self): + self.add_option('-f', '--fps', dest='fps', + help="Frames per second, used to slow down simulator", + type="int") + self.add_option('-n', '--numstrings', dest='numstrings', + help="Number of Holiday strings to simulate [%default]", + type="int", default=1) + + # Listen on multiple TCP/UDP ports, one for each Holiday we simulate + self.add_option('-p', '--portstart', dest='portstart', + help="Port number to start at for UDP listeners [%default]", + type="int", default=9988) + + # Listener mode, TCP or UDP + #self.add_option('-m', '--mode', dest='mode', + # help="Mode of the simulator, UDP or TCP [%default]", + # type="choice", choices=['udp', 'tcp'], default='udp') + + # Which way to draw multiple strings? Horizontally, like in the browser, + # or vertically, like a string curtain? + self.add_option('-o', '--orientation', dest='orientation', + help="Orientation of the strings [%default]", + type="choice", choices=['vertical', 'horizontal'], default='vertical') + + pass + + def parseOptions(self): + """ + Emulate twistedmatrix options parser API + """ + options, args = self.parse_args() + self.options = options + self.args = args + + self.postOptions() + + return self.options, self.args + + def postOptions(self): + pass + +class SimRunner(object): + """ + A simulator for multiple Holidays + + Each Holiday listens on a local IP address, but each on its own port. + """ + # Pixel width of gutters on each side of the screen + gutter_width = 100 + + # Size of each bulb + bulb_diam = 10 + + # Colour of the 'string' between the bulbs + string_color = pygame.Color(255,255,255) + + # width of the 'string' line, in px + string_width = 1 + + # Number of bulbs per Holiday + num_bulbs = 50 + + # space to leave at each end of the string, in px + string_header = 10 + string_footer = 10 + + def __init__(self, options): + self.options = options + self.setup() + + def setup(self): + + self.numstrings = self.options.numstrings + + self.HolidayList = [ ] + for i in range(0, self.numstrings): + self.HolidayList.append( HolidayRemote() ) + pass + + # Space between strings + # FIXME: Depends on orientation + if self.numstrings > 1: + self.spacer_width = ((1024 - (2*self.gutter_width)) / (self.numstrings-1)) - self.bulb_diam + else: + self.spacer_width = 0 + + pygame.init() + # Default font to use + self.fontsize = 24 + + self.myFont = pygame.font.SysFont("None", self.fontsize) + + # Where to start drawing strings + # FIXME: Depends on orientation + self.start_x = 100 + self.start_y = 768 - (3 * self.myFont.get_height()) - 20 + + # FIXME: Depends on orientation + self.string_length = 600 + + pass + + def run(self): + paused = False + + screen = pygame.display.set_mode([1024, 768]) + screen.fill((0,0,0)) + mainloop, x, y, color, delta, fps = True, 25 , 0, (32,32,32), 1, 1000 + Clock = pygame.time.Clock() + + while mainloop: + if self.options.fps: + tickFPS = Clock.tick(self.options.fps) + pass + # Move the simulator forward one tick + if not paused: + pass + + pygame.display.set_caption("Press Esc or q to quit. p to pause. r to reset. FPS: %.2f" % (Clock.get_fps())) + + color = (255,255,255) + + # Black screen and update timer + screen.fill((0,0,0)) + + screen.blit(self.myFont.render("Holiday by MooresCloud Simulator", True, (color)), (x,y)) + screen.blit(self.myFont.render("(c) Justin Warren ", True, (color)), (x,y+self.myFont.get_height())) + + # Let the strings receive and process data + self.recv_data() + + # Draw the Holiday strings + self.draw_strings(screen) + + for event in pygame.event.get(): + if event.type == pygame.QUIT: + mainloop = False + + elif event.type == pygame.KEYDOWN: + + # If quite key pressed, flag for exit + if event.key == pygame.K_ESCAPE or event.key == pygame.K_q: + mainloop = False + + # Toggle pause + elif event.key == pygame.K_p: + if paused: + paused = False + else: + paused = True + pass + pass + + # reset all strings to blank + elif event.key == pygame.K_r: + self.blank_strings() + pass + + pygame.display.update() + pass + pygame.quit() + pass + + def recv_data(self): + """ + Process any data each simulated string may have received + """ + for hol in self.HolidayList: + hol.recv_udp() + pass + + def blank_strings(self): + """ + Reset all strings to black + """ + for hol in self.HolidayList: + hol.globes = [ (0x00, 0x00, 0x00) ] * hol.NUM_GLOBES + pass + + def draw_strings(self, screen): + """ + Draw the Holiday strings on the screen + """ + # Figure out screen dimensions and place the strings accordingly + + for i, hol in enumerate(self.HolidayList): + xpos = self.start_x + (i*(self.bulb_diam + self.spacer_width)) + + pygame.draw.line(screen, self.string_color, + (xpos, self.start_y), + (xpos, self.start_y - self.string_length), + self.string_width) + + # Draw the lights + for j in range(0, hol.NUM_GLOBES): + bulb_y = self.start_y - self.string_footer - ( + j * (self.string_length - ( + self.string_header+self.string_footer) + ) / (self.num_bulbs) + ) + # Fetch the globe color for globe j + r, g, b = hol.globes[j] + pygame.draw.circle(screen, + pygame.Color(r, g, b), + (xpos, bulb_y), + self.bulb_diam / 2, + ) + + pass + + pass + +if __name__ == '__main__': + optparse = BaseOptParser() + options, args = optparse.parseOptions() + + sim = SimRunner(options) + sim.run() + + From 2783483d6e08608d6a2a073bcf710f2b484e566c Mon Sep 17 00:00:00 2001 From: Justin Warren Date: Wed, 30 Oct 2013 15:43:42 +1100 Subject: [PATCH 04/50] * Added ability to select orientation of string displays: vertical or horizontal. * Added option to fix spacing between strings. * Strings are now labelled with their number, to aid in debugging client code. --- simgame/simgame.py | 129 +++++++++++++++++++++++++++++++-------------- 1 file changed, 90 insertions(+), 39 deletions(-) diff --git a/simgame/simgame.py b/simgame/simgame.py index 6b9b7d9..d8493ad 100755 --- a/simgame/simgame.py +++ b/simgame/simgame.py @@ -46,6 +46,9 @@ def addOptions(self): help="Orientation of the strings [%default]", type="choice", choices=['vertical', 'horizontal'], default='vertical') + self.add_option('', '--spacing', dest='spacing', + help="Spacing between strings, in pixels. Overrides dynamic even spacing", + type="int") pass def parseOptions(self): @@ -70,7 +73,7 @@ class SimRunner(object): Each Holiday listens on a local IP address, but each on its own port. """ # Pixel width of gutters on each side of the screen - gutter_width = 100 + gutter_width = 50 # Size of each bulb bulb_diam = 10 @@ -85,7 +88,7 @@ class SimRunner(object): num_bulbs = 50 # space to leave at each end of the string, in px - string_header = 10 + string_header = 20 string_footer = 10 def __init__(self, options): @@ -96,38 +99,52 @@ def setup(self): self.numstrings = self.options.numstrings + self.screen_x = 1024 + self.screen_y = 768 + self.HolidayList = [ ] for i in range(0, self.numstrings): self.HolidayList.append( HolidayRemote() ) pass - # Space between strings - # FIXME: Depends on orientation - if self.numstrings > 1: - self.spacer_width = ((1024 - (2*self.gutter_width)) / (self.numstrings-1)) - self.bulb_diam + if not self.options.spacing: + if self.numstrings > 1: + if self.options.orientation == 'vertical': + self.spacer_width = ((self.screen_x - (2*self.gutter_width)) / self.numstrings) - self.bulb_diam + elif self.options.orientation == 'horizontal': + self.spacer_width = ((self.screen_y - self.gutter_width) / self.numstrings) - self.bulb_diam + pass + pass + else: + self.spacer_width = 0 + pass + pass else: - self.spacer_width = 0 - + self.spacer_width = self.options.spacing + pass + print "spacer:", self.spacer_width + pygame.init() # Default font to use self.fontsize = 24 self.myFont = pygame.font.SysFont("None", self.fontsize) - # Where to start drawing strings - # FIXME: Depends on orientation - self.start_x = 100 - self.start_y = 768 - (3 * self.myFont.get_height()) - 20 - + self.start_x = 0 + self.gutter_width + self.start_y = (3 * self.myFont.get_height()) + # FIXME: Depends on orientation - self.string_length = 600 + if self.options.orientation == 'vertical': + self.string_length = self.screen_y - self.start_y - self.gutter_width + elif self.options.orientation == 'horizontal': + self.string_length = self.screen_x - self.start_x - 2*self.gutter_width pass def run(self): paused = False - screen = pygame.display.set_mode([1024, 768]) + screen = pygame.display.set_mode([self.screen_x, self.screen_y]) screen.fill((0,0,0)) mainloop, x, y, color, delta, fps = True, 25 , 0, (32,32,32), 1, 1000 Clock = pygame.time.Clock() @@ -208,32 +225,66 @@ def draw_strings(self, screen): # Figure out screen dimensions and place the strings accordingly for i, hol in enumerate(self.HolidayList): - xpos = self.start_x + (i*(self.bulb_diam + self.spacer_width)) - - pygame.draw.line(screen, self.string_color, - (xpos, self.start_y), - (xpos, self.start_y - self.string_length), - self.string_width) - - # Draw the lights - for j in range(0, hol.NUM_GLOBES): - bulb_y = self.start_y - self.string_footer - ( - j * (self.string_length - ( - self.string_header+self.string_footer) - ) / (self.num_bulbs) - ) - # Fetch the globe color for globe j - r, g, b = hol.globes[j] - pygame.draw.circle(screen, - pygame.Color(r, g, b), - (xpos, bulb_y), - self.bulb_diam / 2, - ) - + + if self.options.orientation == 'vertical': + xpos = self.start_x + (i*(self.bulb_diam + self.spacer_width)) + + pygame.draw.line(screen, self.string_color, + (xpos, self.start_y), + (xpos, self.start_y + self.string_length), + self.string_width) + + # Label the string with its number + screen.blit(self.myFont.render("%d" % i, True, (255,255,255)), + (xpos - self.bulb_diam/2, self.start_y + self.string_length + self.bulb_diam)) + + # Draw the lights + for j in range(0, hol.NUM_GLOBES): + bulb_y = self.start_y + self.string_header + ( + j * (self.string_length - + (self.string_header+self.string_footer)) / self.num_bulbs + ) + # Fetch the globe color for globe j + r, g, b = hol.globes[j] + pygame.draw.circle(screen, + pygame.Color(r, g, b), + (xpos, bulb_y), + self.bulb_diam / 2, + ) + pass + pass + + elif self.options.orientation == 'horizontal': + ypos = self.start_y + (i*(self.bulb_diam + self.spacer_width)) + + pygame.draw.line(screen, self.string_color, + (self.start_x, ypos), + (self.start_x + self.string_length, ypos), + self.string_width) + + # Label the string with its number + screen.blit(self.myFont.render("%d" % i, True, (255,255,255)), + (self.bulb_diam, ypos-self.bulb_diam)) + + # Draw the lights + for j in range(0, hol.NUM_GLOBES): + bulb_x = self.start_x + self.string_header + ( + j * (self.string_length - + (self.string_header+self.string_footer)) / self.num_bulbs + ) + # Fetch the globe color for globe j + r, g, b = hol.globes[j] + pygame.draw.circle(screen, + pygame.Color(r, g, b), + (bulb_x, ypos), + self.bulb_diam / 2, + ) + pass + pass pass - + pass pass - + if __name__ == '__main__': optparse = BaseOptParser() options, args = optparse.parseOptions() From 85b2d549dffc6a9f978910bad9bfa703e332f75c Mon Sep 17 00:00:00 2001 From: Justin Warren Date: Wed, 30 Oct 2013 17:26:44 +1100 Subject: [PATCH 05/50] * chmod 755 on phloston.py * Added 'switchback' mode to simulator, to simulate looping a Holiday string back on itself in a zigzag/switchback pattern. --- examples/phloston.py | 0 simgame/simgame.py | 157 ++++++++++++++++++++++++++++--------------- 2 files changed, 104 insertions(+), 53 deletions(-) mode change 100644 => 100755 examples/phloston.py diff --git a/examples/phloston.py b/examples/phloston.py old mode 100644 new mode 100755 diff --git a/simgame/simgame.py b/simgame/simgame.py index d8493ad..6c9340f 100755 --- a/simgame/simgame.py +++ b/simgame/simgame.py @@ -9,8 +9,9 @@ Also allows you to simulate a multi-Holiday display by aggregating the display of multiple Holiday instances if required. """ - +import sys import optparse +import math import pygame from holiday import HolidayRemote @@ -49,6 +50,11 @@ def addOptions(self): self.add_option('', '--spacing', dest='spacing', help="Spacing between strings, in pixels. Overrides dynamic even spacing", type="int") + + self.add_option('', '--switchback', dest='switchback', + help="'Switchback' strings, make a single string display like its " + "more than one every m globes", + type="int") pass def parseOptions(self): @@ -107,7 +113,17 @@ def setup(self): self.HolidayList.append( HolidayRemote() ) pass - if not self.options.spacing: + if self.options.switchback: + self.bulbs_per_piece = self.options.switchback + self.num_pieces = int(math.ceil( float(self.num_bulbs) / self.options.switchback)) + pass + else: + self.bulbs_per_piece = self.num_bulbs + self.num_pieces = 1 + + pass + + if not self.options.spacing and not self.options.switchback: if self.numstrings > 1: if self.options.orientation == 'vertical': self.spacer_width = ((self.screen_x - (2*self.gutter_width)) / self.numstrings) - self.bulb_diam @@ -119,11 +135,13 @@ def setup(self): self.spacer_width = 0 pass pass - else: + elif self.options.spacing: self.spacer_width = self.options.spacing pass - print "spacer:", self.spacer_width - + else: + self.spacer_width = 2 * self.bulb_diam + pass + pygame.init() # Default font to use self.fontsize = 24 @@ -133,7 +151,6 @@ def setup(self): self.start_x = 0 + self.gutter_width self.start_y = (3 * self.myFont.get_height()) - # FIXME: Depends on orientation if self.options.orientation == 'vertical': self.string_length = self.screen_y - self.start_y - self.gutter_width elif self.options.orientation == 'horizontal': @@ -172,7 +189,8 @@ def run(self): # Draw the Holiday strings self.draw_strings(screen) - + #sys.exit(1) + for event in pygame.event.get(): if event.type == pygame.QUIT: mainloop = False @@ -223,62 +241,95 @@ def draw_strings(self, screen): Draw the Holiday strings on the screen """ # Figure out screen dimensions and place the strings accordingly + offset_per_bulb = (self.string_length-( + self.string_header+self.string_footer)) / self.bulbs_per_piece for i, hol in enumerate(self.HolidayList): if self.options.orientation == 'vertical': - xpos = self.start_x + (i*(self.bulb_diam + self.spacer_width)) - - pygame.draw.line(screen, self.string_color, - (xpos, self.start_y), - (xpos, self.start_y + self.string_length), - self.string_width) - - # Label the string with its number - screen.blit(self.myFont.render("%d" % i, True, (255,255,255)), - (xpos - self.bulb_diam/2, self.start_y + self.string_length + self.bulb_diam)) - - # Draw the lights - for j in range(0, hol.NUM_GLOBES): - bulb_y = self.start_y + self.string_header + ( - j * (self.string_length - - (self.string_header+self.string_footer)) / self.num_bulbs - ) - # Fetch the globe color for globe j - r, g, b = hol.globes[j] - pygame.draw.circle(screen, - pygame.Color(r, g, b), - (xpos, bulb_y), - self.bulb_diam / 2, - ) + for piece in range(self.num_pieces): + string_offset = self.start_x + ( i*( + self.num_pieces*(self.bulb_diam+self.spacer_width) + )) #+ i*self.spacer_width + xpos = string_offset + piece*(self.bulb_diam + self.spacer_width) + + pygame.draw.line(screen, self.string_color, + (xpos, self.start_y), + (xpos, self.start_y + self.string_length), + self.string_width) + + # Label the string with its number + if piece == 0: + screen.blit(self.myFont.render("%d" % i, True, (255,255,255)), + (xpos - self.bulb_diam/2, self.start_y + self.string_length + self.bulb_diam)) + pass + + # Draw the lights + for m, j in enumerate(range(piece * self.bulbs_per_piece, + min((piece+1)*self.bulbs_per_piece, hol.NUM_GLOBES) + ) + ): + # If even, start at the top and go down + if not piece % 2: + bulb_y = self.start_y + self.string_header + (m * offset_per_bulb) + + # if odd, go from bottom to top + else: + bulb_y = self.start_y + self.string_header + ( (self.bulbs_per_piece-1-m) * offset_per_bulb) + pass + + # Fetch the globe color for globe j + r, g, b = hol.globes[j] + pygame.draw.circle(screen, + pygame.Color(r, g, b), + (xpos, bulb_y), + self.bulb_diam / 2, + ) + pass pass pass elif self.options.orientation == 'horizontal': - ypos = self.start_y + (i*(self.bulb_diam + self.spacer_width)) + + for piece in range(self.num_pieces): + string_offset = self.start_y + ( i*( + self.num_pieces*(self.bulb_diam+self.spacer_width) + )) #+ i*self.spacer_width + + ypos = string_offset + piece*(self.bulb_diam + self.spacer_width) - pygame.draw.line(screen, self.string_color, - (self.start_x, ypos), - (self.start_x + self.string_length, ypos), - self.string_width) + pygame.draw.line(screen, self.string_color, + (self.start_x, ypos), + (self.start_x + self.string_length, ypos), + self.string_width) - # Label the string with its number - screen.blit(self.myFont.render("%d" % i, True, (255,255,255)), - (self.bulb_diam, ypos-self.bulb_diam)) - - # Draw the lights - for j in range(0, hol.NUM_GLOBES): - bulb_x = self.start_x + self.string_header + ( - j * (self.string_length - - (self.string_header+self.string_footer)) / self.num_bulbs - ) - # Fetch the globe color for globe j - r, g, b = hol.globes[j] - pygame.draw.circle(screen, - pygame.Color(r, g, b), - (bulb_x, ypos), - self.bulb_diam / 2, - ) + # Label the string with its number + if piece == 0: + screen.blit(self.myFont.render("%d" % i, True, (255,255,255)), + (self.bulb_diam, ypos-self.bulb_diam)) + pass + + # Draw the lights + for m, j in enumerate(range(piece * self.bulbs_per_piece, + min((piece+1)*self.bulbs_per_piece, hol.NUM_GLOBES) + ) + ): + # If even, start at the top and go down + if not piece % 2: + bulb_x = self.start_x + self.string_header + (m * offset_per_bulb) + # if odd, go from bottom to top + else: + bulb_x = self.start_x + self.string_header + ( (self.bulbs_per_piece-1-m) * offset_per_bulb) + pass + + # Fetch the globe color for globe j + r, g, b = hol.globes[j] + pygame.draw.circle(screen, + pygame.Color(r, g, b), + (bulb_x, ypos), + self.bulb_diam / 2, + ) + pass pass pass pass From bf36f629b20d5c1b662c24a94ec1a0e54978f7db Mon Sep 17 00:00:00 2001 From: Justin Warren Date: Thu, 31 Oct 2013 13:31:15 +1100 Subject: [PATCH 06/50] * Changed string color in simgame to be a darker grey, so it doesn't dominate the display over the globes. * Updated holiscreen to support switchback mode. --- examples/holiscreen.py | 152 ++++++++++++++++++++++++++++------------- simgame/simgame.py | 2 +- 2 files changed, 105 insertions(+), 49 deletions(-) diff --git a/examples/holiscreen.py b/examples/holiscreen.py index fc7df92..6fd0b30 100755 --- a/examples/holiscreen.py +++ b/examples/holiscreen.py @@ -10,6 +10,7 @@ import Image import math import time +import numpy from secretapi.holidaysecretapi import HolidaySecretAPI @@ -20,7 +21,7 @@ # Width of display == number of strings we want to use DEFAULT_WIDTH = 10 -def image_to_globes(img, numstrings=DEFAULT_WIDTH): +def image_to_globes(img, width=DEFAULT_WIDTH, height=DISPLAY_HEIGHT): """ Convert an image file into a Holiday string display @@ -29,48 +30,51 @@ def image_to_globes(img, numstrings=DEFAULT_WIDTH): """ img = img.convert('RGB') - # Ensure we will the 'screen' by fixing the display height - new_width = int(img.size[0] * (float(DISPLAY_HEIGHT) / img.size[1] )) - img.thumbnail( (new_width, DISPLAY_HEIGHT), Image.ANTIALIAS) + # Resize image based on the longest side of our holiscreen + if width < height: + new_width = int(img.size[0] * (float(height) / img.size[1] )) + new_height = height + else: + new_width = width + new_height = int(img.size[0] * (float(width) / img.size[1] )) + + img.thumbnail( (new_width, new_height), Image.ANTIALIAS) # Now we want to sample display_width times across the width of # the image stringlist = [] - width, height = img.size - - slice_width = float(width)/float(numstrings) - for piece in range(numstrings): - # top left is (piece * slice_width, 0) - # bottom right is (piece+1 * offset, height) - bbox = ( int(piece * slice_width), 0, - int(math.ceil((piece+1) * slice_width)), height) - region = img.crop(bbox) - pixeldata = region.load() - - # For this region, average all the points at each Y, - # and save them as a list of colour values for the string - globelist = [] - for y in xrange(region.size[1]): - r = g = b = 0 - for x in xrange(region.size[0]): - # This works even for PNG images with alpha values - tr, tg, tb = pixeldata[x, y] - r += tr - g += tg - b += tb - pass + iw, ih = img.size - # Take average of pixels - #print "r, g, b, rg", r, g, b, region.size[0] - globelist.append((int(r/float(region.size[0])), - int(g/float(region.size[0])), - int(b/float(region.size[0])), - )) - pass + slice_width = float(iw)/float(width) + slice_height = float(ih)/float(height) + # Slice image into boxed partitions slice_width x slice_height + # and average the colour of the pixels in that box to give + # us the pixel colour for our downsampled display. + # FIXME: There's probably a function in PIL to do this, but + # I haven't been able to find it yet. + + for w_slice in range(width): + globelist = [] + for h_slice in range(height): + bbox = ( int(w_slice * slice_width), + int(h_slice * slice_height), + int(math.ceil((w_slice+1) * slice_width)), + int(math.ceil((h_slice+1) * slice_height)) + ) + region = img.crop(bbox) + pixeldata = list(region.getdata()) + + # split out the r, b, g values for each pixel and average them + rlist, glist, blist = zip(*pixeldata) + globelist.append( (int(numpy.average(rlist)), + int(numpy.average(glist)), + int(numpy.average(blist)),) + ) + pass # save the list of globe colours # We reverse, because otherwise the display is upside-down - globelist.reverse() + #globelist.reverse() stringlist.append(globelist) pass return stringlist @@ -106,6 +110,13 @@ def addOptions(self): help="Port number to start at for UDP listeners [%default]", type="int", default=9988) + self.add_option('', '--switchback', dest='switchback', + help="'Switchback' strings, make a single string display like its " + "more than one every m globes", + type="int") + pass + + def parseOptions(self): """ Emulate twistedmatrix options parser API @@ -128,18 +139,53 @@ def postOptions(self): pass pass -def render_image(img, hols): +def render_image(img, hols, width, height, switchback=None): """ Render an image to a set of remote Holidays - """ - globelists = image_to_globes(img, - options.numstrings) - for hol, globelist in zip(hols, globelists): - for i in range(len(globelist)): - r, g, b = globelist[i] - hol.setglobe(i, r, g, b) + @param switchback: How many globes per piece of a switchback + """ + globelists = image_to_globes(img, width, height) + + # Using this indirect array method, rather than set_globes() + # directly, because some weird bug I can't find and squash makes the + # globelists all the same if we try to render the Holidays in one + # go, rather than rendering each 'line' of the switchback. + # Your guess is as good as mine. + holglobes = [] + for i in range(len(hols)): + holglobes.append( [[0x00,0x00,0x00]] * HolidaySecretAPI.NUM_GLOBES ) + pass + + # If switchback mode is enabled, reverse order + # of every second line, so they display the right + # way on zigzag Holidays + pieces = int(math.floor(float(HolidaySecretAPI.NUM_GLOBES) / height)) + + for l, line in enumerate(globelists): + # swap order every second line if in switchback mode + # Which holiday are we talking to? + holid = l*height / (pieces * switchback) + hol = hols[holid] + + #print "holid %d, l %d, oddeven %d, l mod pieces %d" % (holid, l, (l % (pieces)) % 2, l % (pieces)) + basenum = (l%pieces) * height + + for i, values in enumerate(line): + r, g, b = values + if not (l % pieces) % 2: + globe_idx = basenum + i + else: + globe_idx = basenum + (height-i) - 1 + pass + holglobes[holid][globe_idx] = [r,g,b] + #hol.setglobe(globe_idx, r, g, b) pass + pass + + # Render to each Holiday + for i, hol in enumerate(hols): + hol.set_pattern( holglobes[i] ) hol.render() pass pass @@ -157,6 +203,17 @@ def render_image(img, hols): img = Image.open(options.imgfile) + # FIXME: Swap height and width for horizontal orientation + if options.switchback: + height = options.switchback + pieces = int(math.floor(float(HolidaySecretAPI.NUM_GLOBES) / height)) + width = options.numstrings * pieces + else: + height = HolidaySecretAPI.NUM_GLOBES + pieces = options.numstrings + width = options.numstrings + pass + isanimated = False # Detect animated GIFs @@ -171,21 +228,20 @@ def render_image(img, hols): if isanimated and options.animate: # render first frame - render_image(img, hols) + render_image(img, hols, width, height, options.switchback) while True: # get the next frame after a short delay time.sleep(options.anim_sleep) try: img.seek(img.tell()+1) - render_image(img, hols) + render_image(img, hols, width, height, options.switchback) except EOFError: img.seek(0) - render_image(img, hols) + render_image(img, hols, width, height, options.switchback) pass pass pass else: - render_image(img, hols) + render_image(img, hols, width, height, options.switchback) pass - diff --git a/simgame/simgame.py b/simgame/simgame.py index 6c9340f..5b6ec80 100755 --- a/simgame/simgame.py +++ b/simgame/simgame.py @@ -85,7 +85,7 @@ class SimRunner(object): bulb_diam = 10 # Colour of the 'string' between the bulbs - string_color = pygame.Color(255,255,255) + string_color = pygame.Color(40,40,40) # width of the 'string' line, in px string_width = 1 From 60cb8b07f5e4d636d698b0aa83b32cf5b1eea31c Mon Sep 17 00:00:00 2001 From: Justin Warren Date: Fri, 1 Nov 2013 22:05:28 +1100 Subject: [PATCH 07/50] * Updated holiscreen to support horizontal orientation of Holidays. * Added an led_scroller example, to display arbitrary text in a configurable font face, size, and colour as a scrolling LED style display to an array of Holidays. --- examples/holiscreen.py | 100 +++++++++++++------ examples/led_scroller.py | 203 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 272 insertions(+), 31 deletions(-) create mode 100755 examples/led_scroller.py diff --git a/examples/holiscreen.py b/examples/holiscreen.py index 6fd0b30..c929146 100755 --- a/examples/holiscreen.py +++ b/examples/holiscreen.py @@ -14,14 +14,13 @@ from secretapi.holidaysecretapi import HolidaySecretAPI -# FIXME: replace with OptionParser options -# Height of display == number of globes in Holiday strings -DISPLAY_HEIGHT = HolidaySecretAPI.NUM_GLOBES +# Height of display == number of globes in a string +DEFAULT_HEIGHT = HolidaySecretAPI.NUM_GLOBES # Width of display == number of strings we want to use -DEFAULT_WIDTH = 10 +DEFAULT_WIDTH = HolidaySecretAPI.NUM_GLOBES -def image_to_globes(img, width=DEFAULT_WIDTH, height=DISPLAY_HEIGHT): +def image_to_globes(img, width=DEFAULT_WIDTH, height=DEFAULT_HEIGHT): """ Convert an image file into a Holiday string display @@ -53,10 +52,9 @@ def image_to_globes(img, width=DEFAULT_WIDTH, height=DISPLAY_HEIGHT): # us the pixel colour for our downsampled display. # FIXME: There's probably a function in PIL to do this, but # I haven't been able to find it yet. - - for w_slice in range(width): + for h_slice in range(height): globelist = [] - for h_slice in range(height): + for w_slice in range(width): bbox = ( int(w_slice * slice_width), int(h_slice * slice_height), int(math.ceil((w_slice+1) * slice_width)), @@ -110,6 +108,10 @@ def addOptions(self): help="Port number to start at for UDP listeners [%default]", type="int", default=9988) + self.add_option('-o', '--orientation', dest='orientation', + help="Orientation of the strings [%default]", + type="choice", choices=['vertical', 'horizontal'], default='vertical') + self.add_option('', '--switchback', dest='switchback', help="'Switchback' strings, make a single string display like its " "more than one every m globes", @@ -139,14 +141,22 @@ def postOptions(self): pass pass -def render_image(img, hols, width, height, switchback=None): +def render_image(img, hols, width, height, + orientation='vertical', + switchback=None): """ Render an image to a set of remote Holidays @param switchback: How many globes per piece of a switchback """ globelists = image_to_globes(img, width, height) + render_to_hols(globelists, hols, width, height, orientation, switchback) +def render_to_hols(globelists, hols, width, height, + orientation='vertical', switchback=None): + """ + Render a set of globe values to a set of Holidays + """ # Using this indirect array method, rather than set_globes() # directly, because some weird bug I can't find and squash makes the # globelists all the same if we try to render the Holidays in one @@ -156,30 +166,48 @@ def render_image(img, hols, width, height, switchback=None): for i in range(len(hols)): holglobes.append( [[0x00,0x00,0x00]] * HolidaySecretAPI.NUM_GLOBES ) pass + + if orientation == 'vertical': + orientsize = height + else: + orientsize = width # If switchback mode is enabled, reverse order # of every second line, so they display the right # way on zigzag Holidays - pieces = int(math.floor(float(HolidaySecretAPI.NUM_GLOBES) / height)) - + pieces = int(math.floor(float(HolidaySecretAPI.NUM_GLOBES) / orientsize)) + + # The globelist is a set of y values. List [0] is all the vertical globes + # for x = 0, list [1] is the vertical x=1 globes, etc. + # The orientation determines which holiday gets each pixel in the list. + # In vertical orientation, the first list goes to holiday 0, the second + # to holiday 2. In switchback mode, the first n lists go to holiday 0. + # In horizontal mode, the first row is spread between holidays, depending + # on switchback. for l, line in enumerate(globelists): - # swap order every second line if in switchback mode - # Which holiday are we talking to? - holid = l*height / (pieces * switchback) - hol = hols[holid] - - #print "holid %d, l %d, oddeven %d, l mod pieces %d" % (holid, l, (l % (pieces)) % 2, l % (pieces)) - basenum = (l%pieces) * height + basenum = (l%pieces) * orientsize + for i, values in enumerate(line): + + # Which holiday are we talking to? + if switchback: + holid = l*orientsize / (pieces * switchback) + else: + if orientation == 'vertical': + holid = l + else: + holid = l + pass + hol = hols[holid] + r, g, b = values if not (l % pieces) % 2: globe_idx = basenum + i else: - globe_idx = basenum + (height-i) - 1 + globe_idx = basenum + (orientsize-i) - 1 pass holglobes[holid][globe_idx] = [r,g,b] - #hol.setglobe(globe_idx, r, g, b) pass pass @@ -205,13 +233,24 @@ def render_image(img, hols, width, height, switchback=None): # FIXME: Swap height and width for horizontal orientation if options.switchback: - height = options.switchback - pieces = int(math.floor(float(HolidaySecretAPI.NUM_GLOBES) / height)) - width = options.numstrings * pieces + if options.orientation == 'vertical': + height = options.switchback + pieces = int(math.floor(float(HolidaySecretAPI.NUM_GLOBES) / height)) + width = options.numstrings * pieces + else: + width = options.switchback + pieces = int(math.floor(float(HolidaySecretAPI.NUM_GLOBES) / width)) + height = options.numstrings * pieces else: - height = HolidaySecretAPI.NUM_GLOBES - pieces = options.numstrings - width = options.numstrings + if options.orientation == 'vertical': + height = HolidaySecretAPI.NUM_GLOBES + pieces = options.numstrings + width = options.numstrings + else: + width = HolidaySecretAPI.NUM_GLOBES + pieces = options.numstrings + height = options.numstrings + pass pass isanimated = False @@ -228,20 +267,19 @@ def render_image(img, hols, width, height, switchback=None): if isanimated and options.animate: # render first frame - render_image(img, hols, width, height, options.switchback) + render_image(img, hols, width, height, options.orientation, options.switchback) while True: # get the next frame after a short delay time.sleep(options.anim_sleep) - try: img.seek(img.tell()+1) - render_image(img, hols, width, height, options.switchback) + except EOFError: img.seek(0) - render_image(img, hols, width, height, options.switchback) pass + render_image(img, hols, width, height, options.orientation, options.switchback) pass pass else: - render_image(img, hols, width, height, options.switchback) + render_image(img, hols, width, height, options.orientation, options.switchback) pass diff --git a/examples/led_scroller.py b/examples/led_scroller.py new file mode 100755 index 0000000..2d80341 --- /dev/null +++ b/examples/led_scroller.py @@ -0,0 +1,203 @@ +#!/usr/bin/python +""" +A simple hack to turn one or more Holidays into an LED scrolling display +""" + +import optparse +import time +import pygame +import sys + +from secretapi.holidaysecretapi import HolidaySecretAPI + +from holiscreen import render_to_hols + +# Initialise the font module +pygame.font.init() + +class LEDScrollerOptions(optparse.OptionParser): + """ + Command-line options parser + """ + def __init__(self, *args, **kwargs): + optparse.OptionParser.__init__(self, **kwargs) + self.addOptions() + + def addOptions(self): + self.add_option('-n', '--numstrings', dest='numstrings', + help="Number of Holiday strings to simulate [%default]", + type="int", default=7) + + self.add_option('-a', '--animate', dest='animate', + help="Run in scroller mode", + action="store_true" ) + + self.add_option('-s', '--sleeptime', dest='anim_sleep', + help="Sleep between animation frames, in seconds [%default]", + type="float", default=0.1 ) + + # Listen on multiple TCP/UDP ports, one for each Holiday we simulate + self.add_option('-p', '--portstart', dest='portstart', + help="Port number to start at for UDP listeners [%default]", + type="int", default=9988) + + self.add_option('', '--switchback', dest='switchback', + help="'Switchback' strings, make a single string display like its " + "more than one every m globes", + type="int") + + self.add_option('', '--font', dest='fontname', + help="Name of the font to use. [%default]", + type="string", default="couriernew") + + self.add_option('', '--fontsize', dest='fontsize', + help="Size of the font, in points [%default]", + type="int", default="7") + + self.add_option('', '--color', dest='color', + help="Color of the font [%default]", + type="string", default="0xffffff") + + self.add_option('', '--list-fonts', dest='listfonts', + help="List fonts available for use", + action="store_true") + + self.add_option('', '--blank-padding', dest='blankpadding', + help="Spaces to leave between end of string " + "and start when wrapping around. [%default]", + type="int", default=2) + + def parseOptions(self): + """ + Emulate twistedmatrix options parser API + """ + options, args = self.parse_args() + self.options = options + self.args = args + + self.postOptions() + + return self.options, self.args + + def postOptions(self): + pass + +def to_rgb( norm_rgba, mask=(0,0,0) ): + """ + Convert normalized RGBA color to flat RGB color. + + Default is for a black background. + Annoyingly, pygame doesn't seem to have this feature in its + Color module for some reason. Sadface. + """ + #print norm_rgba + r, g, b, a = norm_rgba + bg_r, bg_g, bg_b = mask + + # Convert to target color + # http://stackoverflow.com/questions/2049230/convert-rgba-color-to-rgb + tr = int((((1-a)*r) + (a*bg_r)) * 255) + tg = int((((1-a)*g) + (a*bg_g)) * 255) + tb = int((((1-a)*b) + (a*bg_b)) * 255) + + return (tr, tg, tb) + +def text_to_globes(text, width, height, + color=(255,255,255), + offset_left=0): + """ + Take a text string and return a list of globelists. + + One list will be returned for vertical line, each of width globes + long. This implies Holidays layed out horizontally for display. + + #FIXME: Allow rotation for horizontal display with vertically hung + Holidays. + + The left offset can be set (in pixels), which will control the + window of the text display. Incrementing the offset can be used + to create a scrolling display. + """ + #font = pygame.font.SysFont("None", 10) + + font = pygame.font.SysFont(options.fontname, options.fontsize) + color = pygame.Color(color) + + surface = font.render(text, True, color, (0,0,0) ) + globelists = [] + + # Now fetch the pixels as an array + pa = pygame.PixelArray(surface) + for i in range(min(height, len(pa[0]))): + globes = [] + + # First, grab the pixels in the window + pixels = pa[:,i] + for px in pixels: + + # Convert from a surface color int to RGBA values + (r,g,b,a) = pa.surface.unmap_rgb(px) + globes.append( (r,g,b) ) + pass + + # Pad with blanks if the text is shorter than width + if len(pa) < width: + globes.extend( [(0,0,0)] * (width - len(pa)) ) + pass + globelists.append(globes) + pass + return globelists + +if __name__ == '__main__': + + usage = "Usage: %prog [options]" + optparse = LEDScrollerOptions(usage=usage) + + options, args = optparse.parseOptions() + + height = options.numstrings + + if options.switchback: + width = options.switchback + else: + width = HolidaySecretAPI.NUM_GLOBES + + if options.listfonts: + print '\n'.join(pygame.font.get_fonts()) + sys.exit(0) + + hols = [] + for i in range(options.numstrings): + hols.append(HolidaySecretAPI(port=options.portstart+i)) + pass + + # The text to render is passed in from stdin + text = sys.stdin.read().rstrip() + if len(text) == 0: + text = "Holiday by MooresCloud. The world's most intelligent Christmas lights!" + pass + text = ''.join([text, ' ' * options.blankpadding]) + + glist = text_to_globes(text, width, height, color=options.color) + + #print "glist:", glist + # Scroll the display + offset = 0 + while True: + # Draw only as much of the globelists as will fit in the width + render_glist = [] + for hol_list in glist: + new_list = hol_list[offset:width+offset] + # wraparound + if len(new_list) < width: + new_list.extend(hol_list[:(width-len(new_list))]) + pass + render_glist.append(new_list) + pass + #print "renderlist:", render_glist + render_to_hols(render_glist, hols, width, height, orientation='horizontal') + offset += 1 + if offset > len(glist[0]): + offset = 0 + pass + time.sleep(options.anim_sleep) From 3ce8e78f761c001588cf6d5b8a65089e4aef2135 Mon Sep 17 00:00:00 2001 From: Justin Warren Date: Fri, 1 Nov 2013 23:04:50 +1100 Subject: [PATCH 08/50] * Support for switchback mode in led_scroller.py * Turned on the animation switch in led_scroller, so you can turn scrolling on and off. --- examples/holiscreen.py | 13 ++++++++----- examples/led_scroller.py | 14 ++++++++++++-- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/examples/holiscreen.py b/examples/holiscreen.py index c929146..0f41a53 100755 --- a/examples/holiscreen.py +++ b/examples/holiscreen.py @@ -189,25 +189,28 @@ def render_to_hols(globelists, hols, width, height, basenum = (l%pieces) * orientsize for i, values in enumerate(line): + r, g, b = values # Which holiday are we talking to? if switchback: - holid = l*orientsize / (pieces * switchback) - else: if orientation == 'vertical': - holid = l + holid = l*orientsize / (pieces * switchback) else: - holid = l + holid = l*orientsize / (pieces * switchback) + else: + holid = l pass hol = hols[holid] - r, g, b = values if not (l % pieces) % 2: globe_idx = basenum + i else: globe_idx = basenum + (orientsize-i) - 1 pass holglobes[holid][globe_idx] = [r,g,b] + + #print "line: %d, holid: %d, globe: %d, val: (%d, %d, %d)" % (l, holid, globe_idx, r,g,b) + pass pass diff --git a/examples/led_scroller.py b/examples/led_scroller.py index 2d80341..1d7a61d 100755 --- a/examples/led_scroller.py +++ b/examples/led_scroller.py @@ -7,6 +7,7 @@ import time import pygame import sys +import math from secretapi.holidaysecretapi import HolidaySecretAPI @@ -155,11 +156,14 @@ def text_to_globes(text, width, height, options, args = optparse.parseOptions() - height = options.numstrings + if options.switchback: width = options.switchback + pieces = int(math.floor(float(HolidaySecretAPI.NUM_GLOBES) / width)) + height = pieces * options.numstrings else: + height = options.numstrings width = HolidaySecretAPI.NUM_GLOBES if options.listfonts: @@ -195,9 +199,15 @@ def text_to_globes(text, width, height, render_glist.append(new_list) pass #print "renderlist:", render_glist - render_to_hols(render_glist, hols, width, height, orientation='horizontal') + render_to_hols(render_glist, hols, width, height, + orientation='horizontal', + switchback=options.switchback) offset += 1 if offset > len(glist[0]): offset = 0 pass + + if not options.animate: + sys.exit(0) time.sleep(options.anim_sleep) + From 1469bd92348bcd1c5e41ca86332878a4be2083e0 Mon Sep 17 00:00:00 2001 From: Justin Warren Date: Sat, 2 Nov 2013 15:24:35 +1100 Subject: [PATCH 09/50] * Informative error message from holiscreen if you don't specify enough Holidays to display the number of lines your data needs. * led_scroller trims off blank lines above text so you don't waste Holidays by sending them black lines above (or below) the text. --- examples/holiscreen.py | 14 +++++++++++++- examples/led_scroller.py | 39 ++++++++++++--------------------------- 2 files changed, 25 insertions(+), 28 deletions(-) diff --git a/examples/holiscreen.py b/examples/holiscreen.py index 0f41a53..64b6b1f 100755 --- a/examples/holiscreen.py +++ b/examples/holiscreen.py @@ -11,6 +11,8 @@ import math import time import numpy +import sys +import logging from secretapi.holidaysecretapi import HolidaySecretAPI @@ -20,6 +22,12 @@ # Width of display == number of strings we want to use DEFAULT_WIDTH = HolidaySecretAPI.NUM_GLOBES +log = logging.getLogger(sys.argv[0]) +handler = logging.StreamHandler() +handler.setFormatter(logging.Formatter("%(asctime)s: %(name)s [%(levelname)s]: %(message)s")) +log.addHandler(handler) +log.setLevel(logging.DEBUG) + def image_to_globes(img, width=DEFAULT_WIDTH, height=DEFAULT_HEIGHT): """ Convert an image file into a Holiday string display @@ -200,7 +208,11 @@ def render_to_hols(globelists, hols, width, height, else: holid = l pass - hol = hols[holid] + try: + hol = hols[holid] + except IndexError: + log.error("Not enough Holidays for number of screen lines. Need at least %d." % (holid+1,)) + sys.exit(1) if not (l % pieces) % 2: globe_idx = basenum + i diff --git a/examples/led_scroller.py b/examples/led_scroller.py index 1d7a61d..f82c897 100755 --- a/examples/led_scroller.py +++ b/examples/led_scroller.py @@ -52,7 +52,7 @@ def addOptions(self): type="string", default="couriernew") self.add_option('', '--fontsize', dest='fontsize', - help="Size of the font, in points [%default]", + help="Size of the font, in pixels [%default]", type="int", default="7") self.add_option('', '--color', dest='color', @@ -83,26 +83,6 @@ def parseOptions(self): def postOptions(self): pass -def to_rgb( norm_rgba, mask=(0,0,0) ): - """ - Convert normalized RGBA color to flat RGB color. - - Default is for a black background. - Annoyingly, pygame doesn't seem to have this feature in its - Color module for some reason. Sadface. - """ - #print norm_rgba - r, g, b, a = norm_rgba - bg_r, bg_g, bg_b = mask - - # Convert to target color - # http://stackoverflow.com/questions/2049230/convert-rgba-color-to-rgb - tr = int((((1-a)*r) + (a*bg_r)) * 255) - tg = int((((1-a)*g) + (a*bg_g)) * 255) - tb = int((((1-a)*b) + (a*bg_b)) * 255) - - return (tr, tg, tb) - def text_to_globes(text, width, height, color=(255,255,255), offset_left=0): @@ -125,19 +105,24 @@ def text_to_globes(text, width, height, color = pygame.Color(color) surface = font.render(text, True, color, (0,0,0) ) + globelists = [] # Now fetch the pixels as an array pa = pygame.PixelArray(surface) - for i in range(min(height, len(pa[0]))): + for i in range(len(pa[0])): globes = [] - - # First, grab the pixels in the window pixels = pa[:,i] - for px in pixels: - + pixvals = [ pa.surface.unmap_rgb(x) for x in pixels ] + # Check to see if this is a blank line, i.e. + # all pixels are black. Ignore the line if this is the case. + # We don't want to waste lines on blanks. + if pixvals == [ (0,0,0,255) ] * len(pixvals): + #print "skipping blank line %d" % i + continue + + for (r, g, b, a) in pixvals: # Convert from a surface color int to RGBA values - (r,g,b,a) = pa.surface.unmap_rgb(px) globes.append( (r,g,b) ) pass From 38b498ecf156b6545314679fab3d6819c67dbaa6 Mon Sep 17 00:00:00 2001 From: Justin Warren Date: Sat, 2 Nov 2013 15:49:35 +1100 Subject: [PATCH 10/50] * led_scroller font anti-aliasing is now optional. --- examples/led_scroller.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/examples/led_scroller.py b/examples/led_scroller.py index f82c897..819a957 100755 --- a/examples/led_scroller.py +++ b/examples/led_scroller.py @@ -51,6 +51,10 @@ def addOptions(self): help="Name of the font to use. [%default]", type="string", default="couriernew") + self.add_option('', '--antialias', dest='antialias', + help="Use anti-aliasing for font rendering. [%default]", + action="store_true", default=False) + self.add_option('', '--fontsize', dest='fontsize', help="Size of the font, in pixels [%default]", type="int", default="7") @@ -104,7 +108,7 @@ def text_to_globes(text, width, height, font = pygame.font.SysFont(options.fontname, options.fontsize) color = pygame.Color(color) - surface = font.render(text, True, color, (0,0,0) ) + surface = font.render(text, options.antialias, color, (0,0,0) ) globelists = [] @@ -140,8 +144,6 @@ def text_to_globes(text, width, height, optparse = LEDScrollerOptions(usage=usage) options, args = optparse.parseOptions() - - if options.switchback: width = options.switchback From 75c3937173535a6a94341b0b74c2ab1c563e94ba Mon Sep 17 00:00:00 2001 From: Justin Warren Date: Sun, 22 Dec 2013 11:07:15 +1100 Subject: [PATCH 11/50] * Added ability to specify IP address of remote Holidays to led_scroller.py and holiscreen.py. * holiscreen orientation works now for both vertical and horizontal when using switchback. --- examples/holiscreen.py | 73 ++++++++++++++++++++++++++++------------ examples/led_scroller.py | 19 ++++++++--- 2 files changed, 66 insertions(+), 26 deletions(-) diff --git a/examples/holiscreen.py b/examples/holiscreen.py index 64b6b1f..f2e9d81 100755 --- a/examples/holiscreen.py +++ b/examples/holiscreen.py @@ -140,12 +140,12 @@ def parseOptions(self): return self.options, self.args def postOptions(self): - if not self.options.imgfile and len(self.args) == 0: - self.error("Image filename not given.") + if len(self.args) < 1: + self.error("Specify address and port of remote Holiday(s)") pass if not self.options.imgfile: - self.options.imgfile = self.args[0] + self.error("Image filename not given.") pass pass @@ -157,7 +157,9 @@ def render_image(img, hols, width, height, @param switchback: How many globes per piece of a switchback """ + #log.debug("w x h: %d x %d", width, height) globelists = image_to_globes(img, width, height) + #log.debug("len globelists: %d", len(globelists)) render_to_hols(globelists, hols, width, height, orientation, switchback) def render_to_hols(globelists, hols, width, height, @@ -192,34 +194,57 @@ def render_to_hols(globelists, hols, width, height, # to holiday 2. In switchback mode, the first n lists go to holiday 0. # In horizontal mode, the first row is spread between holidays, depending # on switchback. + #log.debug("globelist len: %d", len(globelists)) for l, line in enumerate(globelists): - - basenum = (l%pieces) * orientsize for i, values in enumerate(line): r, g, b = values + #log.debug("os: %d, pieces: %d, sb: %s, l: %d, i: %d", orientsize, pieces, switchback, l, i) + # Which holiday are we talking to? - if switchback: - if orientation == 'vertical': + if orientation == 'horizontal': + basenum = (l%pieces) * orientsize + + if switchback: holid = l*orientsize / (pieces * switchback) else: - holid = l*orientsize / (pieces * switchback) + holid = l + + if not (l % pieces) % 2: + globe_idx = basenum + i + else: + globe_idx = basenum + (orientsize-i) - 1 + pass + else: - holid = l - pass + basenum = (i % pieces) * orientsize + + if switchback: + holid = i / pieces + #log.debug("basenum: %d", basenum) + else: + holid = i + basenum = 0 + + if not (i % pieces) % 2: + globe_idx = basenum + l + else: + globe_idx = basenum + (orientsize-l) - 1 + pass + try: + #log.debug("holid: %d, globeidx: %d", holid, globe_idx) hol = hols[holid] except IndexError: log.error("Not enough Holidays for number of screen lines. Need at least %d." % (holid+1,)) sys.exit(1) - if not (l % pieces) % 2: - globe_idx = basenum + i - else: - globe_idx = basenum + (orientsize-i) - 1 - pass - holglobes[holid][globe_idx] = [r,g,b] + try: + holglobes[holid][globe_idx] = [r,g,b] + except IndexError: + log.debug("Error at holid %d, globeidx %d", holid, globe_idx) + raise #print "line: %d, holid: %d, globe: %d, val: (%d, %d, %d)" % (l, holid, globe_idx, r,g,b) @@ -236,17 +261,23 @@ def render_to_hols(globelists, hols, width, height, if __name__ == '__main__': - usage = "Usage: %prog [options] " + usage = "Usage: %prog [options] [ ... ]" optparse = HoliscreenOptions(usage=usage) options, args = optparse.parseOptions() hols = [] - for i in range(options.numstrings): - hols.append(HolidaySecretAPI(port=options.portstart+i)) + if len(args) > 1: + for arg in args: + hol_addr, hol_port = arg.split(':') + hols.append(HolidaySecretAPI(addr=hol_addr, port=int(hol_port))) + else: + hol_addr, hol_port = args[0].split(':') + for i in range(options.numstrings): + hols.append(HolidaySecretAPI(addr=hol_addr, port=int(hol_port)+i)) + pass pass - + img = Image.open(options.imgfile) - # FIXME: Swap height and width for horizontal orientation if options.switchback: if options.orientation == 'vertical': height = options.switchback diff --git a/examples/led_scroller.py b/examples/led_scroller.py index 819a957..9efad37 100755 --- a/examples/led_scroller.py +++ b/examples/led_scroller.py @@ -44,7 +44,7 @@ def addOptions(self): self.add_option('', '--switchback', dest='switchback', help="'Switchback' strings, make a single string display like its " - "more than one every m globes", + "more than one every SWITCHBACK globes", type="int") self.add_option('', '--font', dest='fontname', @@ -85,6 +85,9 @@ def parseOptions(self): return self.options, self.args def postOptions(self): + if len(self.args) < 1: + self.error("Specify address and port of remote Holiday(s)") + pass pass def text_to_globes(text, width, height, @@ -140,7 +143,7 @@ def text_to_globes(text, width, height, if __name__ == '__main__': - usage = "Usage: %prog [options]" + usage = "Usage: %prog [options] [ ... ]" optparse = LEDScrollerOptions(usage=usage) options, args = optparse.parseOptions() @@ -158,9 +161,15 @@ def text_to_globes(text, width, height, sys.exit(0) hols = [] - for i in range(options.numstrings): - hols.append(HolidaySecretAPI(port=options.portstart+i)) - pass + if len(args) > 1: + for arg in args: + hol_addr, hol_port = arg.split(':') + hols.append(HolidaySecretAPI(addr=hol_addr, port=int(hol_port))) + else: + hol_addr, hol_port = args[0].split(':') + for i in range(options.numstrings): + hols.append(HolidaySecretAPI(addr=hol_addr, port=int(hol_port)+i)) + pass # The text to render is passed in from stdin text = sys.stdin.read().rstrip() From 4ef975545df523b2169d8c1ad2ebca08d7919e0b Mon Sep 17 00:00:00 2001 From: Justin Warren Date: Sun, 22 Dec 2013 12:35:54 +1100 Subject: [PATCH 12/50] * FIXME comment added to led_scroller * Updated holibeats to support IP address of remote Holiday * Updated holibeats to support switchback mode --- examples/holibeats.py | 171 +++++++++++++++++++++++++++++++-------- examples/led_scroller.py | 2 + 2 files changed, 141 insertions(+), 32 deletions(-) diff --git a/examples/holibeats.py b/examples/holibeats.py index 205ac1d..0775d4b 100755 --- a/examples/holibeats.py +++ b/examples/holibeats.py @@ -5,6 +5,7 @@ import sys import pyaudio import audioop +import math import numpy import scipy import time @@ -21,6 +22,14 @@ RATE = 44100 BUFSIZE = 2048 +import logging +log = logging.getLogger(sys.argv[0]) +handler = logging.StreamHandler() +handler.setFormatter(logging.Formatter("%(asctime)s: %(name)s [%(levelname)s]: %(message)s")) +log.addHandler(handler) +log.setLevel(logging.DEBUG) + + def list_devices(): # List all audio input devices p = pyaudio.PyAudio() @@ -60,40 +69,96 @@ def calc_samp_amp(sample): return result -def send_blinken(hol, value, maxval=None): +def send_blinken(hols, visdata, pieces=1, switchback=None, maxval=None, maxheight=None): """ - Create a light pattern for a remote Holiday - based on the value we receive. + Create a light pattern for a remote Holidays + based on the values we receive. - If maxval is supplied, scale value as proportion of + If maxval is supplied, scale values as proportion of maxval, so the display auto-ranges. """ - # Normalize value based on maxval == 48 globes - if maxval: - value = value/maxval * 48 - pass - - value = int(value) - - # Clip values greater than 50 - if value > 50: - value = 50 - pass + #log.debug("numhols: %d, buckets: %d", len(hols), len(visdata)) + #log.debug("pieces: %d, sb: %d", pieces, switchback) - for i in range(0, value): - if i < 25: - hol.setglobe(i, 50, 180, 50) - elif i < 40: - hol.setglobe(i, 222, 215, 26) + if maxheight is None: + if switchback: + maxheight = float(switchback) else: - hol.setglobe(i, 200, 50, 50) + maxheight = float(HolidaySecretAPI.NUM_GLOBES) + pass pass + + # Only use the first m values from visdata, based on how many we can display + m = len(hols) * pieces - for i in range(value, 51): - hol.setglobe(i, 0, 0, 0) + for i, value in enumerate(visdata[:m]): + holid = int(i / pieces) + + # Set the base globe index for each bucket in switchback mode + if switchback: + basenum = (i % pieces) * switchback + else: + # With no switchback, the bucket basenum is globe 0 + basenum = 0 + pass + + # Normalize value based on maxval == maxheight + if maxval: + value = value/maxval * maxheight + pass + + value = int(value) + + # Clip values greater than maxheight + if value > maxheight: + value = int(maxheight) + pass + + # Set the globes for the holiday for this bucket + # using switchback if required + # Use different colours for different height + # cutoffs, so low values are green, middle yellow/orange + # and high values are red. + + for j in range(0, value): + + # Set the globe index, reversing for switchback mode + # and using the basenum offset for each switchback bucket + if not (i % pieces) % 2: + globe_idx = basenum + j + else: + globe_idx = basenum + (switchback-j) - 1 + pass + + # Set the globe colour based on how far it + # is from the maximum value + if (j/maxheight) < 0.4: + r, g, b = 50, 180, 50 + elif (j/maxheight) < 0.7: + r, g, b = 222, 215, 26 + else: + r, g, b = 200, 50, 50 + pass + hols[holid].setglobe(globe_idx, r, g, b) + pass + + # Blank globes above the value in this bucket + for j in range(value, int(maxheight) + 1): + if not (i % pieces) % 2: + globe_idx = basenum + j + else: + globe_idx = basenum + (switchback-j) - 1 + pass + try: + hols[holid].setglobe(globe_idx, 0, 0, 0) + except IndexError: + log.error("Failed on holid %d", holid) + raise pass - hol.render() + # Render all the Holidays + for hol in hols: + hol.render() class HolibeatOptions(optparse.OptionParser): """ @@ -113,6 +178,10 @@ def addOptions(self): help="Port number to start at for UDP listeners [%default]", type="int", default=9988) + self.add_option('-b', '--buckets', dest='numbuckets', + help="Number of frequency bands (buckets) for display", + type="int") + self.add_option('-m', '--mode', dest='mode', help="Frequency mode: amplitude or power [%default]", type="choice", choices=['amp', 'power'], default='amp') @@ -120,6 +189,15 @@ def addOptions(self): self.add_option('-f', '--fps', dest='fps', help="Frames per second, used to slow down data sending", type="int", default=30) + + self.add_option('', '--switchback', dest='switchback', + help="'Switchback' strings, make a single string display like its " + "more than one every SWITCHBACK globes", + type="int") + + self.add_option('', '--maxheight', dest='maxheight', + help="Manually set the maximum height value for buckets", + type="float") def parseOptions(self): """ @@ -134,6 +212,9 @@ def parseOptions(self): return self.options, self.args def postOptions(self): + if len(self.args) < 1: + self.error("Specify address and port of remote Holiday(s)") + pass pass def chunks(l, n): @@ -146,7 +227,10 @@ def chunks(l, n): if __name__ == '__main__': #list_devices() - optparse = HolibeatOptions() + + usage = "Usage: %prog [options] [ ... ]" + + optparse = HolibeatOptions(usage=usage) options, args = optparse.parseOptions() pa = pyaudio.PyAudio() @@ -166,11 +250,33 @@ def chunks(l, n): # different buckets. window = numpy.hamming(BUFSIZE*2) - hols = [] - for i in range(0, options.numstrings): - hols.append(HolidaySecretAPI(port=options.portstart+i)) + if options.switchback: + pieces = int(math.floor(float(HolidaySecretAPI.NUM_GLOBES) / options.switchback)) + numbuckets = pieces * options.numstrings + else: + pieces = 1 + numbuckets = options.numstrings pass + # Allow manual override of number of buckets + # Mostly used to clip the higher, less interesting bands, so we visualise the lower + # bands with more granularity + if options.numbuckets: + numbuckets = options.numbuckets + pass + + hols = [] + + if len(args) > 1: + for arg in args: + hol_addr, hol_port = arg.split(':') + hols.append(HolidaySecretAPI(addr=hol_addr, port=int(hol_port))) + else: + hol_addr, hol_port = args[0].split(':') + for i in range(options.numstrings): + hols.append(HolidaySecretAPI(addr=hol_addr, port=int(hol_port)+i)) + pass + # Build the list of cutoff frequences as powers of 10 cutoffs = [] exp = [2, 3, 4] @@ -188,8 +294,7 @@ def chunks(l, n): # chunk cutoffs based on number of strings, # and use the max freq val of the chunk for our new cutoff - cutoffs = [ max(c) for c in chunks(cutoffs, len(cutoffs)/options.numstrings) ] - #print cutoffs + cutoffs = [ max(c) for c in chunks(cutoffs, len(cutoffs)/numbuckets) ] # Used for auto-ranging of display maxval = 0 @@ -279,13 +384,15 @@ def chunks(l, n): # particularly for anything not classical music. visdata[0] = visdata[0] / 1.5 - for i in range(0, options.numstrings): + for i in range(0, numbuckets): # Update auto-ranging information if visdata[i] > maxval: maxval = visdata[i] pass - send_blinken(hols[i], visdata[i], maxval) pass + + # Send data to Holidays + send_blinken(hols, visdata, pieces, options.switchback, maxval, options.maxheight) pass # Wait for next timetick diff --git a/examples/led_scroller.py b/examples/led_scroller.py index 9efad37..2f9de5c 100755 --- a/examples/led_scroller.py +++ b/examples/led_scroller.py @@ -188,6 +188,8 @@ def text_to_globes(text, width, height, render_glist = [] for hol_list in glist: new_list = hol_list[offset:width+offset] + + # FIXME: Ability to scroll in both directions, via a flag # wraparound if len(new_list) < width: new_list.extend(hol_list[:(width-len(new_list))]) From 96fb5c22dc98134b6a6e4ecbc6663a92ac810722 Mon Sep 17 00:00:00 2001 From: Justin Warren Date: Sun, 22 Dec 2013 16:02:58 +1100 Subject: [PATCH 13/50] * Added workaround for multiple Holidays all rendering at once, as per holiscreen.py * Added ability to turn off autoranging display if you want. * Added re-balancing of autoranging so if you play something loud, then quiet, the max value will slide downwards and the display will still be interesting. --- examples/holibeats.py | 61 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 50 insertions(+), 11 deletions(-) diff --git a/examples/holibeats.py b/examples/holibeats.py index 0775d4b..534bed8 100755 --- a/examples/holibeats.py +++ b/examples/holibeats.py @@ -9,6 +9,7 @@ import numpy import scipy import time +import datetime #from scipy.signal import kaiserord, lfilter, firwin, freqz from scipy import fft @@ -69,7 +70,11 @@ def calc_samp_amp(sample): return result -def send_blinken(hols, visdata, pieces=1, switchback=None, maxval=None, maxheight=None): +def send_blinken(hols, visdata, pieces=1, + switchback=None, + maxval=None, + maxheight=None, + autorange=True): """ Create a light pattern for a remote Holidays based on the values we receive. @@ -80,6 +85,12 @@ def send_blinken(hols, visdata, pieces=1, switchback=None, maxval=None, maxheigh #log.debug("numhols: %d, buckets: %d", len(hols), len(visdata)) #log.debug("pieces: %d, sb: %d", pieces, switchback) + # Same bug as in holiscreen.py render_to_hols() + holglobes = [] + for i in range(len(hols)): + holglobes.append( [[0x00,0x00,0x00]] * HolidaySecretAPI.NUM_GLOBES ) + pass + if maxheight is None: if switchback: maxheight = float(switchback) @@ -103,7 +114,7 @@ def send_blinken(hols, visdata, pieces=1, switchback=None, maxval=None, maxheigh pass # Normalize value based on maxval == maxheight - if maxval: + if autorange and maxval: value = value/maxval * maxheight pass @@ -133,32 +144,35 @@ def send_blinken(hols, visdata, pieces=1, switchback=None, maxval=None, maxheigh # Set the globe colour based on how far it # is from the maximum value if (j/maxheight) < 0.4: - r, g, b = 50, 180, 50 + r, g, b = 0, 200, 0 elif (j/maxheight) < 0.7: r, g, b = 222, 215, 26 else: - r, g, b = 200, 50, 50 + r, g, b = 240, 50, 50 pass - hols[holid].setglobe(globe_idx, r, g, b) + holglobes[holid][globe_idx] = [r,g,b] pass # Blank globes above the value in this bucket - for j in range(value, int(maxheight) + 1): + for j in range(value, int(maxheight)): if not (i % pieces) % 2: globe_idx = basenum + j else: globe_idx = basenum + (switchback-j) - 1 pass try: - hols[holid].setglobe(globe_idx, 0, 0, 0) + holglobes[holid][globe_idx] = [0,0,0] except IndexError: - log.error("Failed on holid %d", holid) + log.error("Failed on holid %d, globe_idx %d", holid, globe_idx) raise pass # Render all the Holidays - for hol in hols: + for i, hol in enumerate(hols): + hol.set_pattern( holglobes[i] ) hol.render() + pass + pass class HolibeatOptions(optparse.OptionParser): """ @@ -195,6 +209,14 @@ def addOptions(self): "more than one every SWITCHBACK globes", type="int") + self.add_option('', '--autorange', dest='autorange', + help="Dynamically set range of display based on max value", + action='store_true', default=True) + + self.add_option('', '--no-autorange', dest='autorange', + help="Disable auto-ranging display", + action='store_false') + self.add_option('', '--maxheight', dest='maxheight', help="Manually set the maximum height value for buckets", type="float") @@ -298,6 +320,7 @@ def chunks(l, n): # Used for auto-ranging of display maxval = 0 + maxtime = datetime.datetime.now() # Time limiting bits to slow down the UDP firehose # This is stupid, but functional. Should be event driven, probably. @@ -382,17 +405,33 @@ def chunks(l, n): # scale bucket 0 down, because bass always seems to dominate the spectrum # particularly for anything not classical music. - visdata[0] = visdata[0] / 1.5 + visdata[0] = visdata[0] / 1.8 for i in range(0, numbuckets): # Update auto-ranging information if visdata[i] > maxval: maxval = visdata[i] + #log.debug("maxval reset: %f", maxval) + maxtime = datetime.datetime.now() pass pass + # If the maxval was set more than n seconds ago, start + # reducing the maxval gradually by x% per loop until + # we have to set the max again + if datetime.datetime.now() - maxtime > datetime.timedelta(seconds=2): + #log.debug("maxval %f is old. decreasing...", maxval) + maxval = maxval - (maxval * 0.05) + if maxval < 0: + maxval = 0 + pass + # Send data to Holidays - send_blinken(hols, visdata, pieces, options.switchback, maxval, options.maxheight) + send_blinken(hols, visdata, pieces, + switchback=options.switchback, + maxval=maxval, + maxheight=options.maxheight, + autorange=options.autorange) pass # Wait for next timetick From f4f69e259c27daf18b8b521fecba9a5500a03bc0 Mon Sep 17 00:00:00 2001 From: Justin Warren Date: Sun, 22 Dec 2013 22:21:50 +1100 Subject: [PATCH 14/50] * Make a Holiday twinkle like multi-coloured stars --- examples/holibeats.py | 8 +- examples/twinkle.py | 187 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 193 insertions(+), 2 deletions(-) create mode 100755 examples/twinkle.py diff --git a/examples/holibeats.py b/examples/holibeats.py index 534bed8..9ee7563 100755 --- a/examples/holibeats.py +++ b/examples/holibeats.py @@ -221,6 +221,10 @@ def addOptions(self): help="Manually set the maximum height value for buckets", type="float") + self.add_option('', '--quietseconds', dest='quiet_seconds', + help="Period of relative quiet to reset autoranging", + type="int", default=5) + def parseOptions(self): """ Emulate twistedmatrix options parser API @@ -419,9 +423,9 @@ def chunks(l, n): # If the maxval was set more than n seconds ago, start # reducing the maxval gradually by x% per loop until # we have to set the max again - if datetime.datetime.now() - maxtime > datetime.timedelta(seconds=2): + if datetime.datetime.now() - maxtime > datetime.timedelta(seconds=options.quiet_seconds): #log.debug("maxval %f is old. decreasing...", maxval) - maxval = maxval - (maxval * 0.05) + maxval = maxval - (maxval * 0.01) if maxval < 0: maxval = 0 pass diff --git a/examples/twinkle.py b/examples/twinkle.py new file mode 100755 index 0000000..c721668 --- /dev/null +++ b/examples/twinkle.py @@ -0,0 +1,187 @@ +#!/usr/bin/python +""" +Twinkling pretty lights on the Holiday tree +""" + +import optparse +import time +import sys +import random +import colorsys + +from secretapi.holidaysecretapi import HolidaySecretAPI + +import logging +log = logging.getLogger(sys.argv[0]) +handler = logging.StreamHandler() +handler.setFormatter(logging.Formatter("%(asctime)s: %(name)s [%(levelname)s]: %(message)s")) +log.addHandler(handler) +log.setLevel(logging.DEBUG) + +class TwinkleOptions(optparse.OptionParser): + """ + Command-line options parser + """ + def __init__(self, *args, **kwargs): + optparse.OptionParser.__init__(self, **kwargs) + self.addOptions() + + def addOptions(self): + self.add_option('-n', '--numstrings', dest='numstrings', + help="Number of Holiday strings to use [%default]", + type="int", default=1) + + self.add_option('-c', '--change_chance', dest='change_chance', + help="% chance a given globe will change each round [%default]", + type="float", default=1.0 ) + + self.add_option('-a', '--animsleep', dest='anim_sleep', + help="Sleep between animation frames, in seconds [%default]", + type="float", default=0.1 ) + + self.add_option('-H', '--HUESTEP', dest='huestep_max', + help="Maximum step between hues [%default]", + type="float", default=0.1 ) + + self.add_option('-S', '--SATSTEP', dest='satstep_max', + help="Maximum step between saturations [%default]", + type="float", default=0.01 ) + + self.add_option('-V', '--VALSTEP', dest='valstep_max', + help="Maximum step between values [%default]", + type="float", default=0.2 ) + + # Send on multiple TCP/UDP ports, one for each Holiday we simulate + self.add_option('-p', '--portstart', dest='portstart', + help="Port number to start at for UDP listeners [%default]", + type="int", default=9988) + + def parseOptions(self): + """ + Emulate twistedmatrix options parser API + """ + options, args = self.parse_args() + self.options = options + self.args = args + + self.postOptions() + + return self.options, self.args + + def postOptions(self): + if len(self.args) < 1: + self.error("Specify address and port of remote Holiday(s)") + pass + pass + +def init_hol(hol): + """ + Initialize a Holiday to some random-ish colors + """ + for globeidx in range(0, HolidaySecretAPI.NUM_GLOBES): + color = [] + for i in range(0, 3): + color.append(random.randint(0, 255)) + pass + r, g, b = color + hol.setglobe(globeidx, r, g, b) + pass + hol.render() + +def twinkle_holiday(hol, + huestep_max=0.1, + satstep_max=0.1, + valstep_max=0.5, + change_chance=1.0 + ): + """ + Make a Holiday twinkle like the stars + """ + # For each globe, mostly have a random drift of brightness + # and hue by but occasionally jump in brightness up or down + for idx in range(0, HolidaySecretAPI.NUM_GLOBES): + + # % chance of updating a given globe + if random.random() < change_chance: + + r, g, b = hol.getglobe(idx) + (h, s, v) = colorsys.rgb_to_hsv(r/255.0, g/255.0, b/255.0) + #log.debug("start h s v: %f %f %f", h, s, v) + # Adjust hue by a random amount + huestep = random.random() * huestep_max + # 50% chance of positive or negative step + if random.randint(0, 1): + h += huestep + if h > 1.0: + h = h - 1.0 + else: + h -= huestep + if h < 0.0: + h = 1.0 + h + pass + + satstep = random.random() * satstep_max + if random.randint(0, 1): + s += satstep + if s > 1.0: + s = 1.0 + else: + s -= satstep + # Make sure things stay bright and colorful! + if s < 0.5: + s = 0.5 + + # Adjust value by a random amount + valstep = random.random() * valstep_max + # 50% chance of positive or negative step + if random.randint(0, 1): + v += valstep + if v > 1.0: + v = 1.0 + else: + v -= valstep + if v < 0.0: + v = 0.0 + pass + + #log.debug("end h s v: %f %f %f", h, s, v) + + (r, g, b) = colorsys.hsv_to_rgb(h, s, v) + #log.debug("r g b: %f %f %f", r, g, b) + hol.setglobe(idx, int(255*r), int(255*g), int(255*b)) + pass + pass + hol.render() + +if __name__ == '__main__': + + usage = "Usage: %prog [options] [ ... ]" + optparse = TwinkleOptions(usage=usage) + + options, args = optparse.parseOptions() + + hols = [] + if len(args) > 1: + for arg in args: + hol_addr, hol_port = arg.split(':') + hols.append(HolidaySecretAPI(addr=hol_addr, port=int(hol_port))) + else: + hol_addr, hol_port = args[0].split(':') + for i in range(options.numstrings): + hols.append(HolidaySecretAPI(addr=hol_addr, port=int(hol_port)+i)) + pass + + for hol in hols: + init_hol(hol) + pass + + while True: + for hol in hols: + twinkle_holiday(hol, + options.huestep_max, + options.satstep_max, + options.valstep_max, + options.change_chance) + pass + time.sleep(options.anim_sleep) + From e2c26d115725071a54e8318d1871a12538a3a845 Mon Sep 17 00:00:00 2001 From: Justin Warren Date: Mon, 23 Dec 2013 07:17:01 +1100 Subject: [PATCH 15/50] * Adjusted twinkle.py saturation minimum and startup brightness. --- examples/twinkle.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/twinkle.py b/examples/twinkle.py index c721668..f74c5fc 100755 --- a/examples/twinkle.py +++ b/examples/twinkle.py @@ -81,7 +81,7 @@ def init_hol(hol): for globeidx in range(0, HolidaySecretAPI.NUM_GLOBES): color = [] for i in range(0, 3): - color.append(random.randint(0, 255)) + color.append(random.randint(0, 130)) pass r, g, b = color hol.setglobe(globeidx, r, g, b) @@ -128,8 +128,8 @@ def twinkle_holiday(hol, else: s -= satstep # Make sure things stay bright and colorful! - if s < 0.5: - s = 0.5 + if s < 0.2: + s = 0.2 # Adjust value by a random amount valstep = random.random() * valstep_max From c9f63d5c6e77e293a7cb27a602b0dbb24e786559 Mon Sep 17 00:00:00 2001 From: Justin Warren Date: Mon, 23 Dec 2013 11:42:33 +1100 Subject: [PATCH 16/50] * Updated holibeats colours to be have richer yellow and red --- examples/holibeats.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/holibeats.py b/examples/holibeats.py index 9ee7563..7623629 100755 --- a/examples/holibeats.py +++ b/examples/holibeats.py @@ -143,12 +143,12 @@ def send_blinken(hols, visdata, pieces=1, # Set the globe colour based on how far it # is from the maximum value - if (j/maxheight) < 0.4: - r, g, b = 0, 200, 0 + if (j/maxheight) < 0.3: + r, g, b = 0, 200, 0 # green elif (j/maxheight) < 0.7: - r, g, b = 222, 215, 26 + r, g, b = 220, 220, 00 # yellow else: - r, g, b = 240, 50, 50 + r, g, b = 240, 10, 10 # red pass holglobes[holid][globe_idx] = [r,g,b] pass @@ -210,7 +210,7 @@ def addOptions(self): type="int") self.add_option('', '--autorange', dest='autorange', - help="Dynamically set range of display based on max value", + help="Dynamically set range of display based on max value [%default]", action='store_true', default=True) self.add_option('', '--no-autorange', dest='autorange', @@ -222,7 +222,7 @@ def addOptions(self): type="float") self.add_option('', '--quietseconds', dest='quiet_seconds', - help="Period of relative quiet to reset autoranging", + help="Period of relative quiet to reset autoranging [%default]", type="int", default=5) def parseOptions(self): From b6d223db7532f4acd4111135ce9627410017f3fe Mon Sep 17 00:00:00 2001 From: Justin Warren Date: Mon, 23 Dec 2013 17:47:16 +1100 Subject: [PATCH 17/50] * Added some alternate colour schemes for holibeats. --- examples/holibeats.py | 54 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 44 insertions(+), 10 deletions(-) diff --git a/examples/holibeats.py b/examples/holibeats.py index 7623629..b0b3583 100755 --- a/examples/holibeats.py +++ b/examples/holibeats.py @@ -30,7 +30,6 @@ log.addHandler(handler) log.setLevel(logging.DEBUG) - def list_devices(): # List all audio input devices p = pyaudio.PyAudio() @@ -74,7 +73,8 @@ def send_blinken(hols, visdata, pieces=1, switchback=None, maxval=None, maxheight=None, - autorange=True): + autorange=True, + colorscheme='default'): """ Create a light pattern for a remote Holidays based on the values we receive. @@ -143,13 +143,8 @@ def send_blinken(hols, visdata, pieces=1, # Set the globe colour based on how far it # is from the maximum value - if (j/maxheight) < 0.3: - r, g, b = 0, 200, 0 # green - elif (j/maxheight) < 0.7: - r, g, b = 220, 220, 00 # yellow - else: - r, g, b = 240, 10, 10 # red - pass + (r, g, b) = get_val_color( j/maxheight, colorscheme ) + #log.debug("%d %d %d %d", globe_idx, r, g, b) holglobes[holid][globe_idx] = [r,g,b] pass @@ -174,6 +169,38 @@ def send_blinken(hols, visdata, pieces=1, pass pass +def get_val_color(val, scheme='default'): + """ + Return the globe color based on the scheme we're using. + + @param val: is a float value between 0.0 and 1.0 + """ + if scheme == 'default': + if val < 0.3: + r, g, b = 0, 200, 0 # green + elif val < 0.7: + r, g, b = 220, 220, 00 # yellow + else: + r, g, b = 240, 10, 10 # red + pass + + elif scheme == 'blue': + r, g, b = 0, 0, 30 + 225 * val + elif scheme == 'red': + r, g, b = 30 + 225 * val, 0, 0 + elif scheme == 'green': + r, g, b = 0, 30 + 225 * val, 0 + + elif scheme == 'yellow': + r, g, b = 30 + 225 * val, 30 + 225 * val, 0 + + pass + + r = int(r) + g = int(g) + b = int(b) + return (r, g, b) + class HolibeatOptions(optparse.OptionParser): """ Command-line options parser @@ -192,6 +219,11 @@ def addOptions(self): help="Port number to start at for UDP listeners [%default]", type="int", default=9988) + self.add_option('-c', '--colorscheme', dest='colorscheme', + help=" [%default]", + type="choice", choices = ['default', 'blue', 'green', 'red', 'yellow'], + default='default') + self.add_option('-b', '--buckets', dest='numbuckets', help="Number of frequency bands (buckets) for display", type="int") @@ -224,6 +256,7 @@ def addOptions(self): self.add_option('', '--quietseconds', dest='quiet_seconds', help="Period of relative quiet to reset autoranging [%default]", type="int", default=5) + def parseOptions(self): """ @@ -435,7 +468,8 @@ def chunks(l, n): switchback=options.switchback, maxval=maxval, maxheight=options.maxheight, - autorange=options.autorange) + autorange=options.autorange, + colorscheme=options.colorscheme) pass # Wait for next timetick From c05e62d6200b0ebd465e45a3f5328912cd571b1c Mon Sep 17 00:00:00 2001 From: Justin Warren Date: Mon, 23 Dec 2013 18:38:36 +1100 Subject: [PATCH 18/50] * Updates to twinkle.py to keep it bright and colorful. --- examples/soundlevel.py | 12 +++++++----- examples/twinkle.py | 21 ++++++++++++++------- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/examples/soundlevel.py b/examples/soundlevel.py index 529d245..e5b69c3 100755 --- a/examples/soundlevel.py +++ b/examples/soundlevel.py @@ -48,11 +48,13 @@ def render(hol, value): CHANNELS = 2 RATE = 44100 INPUT_BLOCK_TIME = 0.02 - BUFFER = 4096 #Seems to work... + BUFFER = 1024 #Seems to work... # How do we select the appropriate input device? - input_device = 0 #Built-in Microphone (seems good for OSX) + #input_device = 0 #Built-in Microphone (seems good for OSX) #input_device = 3 # this seems to be correct for juno + input_device = 15 # Ubuntu default + pa = pyaudio.PyAudio() @@ -60,10 +62,10 @@ def render(hol, value): channels = CHANNELS, rate = RATE, input = True, - input_device_index = input_device, + #input_device_index = input_device, frames_per_buffer = BUFFER) - SCALE = 100 # Probably need to tweak this + SCALE = 300 # Probably need to tweak this MAX_LIGHT = 50 errorcount = 0 print "Press Ctrl-C to quit" @@ -78,4 +80,4 @@ def render(hol, value): #print amplitude render(hol, min(amplitude / SCALE, MAX_LIGHT)) - \ No newline at end of file + diff --git a/examples/twinkle.py b/examples/twinkle.py index f74c5fc..bfc67df 100755 --- a/examples/twinkle.py +++ b/examples/twinkle.py @@ -80,10 +80,17 @@ def init_hol(hol): """ for globeidx in range(0, HolidaySecretAPI.NUM_GLOBES): color = [] - for i in range(0, 3): - color.append(random.randint(0, 130)) - pass + # red + color.append(random.randint(0, 130)) + #color.append(0) + # green + color.append(random.randint(0, 130)) + # blue + color.append(random.randint(0, 130)) + #color.append(0) + r, g, b = color + hol.setglobe(globeidx, r, g, b) pass hol.render() @@ -123,8 +130,8 @@ def twinkle_holiday(hol, satstep = random.random() * satstep_max if random.randint(0, 1): s += satstep - if s > 1.0: - s = 1.0 + if s > 0.8: + s = 0.8 else: s -= satstep # Make sure things stay bright and colorful! @@ -140,8 +147,8 @@ def twinkle_holiday(hol, v = 1.0 else: v -= valstep - if v < 0.0: - v = 0.0 + if v < 0.2: + v = 0.2 pass #log.debug("end h s v: %f %f %f", h, s, v) From 3fa8fe42c006dd68a65777c33090b50414f3cf13 Mon Sep 17 00:00:00 2001 From: Justin Warren Date: Tue, 24 Dec 2013 15:10:01 +1100 Subject: [PATCH 19/50] * Added REST API support to the simgame multi-Holiday simulator --- simgame/holiday.py | 60 ++++++++++++++++++++++++++++++++++++++++++---- simgame/simgame.py | 11 ++++++++- 2 files changed, 66 insertions(+), 5 deletions(-) diff --git a/simgame/holiday.py b/simgame/holiday.py index 26279cb..95aaf1f 100644 --- a/simgame/holiday.py +++ b/simgame/holiday.py @@ -4,6 +4,10 @@ import select, socket import array +from Queue import Empty +from multiprocessing import Process, Queue +from iotas import iotas + UDP_HEADER_LENGTH = 10 UDP_DATA_LENGTH = 150 UDP_MSG_LENGTH = UDP_HEADER_LENGTH + UDP_DATA_LENGTH @@ -30,17 +34,17 @@ def __init__(self, remote=False, addr='', if not remote: self.addr = addr - self.tcpport = tcpport + if udpport is None: bound_port = False - for port in range(9988, 10100): + for udpport in range(9988, 10100): try: - self.bind_udp(port) + self.bind_udp(udpport) bound_port = True break except socket.error, e: num, s = e - # Ignore Address already in use + # Try again if port is in use, else raise if num != 98: raise pass @@ -51,10 +55,27 @@ def __init__(self, remote=False, addr='', else: self.bind_udp(udpport) + print "UDP listening on (%s, %s)" % (self.addr, self.udpport) + + # Set up REST API on a TCP port + self.q = Queue() + + self.iop = Process(target=iotas.run, kwargs={ 'port': 8080, + 'queue': self.q }) + self.iop.start() else: raise NotImplementedError("Listening Simulator only. Does not send to remote devices.") + def exit(self): + """ + Force shutdown of processes + """ + self.iop.terminate() + + def __del__(self): + self.exit() + def bind_udp(self, udpport): """ Try to bind to a UDP port, retrying a range if one is already in use @@ -95,3 +116,34 @@ def recv_udp(self): self.globes[i] = [ coldata[0], coldata[1], coldata[2] ] pass + def recv_tcp(self): + """ + Receive data on the TCP port and process it + + Reception of data is via the Queue + """ + # Get all the data available, and only use the latest + # This will throw away all old data if the main loop is + # too slow, so we at least catch up. + data = None + while True: + try: + data = self.q.get(block=False) + + except Empty: + break + pass + + if data is not None: + # Data is a list of globe values encoded as + # 3 x 2-char hex values, one per line + globedata = data.split() + for i, vals in enumerate(globedata): + r = int(vals[:2], 16) + g = int(vals[2:4], 16) + b = int(vals[4:6], 16) + self.globes[i] = [ r, g, b ] + pass + pass + pass + diff --git a/simgame/simgame.py b/simgame/simgame.py index 5b6ec80..4bdf2ca 100755 --- a/simgame/simgame.py +++ b/simgame/simgame.py @@ -32,7 +32,11 @@ def addOptions(self): type="int", default=1) # Listen on multiple TCP/UDP ports, one for each Holiday we simulate - self.add_option('-p', '--portstart', dest='portstart', + self.add_option('-t', '--tcp-portstart', dest='tcp_portstart', + help="Port number to start at for REST API listeners [%default]", + type="int", default=8080) + + self.add_option('-u', '--udp-portstart', dest='udp_portstart', help="Port number to start at for UDP listeners [%default]", type="int", default=9988) @@ -218,6 +222,10 @@ def run(self): pygame.display.update() pass pygame.quit() + + for hol in self.HolidayList: + hol.exit() + pass pass def recv_data(self): @@ -226,6 +234,7 @@ def recv_data(self): """ for hol in self.HolidayList: hol.recv_udp() + hol.recv_tcp() pass def blank_strings(self): From 4fcf29d7110f5b14f8325750353eec5e049a4115 Mon Sep 17 00:00:00 2001 From: Justin Warren Date: Thu, 26 Dec 2013 10:48:19 +1100 Subject: [PATCH 20/50] * Added options to twinkle.py to set basecolor and max difference from baseline color for tighter control over colour drift etc. for more gentle twinkling. --- examples/twinkle.py | 98 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 78 insertions(+), 20 deletions(-) diff --git a/examples/twinkle.py b/examples/twinkle.py index bfc67df..fe99fe8 100755 --- a/examples/twinkle.py +++ b/examples/twinkle.py @@ -31,6 +31,10 @@ def addOptions(self): help="Number of Holiday strings to use [%default]", type="int", default=1) + self.add_option('-b', '--basecolor', dest='basecolor', + help="Color to initialize string to, as #nnnnnn", + type="string") + self.add_option('-c', '--change_chance', dest='change_chance', help="% chance a given globe will change each round [%default]", type="float", default=1.0 ) @@ -50,7 +54,19 @@ def addOptions(self): self.add_option('-V', '--VALSTEP', dest='valstep_max', help="Maximum step between values [%default]", type="float", default=0.2 ) - + + self.add_option('', '--huediff-max', dest='huediff_max', + help="Maximum hue difference from basecolor [%default]", + type="float", default=1.0 ) + + self.add_option('', '--satdiff-max', dest='satdiff_max', + help="Maximum saturation difference from basecolor [%default]", + type="float", default=1.0 ) + + self.add_option('', '--valdiff-max', dest='valdiff_max', + help="Maximum value difference from basecolor [%default]", + type="float", default=1.0 ) + # Send on multiple TCP/UDP ports, one for each Holiday we simulate self.add_option('-p', '--portstart', dest='portstart', help="Port number to start at for UDP listeners [%default]", @@ -74,38 +90,50 @@ def postOptions(self): pass pass -def init_hol(hol): +def init_hol(hol, basecolor=None): """ Initialize a Holiday to some random-ish colors """ - for globeidx in range(0, HolidaySecretAPI.NUM_GLOBES): - color = [] - # red - color.append(random.randint(0, 130)) - #color.append(0) - # green - color.append(random.randint(0, 130)) - # blue - color.append(random.randint(0, 130)) - #color.append(0) - - r, g, b = color - - hol.setglobe(globeidx, r, g, b) - pass + if basecolor is not None: + (r, g, b) = basecolor + hol.fill(r, g, b) + else: + for globeidx in range(0, HolidaySecretAPI.NUM_GLOBES): + color = [] + # red + color.append(random.randint(0, 130)) + #color.append(0) + # green + color.append(random.randint(0, 130)) + # blue + color.append(random.randint(0, 130)) + #color.append(0) + + r, g, b = color + + hol.setglobe(globeidx, r, g, b) + pass hol.render() def twinkle_holiday(hol, huestep_max=0.1, satstep_max=0.1, valstep_max=0.5, - change_chance=1.0 + huediff_max=1.0, + satdiff_max=1.0, + valdiff_max=1.0, + change_chance=1.0, + basecolor=None ): """ Make a Holiday twinkle like the stars """ # For each globe, mostly have a random drift of brightness # and hue by but occasionally jump in brightness up or down + if basecolor: + (base_r, base_g, base_b) = basecolor + base_hsv = colorsys.rgb_to_hsv(base_r/255.0, base_g/255.0, base_b/255.0) + for idx in range(0, HolidaySecretAPI.NUM_GLOBES): # % chance of updating a given globe @@ -119,10 +147,15 @@ def twinkle_holiday(hol, # 50% chance of positive or negative step if random.randint(0, 1): h += huestep + if basecolor and abs(base_hsv[0] - h) > huediff_max: + h = base_hsv[0] + huediff_max if h > 1.0: h = h - 1.0 else: h -= huestep + if basecolor and abs(h - base_hsv[0]) > huediff_max: + h = base_hsv[0] - huediff_max + if h < 0.0: h = 1.0 + h pass @@ -130,10 +163,16 @@ def twinkle_holiday(hol, satstep = random.random() * satstep_max if random.randint(0, 1): s += satstep + if basecolor and abs(base_hsv[1] - s) > satdiff_max: + s = base_hsv[1] + satdiff_max + if s > 0.8: s = 0.8 else: s -= satstep + if basecolor and abs(s - base_hsv[1]) > satdiff_max: + s = base_hsv[1] - satdiff_max + # Make sure things stay bright and colorful! if s < 0.2: s = 0.2 @@ -143,10 +182,16 @@ def twinkle_holiday(hol, # 50% chance of positive or negative step if random.randint(0, 1): v += valstep + if basecolor and abs(base_hsv[2] - v) > valdiff_max: + v = base_hsv[2] + valdiff_max + if v > 1.0: v = 1.0 else: v -= valstep + if basecolor and abs(v - base_hsv[2]) > valdiff_max: + v = base_hsv[2] - valdiff_max + if v < 0.2: v = 0.2 pass @@ -166,6 +211,15 @@ def twinkle_holiday(hol, optparse = TwinkleOptions(usage=usage) options, args = optparse.parseOptions() + + if options.basecolor is not None: + bc = options.basecolor.lstrip('#') + r = int(bc[:2], 16) + g = int(bc[2:4], 16) + b = int(bc[4:6], 16) + basecolor = (r, g, b) + else: + basecolor = None hols = [] if len(args) > 1: @@ -179,7 +233,7 @@ def twinkle_holiday(hol, pass for hol in hols: - init_hol(hol) + init_hol(hol, basecolor) pass while True: @@ -188,7 +242,11 @@ def twinkle_holiday(hol, options.huestep_max, options.satstep_max, options.valstep_max, - options.change_chance) + options.huediff_max, + options.satdiff_max, + options.valdiff_max, + options.change_chance, + basecolor) pass time.sleep(options.anim_sleep) From 1d58fbab588b23c6896740dc064585948c02c79f Mon Sep 17 00:00:00 2001 From: Justin Warren Date: Thu, 26 Dec 2013 10:54:47 +1100 Subject: [PATCH 21/50] * Moved twinkle saturation limits to full range now that options for limits exist. --- examples/twinkle.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/twinkle.py b/examples/twinkle.py index fe99fe8..9df254d 100755 --- a/examples/twinkle.py +++ b/examples/twinkle.py @@ -166,16 +166,16 @@ def twinkle_holiday(hol, if basecolor and abs(base_hsv[1] - s) > satdiff_max: s = base_hsv[1] + satdiff_max - if s > 0.8: - s = 0.8 + if s > 1.0: + s = 1.0 else: s -= satstep if basecolor and abs(s - base_hsv[1]) > satdiff_max: s = base_hsv[1] - satdiff_max # Make sure things stay bright and colorful! - if s < 0.2: - s = 0.2 + if s < 0.0: + s = 0.0 # Adjust value by a random amount valstep = random.random() * valstep_max From f900653163419740a860cdf67ed63ee094638351 Mon Sep 17 00:00:00 2001 From: Justin Warren Date: Thu, 26 Dec 2013 11:57:28 +1100 Subject: [PATCH 22/50] * Added ability to set initial light pattern to twinkle.py via JSON file. * Added chase mode, both forwards and backwards to twinkle.py --- examples/twinkle.py | 65 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 52 insertions(+), 13 deletions(-) diff --git a/examples/twinkle.py b/examples/twinkle.py index 9df254d..25bf26d 100755 --- a/examples/twinkle.py +++ b/examples/twinkle.py @@ -8,6 +8,9 @@ import sys import random import colorsys +import webcolors + +import json from secretapi.holidaysecretapi import HolidaySecretAPI @@ -67,11 +70,18 @@ def addOptions(self): help="Maximum value difference from basecolor [%default]", type="float", default=1.0 ) - # Send on multiple TCP/UDP ports, one for each Holiday we simulate - self.add_option('-p', '--portstart', dest='portstart', - help="Port number to start at for UDP listeners [%default]", - type="int", default=9988) + self.add_option('-f', '--patternfile', dest='patternfile', + help="Initalise string with a pattern from a JSON format file", + type="string") + + self.add_option('', '--chase', dest='chase', + help="Lights chase around the string", + action="store_true", default=False) + self.add_option('', '--chase-direction', dest='chase_direction', + help="Set direction of chase, if chase enabled [%default]", + type="choice", choices=['forward', 'backward'], default='forward') + def parseOptions(self): """ Emulate twistedmatrix options parser API @@ -88,15 +98,27 @@ def postOptions(self): if len(self.args) < 1: self.error("Specify address and port of remote Holiday(s)") pass + + self.options.initpattern = None + if self.options.patternfile: + with open(self.options.patternfile) as fp: + jdata = json.load(fp) + self.options.initpattern = jdata['lights'] + pass pass -def init_hol(hol, basecolor=None): +def init_hol(hol, basecolor=None, pattern=None): """ Initialize a Holiday to some random-ish colors """ if basecolor is not None: (r, g, b) = basecolor hol.fill(r, g, b) + elif pattern is not None: + for globeidx, vals in enumerate(pattern): + (r, g, b) = webcolors.hex_to_rgb(vals) + hol.setglobe(globeidx, r, g, b) + pass else: for globeidx in range(0, HolidaySecretAPI.NUM_GLOBES): color = [] @@ -123,7 +145,9 @@ def twinkle_holiday(hol, satdiff_max=1.0, valdiff_max=1.0, change_chance=1.0, - basecolor=None + basecolor=None, + chase=False, + chase_direction='forward', ): """ Make a Holiday twinkle like the stars @@ -203,6 +227,23 @@ def twinkle_holiday(hol, hol.setglobe(idx, int(255*r), int(255*g), int(255*b)) pass pass + + # Chase mode? + if chase: + if chase_direction == 'forward': + # Rotate all globes around by one place + oldglobes = hol.globes[:] + hol.globes = oldglobes[1:] + hol.globes.append(oldglobes[0]) + pass + else: + log.debug("old: %s", hol.globes) + oldglobes = hol.globes[:] + hol.globes = oldglobes[:-1] + hol.globes.insert(0, oldglobes[-1]) + log.debug("new: %s", hol.globes) + pass + hol.render() if __name__ == '__main__': @@ -213,11 +254,7 @@ def twinkle_holiday(hol, options, args = optparse.parseOptions() if options.basecolor is not None: - bc = options.basecolor.lstrip('#') - r = int(bc[:2], 16) - g = int(bc[2:4], 16) - b = int(bc[4:6], 16) - basecolor = (r, g, b) + basecolor = webcolors.hex_to_rgb(options.basecolor) else: basecolor = None @@ -233,7 +270,7 @@ def twinkle_holiday(hol, pass for hol in hols: - init_hol(hol, basecolor) + init_hol(hol, basecolor, options.initpattern) pass while True: @@ -246,7 +283,9 @@ def twinkle_holiday(hol, options.satdiff_max, options.valdiff_max, options.change_chance, - basecolor) + basecolor, + options.chase, + options.chase_direction) pass time.sleep(options.anim_sleep) From 63b4080ef8d9b2a28d488b62719f76575b422432 Mon Sep 17 00:00:00 2001 From: Justin Warren Date: Thu, 26 Dec 2013 11:58:25 +1100 Subject: [PATCH 23/50] * Added example twinkle.py Xmas themed JSON light patterns --- examples/xmas.json | 10 ++++++++++ examples/xmas2.json | 19 +++++++++++++++++++ examples/xmas3.json | 26 ++++++++++++++++++++++++++ examples/xmas4.json | 32 ++++++++++++++++++++++++++++++++ 4 files changed, 87 insertions(+) create mode 100644 examples/xmas.json create mode 100644 examples/xmas2.json create mode 100644 examples/xmas3.json create mode 100644 examples/xmas4.json diff --git a/examples/xmas.json b/examples/xmas.json new file mode 100644 index 0000000..ed3ac49 --- /dev/null +++ b/examples/xmas.json @@ -0,0 +1,10 @@ +{ "lights": [ "#FF0000", "#00FF00", "#FFFFFF", "#FF0000", "#00FF00", +"#FFFFFF", "#FF0000", "#00FF00", "#FFFFFF", "#FF0000", +"#00FF00", "#FFFFFF", "#FF0000", "#00FF00", "#FFFFFF", +"#FF0000", "#00FF00", "#FFFFFF", "#FF0000", "#00FF00", +"#FFFFFF", "#FF0000", "#00FF00", "#FFFFFF", "#FF0000", +"#00FF00", "#FFFFFF", "#FF0000", "#00FF00", "#FFFFFF", +"#FF0000", "#00FF00", "#FFFFFF", "#FF0000", "#00FF00", +"#FFFFFF", "#FF0000", "#00FF00", "#FFFFFF", "#FF0000", +"#00FF00", "#FFFFFF", "#FF0000", "#00FF00", "#FFFFFF", +"#FF0000", "#00FF00", "#FFFFFF", "#FF0000", "#00FF00" ] } diff --git a/examples/xmas2.json b/examples/xmas2.json new file mode 100644 index 0000000..8521bb6 --- /dev/null +++ b/examples/xmas2.json @@ -0,0 +1,19 @@ +{ "lights": [ +"#FF0000", "#FF0000", "#FF0000", +"#00FF00", "#00FF00", "#00FF00", +"#FFFFFF", "#FFFFFF", "#FFFFFF", +"#FF0000", "#FF0000", "#FF0000", +"#00FF00", "#00FF00", "#00FF00", +"#FFFFFF", "#FFFFFF", "#FFFFFF", +"#FF0000", "#FF0000", "#FF0000", +"#00FF00", "#00FF00", "#00FF00", +"#FFFFFF", "#FFFFFF", "#FFFFFF", +"#FF0000", "#FF0000", "#FF0000", +"#00FF00", "#00FF00", "#00FF00", +"#FFFFFF", "#FFFFFF", "#FFFFFF", +"#FF0000", "#FF0000", "#FF0000", +"#00FF00", "#00FF00", "#00FF00", +"#FFFFFF", "#FFFFFF", "#FFFFFF", +"#FF0000", "#FF0000", "#FF0000", +"#00FF00", "#00FF00" + ] } diff --git a/examples/xmas3.json b/examples/xmas3.json new file mode 100644 index 0000000..8309261 --- /dev/null +++ b/examples/xmas3.json @@ -0,0 +1,26 @@ +{ "lights": [ +"#FF0000", "#FF0000", "#FF0000", +"#FF0000", "#FF0000", +"#00FF00", "#00FF00", "#00FF00", +"#00FF00", "#00FF00", +"#FFFFFF", "#FFFFFF", "#FFFFFF", +"#FFFFFF", "#FFFFFF", + +"#FF0000", "#FF0000", "#FF0000", +"#FF0000", "#FF0000", +"#00FF00", "#00FF00", "#00FF00", +"#00FF00", "#00FF00", +"#FFFFFF", "#FFFFFF", "#FFFFFF", +"#FFFFFF", "#FFFFFF", + +"#FF0000", "#FF0000", "#FF0000", +"#FF0000", "#FF0000", +"#00FF00", "#00FF00", "#00FF00", +"#00FF00", "#00FF00", +"#FFFFFF", "#FFFFFF", "#FFFFFF", +"#FFFFFF", "#FFFFFF", + +"#FF0000", "#FF0000", "#FF0000", +"#00FF00", "#00FF00" + + ] } diff --git a/examples/xmas4.json b/examples/xmas4.json new file mode 100644 index 0000000..2cd3b11 --- /dev/null +++ b/examples/xmas4.json @@ -0,0 +1,32 @@ +{ "lights": [ +"#FF0000", "#00FF00", +"#FF0000", "#00FF00", +"#FF0000", "#00FF00", +"#FF0000", "#00FF00", +"#FF0000", "#00FF00", + +"#FF0000", "#00FF00", +"#FF0000", "#00FF00", +"#FF0000", "#00FF00", +"#FF0000", "#00FF00", +"#FF0000", "#00FF00", + +"#FF0000", "#00FF00", +"#FF0000", "#00FF00", +"#FF0000", "#00FF00", +"#FF0000", "#00FF00", +"#FF0000", "#00FF00", + +"#FF0000", "#00FF00", +"#FF0000", "#00FF00", +"#FF0000", "#00FF00", +"#FF0000", "#00FF00", +"#FF0000", "#00FF00", + +"#FF0000", "#00FF00", +"#FF0000", "#00FF00", +"#FF0000", "#00FF00", +"#FF0000", "#00FF00", +"#FF0000", "#00FF00" + + ] } From a2b667b6e890dd0fc160d638e17656cabd5a25a7 Mon Sep 17 00:00:00 2001 From: Justin Warren Date: Fri, 27 Dec 2013 09:23:17 +1100 Subject: [PATCH 24/50] * Twinkle now supports simplex noise twinkling, which looks a bit more natural than random HSV changes. * Updated base Holiday to use tuples internally instead of lists, because platonic colours are immutable. Lists had side-effects of when using shallow copies via slice notation [:]. Simplifies the code, too. --- examples/holiday.py | 25 ++--- examples/twinkle.py | 228 ++++++++++++++++++++++++-------------------- 2 files changed, 136 insertions(+), 117 deletions(-) diff --git a/examples/holiday.py b/examples/holiday.py index 988d614..40b76a6 100644 --- a/examples/holiday.py +++ b/examples/holiday.py @@ -31,17 +31,11 @@ class Holiday: NUM_GLOBES = 50 # Storage for all 50 globe values - # - globes = [ [ 0x00, 0x00, 0x00], [ 0x00, 0x00, 0x00], [ 0x00, 0x00, 0x00], [ 0x00, 0x00, 0x00], [ 0x00, 0x00, 0x00], - [ 0x00, 0x00, 0x00], [ 0x00, 0x00, 0x00], [ 0x00, 0x00, 0x00], [ 0x00, 0x00, 0x00], [ 0x00, 0x00, 0x00], - [ 0x00, 0x00, 0x00], [ 0x00, 0x00, 0x00], [ 0x00, 0x00, 0x00], [ 0x00, 0x00, 0x00], [ 0x00, 0x00, 0x00], - [ 0x00, 0x00, 0x00], [ 0x00, 0x00, 0x00], [ 0x00, 0x00, 0x00], [ 0x00, 0x00, 0x00], [ 0x00, 0x00, 0x00], - [ 0x00, 0x00, 0x00], [ 0x00, 0x00, 0x00], [ 0x00, 0x00, 0x00], [ 0x00, 0x00, 0x00], [ 0x00, 0x00, 0x00], - [ 0x00, 0x00, 0x00], [ 0x00, 0x00, 0x00], [ 0x00, 0x00, 0x00], [ 0x00, 0x00, 0x00], [ 0x00, 0x00, 0x00], - [ 0x00, 0x00, 0x00], [ 0x00, 0x00, 0x00], [ 0x00, 0x00, 0x00], [ 0x00, 0x00, 0x00], [ 0x00, 0x00, 0x00], - [ 0x00, 0x00, 0x00], [ 0x00, 0x00, 0x00], [ 0x00, 0x00, 0x00], [ 0x00, 0x00, 0x00], [ 0x00, 0x00, 0x00], - [ 0x00, 0x00, 0x00], [ 0x00, 0x00, 0x00], [ 0x00, 0x00, 0x00], [ 0x00, 0x00, 0x00], [ 0x00, 0x00, 0x00], - [ 0x00, 0x00, 0x00], [ 0x00, 0x00, 0x00], [ 0x00, 0x00, 0x00], [ 0x00, 0x00, 0x00], [ 0x00, 0x00, 0x00] ] + # Use a list of tuples, because tuples are immutable, as are specific colours. + # If you want a globe to display a new colour, you need to provide a new tuple + # for that colour, rather than changing the colour 'blue' to 'red' + + globes = [ (0,0,0), ] * NUM_GLOBES def __init__(self, remote=False, addr=''): """If remote, you better supply a valid address. @@ -57,16 +51,15 @@ def __init__(self, remote=False, addr=''): def setglobe(self, globenum, r, g, b): """Set a globe""" if (globenum < 0) or (globenum >= self.NUM_GLOBES): - return - self.globes[globenum][0] = r - self.globes[globenum][1] = g - self.globes[globenum][2] = b + # Fail hard, don't ignore errors + raise IndexError("globenum %d does not exist", globenum) + return self.globes[globenum] def getglobe(self, globenum): """Return a tuple representing a globe's RGB color value""" if (globenum < 0) or (globenum >= self.NUM_GLOBES): return False - return (self.globes[globenum][0], self.globes[globenum][1], self.globes[globenum][2]) + return self.globes[globenum] def set_pattern(self, pattern): """ diff --git a/examples/twinkle.py b/examples/twinkle.py index 25bf26d..d9f1071 100755 --- a/examples/twinkle.py +++ b/examples/twinkle.py @@ -9,9 +9,10 @@ import random import colorsys import webcolors - import json +from simplexnoise import raw_noise_2d + from secretapi.holidaysecretapi import HolidaySecretAPI import logging @@ -46,6 +47,14 @@ def addOptions(self): help="Sleep between animation frames, in seconds [%default]", type="float", default=0.1 ) + self.add_option('-f', '--patternfile', dest='patternfile', + help="Initalise string with a pattern from a JSON format file", + type="string") + + self.add_option('-t', '--twinkle-algo', dest='twinkle_algo', + help="Algorithm to use for twinkling [%default]", + type="choice", choices=['random', 'simplex'], default='random') + self.add_option('-H', '--HUESTEP', dest='huestep_max', help="Maximum step between hues [%default]", type="float", default=0.1 ) @@ -70,10 +79,6 @@ def addOptions(self): help="Maximum value difference from basecolor [%default]", type="float", default=1.0 ) - self.add_option('-f', '--patternfile', dest='patternfile', - help="Initalise string with a pattern from a JSON format file", - type="string") - self.add_option('', '--chase', dest='chase', help="Lights chase around the string", action="store_true", default=False) @@ -112,7 +117,7 @@ def init_hol(hol, basecolor=None, pattern=None): Initialize a Holiday to some random-ish colors """ if basecolor is not None: - (r, g, b) = basecolor + (r, g, b) = webcolors.hex_to_rgb(basecolor) hol.fill(r, g, b) elif pattern is not None: for globeidx, vals in enumerate(pattern): @@ -136,101 +141,130 @@ def init_hol(hol, basecolor=None, pattern=None): hol.setglobe(globeidx, r, g, b) pass hol.render() + + pattern = hol.globes[:] + return pattern -def twinkle_holiday(hol, - huestep_max=0.1, - satstep_max=0.1, - valstep_max=0.5, - huediff_max=1.0, - satdiff_max=1.0, - valdiff_max=1.0, - change_chance=1.0, - basecolor=None, - chase=False, - chase_direction='forward', - ): +def twinkle_holiday(hol, options, init_pattern, noise_array=None): """ Make a Holiday twinkle like the stars """ # For each globe, mostly have a random drift of brightness # and hue by but occasionally jump in brightness up or down - if basecolor: - (base_r, base_g, base_b) = basecolor + if options.basecolor: + (base_r, base_g, base_b) = webcolors.hex_to_rgb(options.basecolor) base_hsv = colorsys.rgb_to_hsv(base_r/255.0, base_g/255.0, base_b/255.0) + pass - for idx in range(0, HolidaySecretAPI.NUM_GLOBES): - - # % chance of updating a given globe - if random.random() < change_chance: + if noise_array is None: + noise_array = [ 0, ] * HolidaySecretAPI.NUM_GLOBES + pass - r, g, b = hol.getglobe(idx) - (h, s, v) = colorsys.rgb_to_hsv(r/255.0, g/255.0, b/255.0) - #log.debug("start h s v: %f %f %f", h, s, v) - # Adjust hue by a random amount - huestep = random.random() * huestep_max - # 50% chance of positive or negative step - if random.randint(0, 1): - h += huestep - if basecolor and abs(base_hsv[0] - h) > huediff_max: - h = base_hsv[0] + huediff_max - if h > 1.0: - h = h - 1.0 - else: - h -= huestep - if basecolor and abs(h - base_hsv[0]) > huediff_max: - h = base_hsv[0] - huediff_max + for idx in range(0, HolidaySecretAPI.NUM_GLOBES): - if h < 0.0: - h = 1.0 + h + # Choose globe update algorithm + if options.twinkle_algo == 'simplex': + nv = raw_noise_2d(noise_array[idx], random.random()) / 5.0 + noise_array[idx] += nv + if noise_array[idx] > 1.0: + noise_array[idx] = 1.0 + pass + elif noise_array[idx] < -1.0: + noise_array[idx] = -1.0 pass - satstep = random.random() * satstep_max - if random.randint(0, 1): - s += satstep - if basecolor and abs(base_hsv[1] - s) > satdiff_max: - s = base_hsv[1] + satdiff_max + ranger = (noise_array[idx] + 1.0) / 2.0 - if s > 1.0: - s = 1.0 - else: - s -= satstep - if basecolor and abs(s - base_hsv[1]) > satdiff_max: - s = base_hsv[1] - satdiff_max - - # Make sure things stay bright and colorful! - if s < 0.0: - s = 0.0 - - # Adjust value by a random amount - valstep = random.random() * valstep_max - # 50% chance of positive or negative step - if random.randint(0, 1): - v += valstep - if basecolor and abs(base_hsv[2] - v) > valdiff_max: - v = base_hsv[2] + valdiff_max - - if v > 1.0: - v = 1.0 + # Adjust colour. If basecolor, adjust from basecolor + if options.basecolor: + (base_r, base_g, base_b) = webcolors.hex_to_rgb(options.basecolor) + r = int(base_r * ranger) + g = int(base_g * ranger) + b = int(base_b * ranger) + pass else: - v -= valstep - if basecolor and abs(v - base_hsv[2]) > valdiff_max: - v = base_hsv[2] - valdiff_max - - if v < 0.2: - v = 0.2 + # adjust from original color + (base_r, base_g, base_b) = init_pattern[idx] + #log.debug("adjusting from orig: %d %d %d", base_r, base_g, base_b) + r = int(base_r * ranger) + g = int(base_g * ranger) + b = int(base_b * ranger) + pass + hol.setglobe(idx, r, g, b) + + else: + # % chance of updating a given globe + if random.random() < options.change_chance: + + r, g, b = hol.getglobe(idx) + (h, s, v) = colorsys.rgb_to_hsv(r/255.0, g/255.0, b/255.0) + #log.debug("start h s v: %f %f %f", h, s, v) + # Adjust hue by a random amount + huestep = random.random() * options.huestep_max + # 50% chance of positive or negative step + if random.randint(0, 1): + h += huestep + if options.basecolor and abs(base_hsv[0] - h) > options.huediff_max: + h = base_hsv[0] + options.huediff_max + if h > 1.0: + h = h - 1.0 + else: + h -= huestep + if options.basecolor and abs(h - base_hsv[0]) > options.huediff_max: + h = base_hsv[0] - options.huediff_max + + if h < 0.0: + h = 1.0 + h + pass + + satstep = random.random() * options.satstep_max + if random.randint(0, 1): + s += satstep + if options.basecolor and abs(base_hsv[1] - s) > options.satdiff_max: + s = base_hsv[1] + options.satdiff_max + + if s > 1.0: + s = 1.0 + else: + s -= satstep + if options.basecolor and abs(s - base_hsv[1]) > options.satdiff_max: + s = base_hsv[1] - options.satdiff_max + + # Make sure things stay bright and colorful! + if s < 0.0: + s = 0.0 + + # Adjust value by a random amount + valstep = random.random() * options.valstep_max + # 50% chance of positive or negative step + if random.randint(0, 1): + v += valstep + if options.basecolor and abs(base_hsv[2] - v) > options.valdiff_max: + v = base_hsv[2] + options.valdiff_max + + if v > 1.0: + v = 1.0 + else: + v -= valstep + if options.basecolor and abs(v - base_hsv[2]) > options.valdiff_max: + v = base_hsv[2] - options.valdiff_max + + if v < 0.2: + v = 0.2 + pass + + #log.debug("end h s v: %f %f %f", h, s, v) + + (r, g, b) = colorsys.hsv_to_rgb(h, s, v) + #log.debug("r g b: %f %f %f", r, g, b) + hol.setglobe(idx, int(255*r), int(255*g), int(255*b)) pass - - #log.debug("end h s v: %f %f %f", h, s, v) - - (r, g, b) = colorsys.hsv_to_rgb(h, s, v) - #log.debug("r g b: %f %f %f", r, g, b) - hol.setglobe(idx, int(255*r), int(255*g), int(255*b)) pass pass # Chase mode? - if chase: - if chase_direction == 'forward': + if options.chase: + if options.chase_direction == 'forward': # Rotate all globes around by one place oldglobes = hol.globes[:] hol.globes = oldglobes[1:] @@ -253,12 +287,12 @@ def twinkle_holiday(hol, options, args = optparse.parseOptions() - if options.basecolor is not None: - basecolor = webcolors.hex_to_rgb(options.basecolor) - else: - basecolor = None - hols = [] + # List of holiday initial patterns + hol_inits = [] + + # List of holiday noise patterns + hol_noise = [] if len(args) > 1: for arg in args: hol_addr, hol_port = arg.split(':') @@ -269,23 +303,15 @@ def twinkle_holiday(hol, hols.append(HolidaySecretAPI(addr=hol_addr, port=int(hol_port)+i)) pass + # Initialise holidays for hol in hols: - init_hol(hol, basecolor, options.initpattern) + hol_inits.append(init_hol(hol, options.basecolor, options.initpattern)) + hol_noise.append(None) pass - + while True: - for hol in hols: - twinkle_holiday(hol, - options.huestep_max, - options.satstep_max, - options.valstep_max, - options.huediff_max, - options.satdiff_max, - options.valdiff_max, - options.change_chance, - basecolor, - options.chase, - options.chase_direction) + for i, hol in enumerate(hols): + noise = twinkle_holiday(hol, options, hol_inits[i], hol_noise[i]) pass time.sleep(options.anim_sleep) From a35474ff188daa08ac3659127c784db773b4d5e7 Mon Sep 17 00:00:00 2001 From: Justin Warren Date: Fri, 27 Dec 2013 09:25:40 +1100 Subject: [PATCH 25/50] * Added missing simplexnoise.py file. --- examples/simplexnoise.py | 576 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 576 insertions(+) create mode 100644 examples/simplexnoise.py diff --git a/examples/simplexnoise.py b/examples/simplexnoise.py new file mode 100644 index 0000000..231b783 --- /dev/null +++ b/examples/simplexnoise.py @@ -0,0 +1,576 @@ +# Copyright (c) 2012 Eliot Eshelman +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +############################################################################### + +"""2D, 3D and 4D Simplex Noise functions return 'random' values in (-1, 1). + +This algorithm was originally designed by Ken Perlin, but my code has been +adapted from the implementation written by Stefan Gustavson (stegu@itn.liu.se) + +Raw Simplex noise functions return the value generated by Ken's algorithm. + +Scaled Raw Simplex noise functions adjust the range of values returned from the +traditional (-1, 1) to whichever bounds are passed to the function. + +Multi-Octave Simplex noise functions compine multiple noise values to create a +more complex result. Each successive layer of noise is adjusted and scaled. + +Scaled Multi-Octave Simplex noise functions scale the values returned from the +traditional (-1,1) range to whichever range is passed to the function. + +In many cases, you may think you only need a 1D noise function, but in practice +2D is almost always better. For instance, if you're using the current frame +number as the parameter for the noise, all objects will end up with the same +noise value at each frame. By adding a second parameter on the second +dimension, you can ensure that each gets a unique noise value and they don't +all look identical. +""" + +import math + +def octave_noise_2d(octaves, persistence, scale, x, y): + """2D Multi-Octave Simplex noise. + + For each octave, a higher frequency/lower amplitude function will be added + to the original. The higher the persistence [0-1], the more of each + succeeding octave will be added. + """ + total = 0.0 + frequency = scale + amplitude = 1.0 + + # We have to keep track of the largest possible amplitude, + # because each octave adds more, and we need a value in [-1, 1]. + maxAmplitude = 0.0; + + for i in range(octaves): + total += raw_noise_2d(x * frequency, y * frequency) * amplitude + frequency *= 2.0 + maxAmplitude += amplitude; + amplitude *= persistence + + return total / maxAmplitude + +def octave_noise_3d(octaves, persistence, scale, x, y, z): + """3D Multi-Octave Simplex noise. + + For each octave, a higher frequency/lower amplitude function will be added + to the original. The higher the persistence [0-1], the more of each + succeeding octave will be added. + """ + total = 0.0 + frequency = scale + amplitude = 1.0 + + # We have to keep track of the largest possible amplitude, + # because each octave adds more, and we need a value in [-1, 1]. + maxAmplitude = 0.0; + + for i in range(octaves): + total += raw_noise_3d( x * frequency, + y * frequency, + z * frequency) * amplitude + frequency *= 2.0 + maxAmplitude += amplitude; + amplitude *= persistence + + return total / maxAmplitude + +def octave_noise_4d(octaves, persistence, scale, x, y, z, w): + """4D Multi-Octave Simplex noise. + + For each octave, a higher frequency/lower amplitude function will be added + to the original. The higher the persistence [0-1], the more of each + succeeding octave will be added. + """ + total = 0.0 + frequency = scale + amplitude = 1.0 + + # We have to keep track of the largest possible amplitude, + # because each octave adds more, and we need a value in [-1, 1]. + maxAmplitude = 0.0; + + for i in range(octaves): + total += raw_noise_4d( x * frequency, + y * frequency, + z * frequency, + w * frequency) * amplitude + frequency *= 2.0 + maxAmplitude += amplitude; + amplitude *= persistence + + return total / maxAmplitude + +def scaled_octave_noise_2d(octaves, persistence, scale, loBound, hiBound, x, y): + """2D Scaled Multi-Octave Simplex noise. + + Returned value will be between loBound and hiBound. + """ + return (octave_noise_2d(octaves, persistence, scale, x, y) * + (hiBound - loBound) / 2 + + (hiBound + loBound) / 2) + +def scaled_octave_noise_3d(octaves, persistence, scale, loBound, hiBound, x, y, z): + """3D Scaled Multi-Octave Simplex noise. + + Returned value will be between loBound and hiBound. + """ + return (octave_noise_3d(octaves, persistence, scale, x, y, z) * + (hiBound - loBound) / 2 + + (hiBound + loBound) / 2) + +def scaled_octave_noise_4d(octaves, persistence, scale, loBound, hiBound, x, y, z, w): + """4D Scaled Multi-Octave Simplex noise. + + Returned value will be between loBound and hiBound. + """ + return (octave_noise_4d(octaves, persistence, scale, x, y, z, w) * + (hiBound - loBound) / 2 + + (hiBound + loBound) / 2) + +def scaled_raw_noise_2d(loBound, hiBound, x, y): + """2D Scaled Raw Simplex noise. + + Returned value will be between loBound and hiBound. + """ + return (raw_noise_2d(x, y) * + (hiBound - loBound) / 2+ + (hiBound + loBound) / 2) + +def scaled_raw_noise_3d(loBound, hiBound, x, y, z): + """3D Scaled Raw Simplex noise. + + Returned value will be between loBound and hiBound. + """ + return (raw_noise_3d(x, y, z) * + (hiBound - loBound) / 2+ + (hiBound + loBound) / 2) + +def scaled_raw_noise_4d(loBound, hiBound, x, y, z, w): + """4D Scaled Raw Simplex noise. + + Returned value will be between loBound and hiBound. + """ + return (raw_noise_4d(x, y, z, w) * + (hiBound - loBound) / 2+ + (hiBound + loBound) / 2) + +def raw_noise_2d(x, y): + """2D Raw Simplex noise.""" + # Noise contributions from the three corners + n0, n1, n2 = 0.0, 0.0, 0.0 + + # Skew the input space to determine which simplex cell we're in + F2 = 0.5 * (math.sqrt(3.0) - 1.0) + # Hairy skew factor for 2D + s = (x + y) * F2 + i = int(x + s) + j = int(y + s) + + G2 = (3.0 - math.sqrt(3.0)) / 6.0 + t = float(i + j) * G2 + # Unskew the cell origin back to (x,y) space + X0 = i - t + Y0 = j - t + # The x,y distances from the cell origin + x0 = x - X0 + y0 = y - Y0 + + # For the 2D case, the simplex shape is an equilateral triangle. + # Determine which simplex we are in. + i1, j1 = 0, 0 # Offsets for second (middle) corner of simplex in (i,j) coords + if x0 > y0: # lower triangle, XY order: (0,0)->(1,0)->(1,1) + i1 = 1 + j1 = 0 + else: # upper triangle, YX order: (0,0)->(0,1)->(1,1) + i1 = 0 + j1 = 1 + + # A step of (1,0) in (i,j) means a step of (1-c,-c) in (x,y), and + # a step of (0,1) in (i,j) means a step of (-c,1-c) in (x,y), where + # c = (3-sqrt(3))/6 + x1 = x0 - i1 + G2 # Offsets for middle corner in (x,y) unskewed coords + y1 = y0 - j1 + G2 + x2 = x0 - 1.0 + 2.0 * G2 # Offsets for last corner in (x,y) unskewed coords + y2 = y0 - 1.0 + 2.0 * G2 + + # Work out the hashed gradient indices of the three simplex corners + ii = int(i) & 255 + jj = int(j) & 255 + gi0 = _perm[ii+_perm[jj]] % 12 + gi1 = _perm[ii+i1+_perm[jj+j1]] % 12 + gi2 = _perm[ii+1+_perm[jj+1]] % 12 + + # Calculate the contribution from the three corners + t0 = 0.5 - x0*x0 - y0*y0 + if t0 < 0: + n0 = 0.0 + else: + t0 *= t0 + n0 = t0 * t0 * dot2d(_grad3[gi0], x0, y0) + + t1 = 0.5 - x1*x1 - y1*y1 + if t1 < 0: + n1 = 0.0 + else: + t1 *= t1 + n1 = t1 * t1 * dot2d(_grad3[gi1], x1, y1) + + t2 = 0.5 - x2*x2-y2*y2 + if t2 < 0: + n2 = 0.0 + else: + t2 *= t2 + n2 = t2 * t2 * dot2d(_grad3[gi2], x2, y2) + + # Add contributions from each corner to get the final noise value. + # The result is scaled to return values in the interval [-1,1]. + return 70.0 * (n0 + n1 + n2) + +def raw_noise_3d(x, y, z): + """3D Raw Simplex noise.""" + # Noise contributions from the four corners + n0, n1, n2, n3 = 0.0, 0.0, 0.0, 0.0 + + # Skew the input space to determine which simplex cell we're in + F3 = 1.0/3.0 + # Very nice and simple skew factor for 3D + s = (x+y+z) * F3 + i = int(x + s) + j = int(y + s) + k = int(z + s) + + G3 = 1.0 / 6.0 + t = float(i+j+k) * G3 + # Unskew the cell origin back to (x,y,z) space + X0 = i - t + Y0 = j - t + Z0 = k - t + # The x,y,z distances from the cell origin + x0 = x - X0 + y0 = y - Y0 + z0 = z - Z0 + + # For the 3D case, the simplex shape is a slightly irregular tetrahedron. + # Determine which simplex we are in. + i1, j1, k1 = 0,0,0 # Offsets for second corner of simplex in (i,j,k) coords + i2, j2, k2 = 0,0,0 # Offsets for third corner of simplex in (i,j,k) coords + + if x0 >= y0: + if y0 >= z0: # X Y Z order + i1 = 1 + j1 = 0 + k1 = 0 + i2 = 1 + j2 = 1 + k2 = 0 + elif x0 >= z0: # X Z Y order + i1 = 1 + j1 = 0 + k1 = 0 + i2 = 1 + j2 = 0 + k2 = 1 + else: # Z X Y order + i1 = 0 + j1 = 0 + k1 = 1 + i2 = 1 + j2 = 0 + k2 = 1 + else: + if y0 < z0: # Z Y X order + i1 = 0 + j1 = 0 + k1 = 1 + i2 = 0 + j2 = 1 + k2 = 1 + elif x0 < z0: # Y Z X order + i1 = 0 + j1 = 1 + k1 = 0 + i2 = 0 + j2 = 1 + k2 = 1 + else: # Y X Z order + i1 = 0 + j1 = 1 + k1 = 0 + i2 = 1 + j2 = 1 + k2 = 0 + + # A step of (1,0,0) in (i,j,k) means a step of (1-c,-c,-c) in (x,y,z), + # a step of (0,1,0) in (i,j,k) means a step of (-c,1-c,-c) in (x,y,z), and + # a step of (0,0,1) in (i,j,k) means a step of (-c,-c,1-c) in (x,y,z), where + # c = 1/6. + x1 = x0 - i1 + G3 # Offsets for second corner in (x,y,z) coords + y1 = y0 - j1 + G3 + z1 = z0 - k1 + G3 + x2 = x0 - i2 + 2.0*G3 # Offsets for third corner in (x,y,z) coords + y2 = y0 - j2 + 2.0*G3 + z2 = z0 - k2 + 2.0*G3 + x3 = x0 - 1.0 + 3.0*G3 # Offsets for last corner in (x,y,z) coords + y3 = y0 - 1.0 + 3.0*G3 + z3 = z0 - 1.0 + 3.0*G3 + + # Work out the hashed gradient indices of the four simplex corners + ii = int(i) & 255 + jj = int(j) & 255 + kk = int(k) & 255 + gi0 = _perm[ii+_perm[jj+_perm[kk]]] % 12 + gi1 = _perm[ii+i1+_perm[jj+j1+_perm[kk+k1]]] % 12 + gi2 = _perm[ii+i2+_perm[jj+j2+_perm[kk+k2]]] % 12 + gi3 = _perm[ii+1+_perm[jj+1+_perm[kk+1]]] % 12 + + # Calculate the contribution from the four corners + t0 = 0.6 - x0*x0 - y0*y0 - z0*z0 + if t0 < 0: + n0 = 0.0 + else: + t0 *= t0 + n0 = t0 * t0 * dot3d(_grad3[gi0], x0, y0, z0) + + t1 = 0.6 - x1*x1 - y1*y1 - z1*z1 + if t1 < 0: + n1 = 0.0 + else: + t1 *= t1 + n1 = t1 * t1 * dot3d(_grad3[gi1], x1, y1, z1) + + t2 = 0.6 - x2*x2 - y2*y2 - z2*z2 + if t2 < 0: + n2 = 0.0 + else: + t2 *= t2 + n2 = t2 * t2 * dot3d(_grad3[gi2], x2, y2, z2) + + t3 = 0.6 - x3*x3 - y3*y3 - z3*z3 + if t3 < 0: + n3 = 0.0 + else: + t3 *= t3 + n3 = t3 * t3 * dot3d(_grad3[gi3], x3, y3, z3) + + # Add contributions from each corner to get the final noise value. + # The result is scaled to stay just inside [-1,1] + return 32.0 * (n0 + n1 + n2 + n3) + +def raw_noise_4d(x, y, z, w): + """4D Raw Simplex noise.""" + # Noise contributions from the five corners + n0, n1, n2, n3, n4 = 0.0, 0.0, 0.0, 0.0, 0.0 + + # The skewing and unskewing factors are hairy again for the 4D case + F4 = (math.sqrt(5.0)-1.0) / 4.0 + # Skew the (x,y,z,w) space to determine which cell of 24 simplices we're in + s = (x + y + z + w) * F4 + i = int(x + s) + j = int(y + s) + k = int(z + s) + l = int(w + s) + + G4 = (5.0-math.sqrt(5.0)) / 20.0 + t = (i + j + k + l) * G4 + # Unskew the cell origin back to (x,y,z,w) space + X0 = i - t + Y0 = j - t + Z0 = k - t + W0 = l - t + # The x,y,z,w distances from the cell origin + x0 = x - X0 + y0 = y - Y0 + z0 = z - Z0 + w0 = w - W0 + + # For the 4D case, the simplex is a 4D shape I won't even try to describe. + # To find out which of the 24 possible simplices we're in, we need to + # determine the magnitude ordering of x0, y0, z0 and w0. + # The method below is a good way of finding the ordering of x,y,z,w and + # then find the correct traversal order for the simplex we're in. + # First, six pair-wise comparisons are performed between each possible pair + # of the four coordinates, and the results are used to add up binary bits + # for an integer index. + c1 = 32 if x0 > y0 else 0 + c2 = 16 if x0 > z0 else 0 + c3 = 8 if y0 > z0 else 0 + c4 = 4 if x0 > w0 else 0 + c5 = 2 if y0 > w0 else 0 + c6 = 1 if z0 > w0 else 0 + c = c1 + c2 + c3 + c4 + c5 + c6 + + i1, j1, k1, l1 = 0,0,0,0 # The integer offsets for the second simplex corner + i2, j2, k2, l2 = 0,0,0,0 # The integer offsets for the third simplex corner + i3, j3, k3, l3 = 0,0,0,0 # The integer offsets for the fourth simplex corner + + # simplex[c] is a 4-vector with the numbers 0, 1, 2 and 3 in some order. + # Many values of c will never occur, since e.g. x>y>z>w makes x= 3 else 0 + j1 = 1 if _simplex[c][1] >= 3 else 0 + k1 = 1 if _simplex[c][2] >= 3 else 0 + l1 = 1 if _simplex[c][3] >= 3 else 0 + # The number 2 in the "simplex" array is at the second largest coordinate. + i2 = 1 if _simplex[c][0] >= 2 else 0 + j2 = 1 if _simplex[c][1] >= 2 else 0 + k2 = 1 if _simplex[c][2] >= 2 else 0 + l2 = 1 if _simplex[c][3] >= 2 else 0 + # The number 1 in the "simplex" array is at the second smallest coordinate. + i3 = 1 if _simplex[c][0] >= 1 else 0 + j3 = 1 if _simplex[c][1] >= 1 else 0 + k3 = 1 if _simplex[c][2] >= 1 else 0 + l3 = 1 if _simplex[c][3] >= 1 else 0 + # The fifth corner has all coordinate offsets = 1, so no need to look that up. + x1 = x0 - i1 + G4 # Offsets for second corner in (x,y,z,w) coords + y1 = y0 - j1 + G4 + z1 = z0 - k1 + G4 + w1 = w0 - l1 + G4 + x2 = x0 - i2 + 2.0*G4 # Offsets for third corner in (x,y,z,w) coords + y2 = y0 - j2 + 2.0*G4 + z2 = z0 - k2 + 2.0*G4 + w2 = w0 - l2 + 2.0*G4 + x3 = x0 - i3 + 3.0*G4 # Offsets for fourth corner in (x,y,z,w) coords + y3 = y0 - j3 + 3.0*G4 + z3 = z0 - k3 + 3.0*G4 + w3 = w0 - l3 + 3.0*G4 + x4 = x0 - 1.0 + 4.0*G4 # Offsets for last corner in (x,y,z,w) coords + y4 = y0 - 1.0 + 4.0*G4 + z4 = z0 - 1.0 + 4.0*G4 + w4 = w0 - 1.0 + 4.0*G4 + + # Work out the hashed gradient indices of the five simplex corners + ii = int(i) & 255 + jj = int(j) & 255 + kk = int(k) & 255 + ll = int(l) & 255 + gi0 = _perm[ii+_perm[jj+_perm[kk+_perm[ll]]]] % 32 + gi1 = _perm[ii+i1+_perm[jj+j1+_perm[kk+k1+_perm[ll+l1]]]] % 32 + gi2 = _perm[ii+i2+_perm[jj+j2+_perm[kk+k2+_perm[ll+l2]]]] % 32 + gi3 = _perm[ii+i3+_perm[jj+j3+_perm[kk+k3+_perm[ll+l3]]]] % 32 + gi4 = _perm[ii+1+_perm[jj+1+_perm[kk+1+_perm[ll+1]]]] % 32 + + # Calculate the contribution from the five corners + t0 = 0.6 - x0*x0 - y0*y0 - z0*z0 - w0*w0 + if t0 < 0: + n0 = 0.0 + else: + t0 *= t0 + n0 = t0 * t0 * dot4d(_grad4[gi0], x0, y0, z0, w0) + + t1 = 0.6 - x1*x1 - y1*y1 - z1*z1 - w1*w1 + if t1 < 0: + n1 = 0.0 + else: + t1 *= t1 + n1 = t1 * t1 * dot4d(_grad4[gi1], x1, y1, z1, w1) + + t2 = 0.6 - x2*x2 - y2*y2 - z2*z2 - w2*w2 + if t2 < 0: + n2 = 0.0 + else: + t2 *= t2 + n2 = t2 * t2 * dot4d(_grad4[gi2], x2, y2, z2, w2) + + t3 = 0.6 - x3*x3 - y3*y3 - z3*z3 - w3*w3 + if t3 < 0: + n3 = 0.0 + else: + t3 *= t3 + n3 = t3 * t3 * dot4d(_grad4[gi3], x3, y3, z3, w3) + + t4 = 0.6 - x4*x4 - y4*y4 - z4*z4 - w4*w4 + if t4 < 0: + n4 = 0.0 + else: + t4 *= t4 + n4 = t4 * t4 * dot4d(_grad4[gi4], x4, y4, z4, w4) + + # Sum up and scale the result to cover the range [-1,1] + return 27.0 * (n0 + n1 + n2 + n3 + n4) + + +def dot2d(g, x, y): + return g[0]*x + g[1]*y + +def dot3d(g, x, y, z): + return g[0]*x + g[1]*y + g[2]*z + +def dot4d(g, x, y, z, w): + return g[0]*x + g[1]*y + g[2]*z + g[3]*w + + +"""The gradients are the midpoints of the vertices of a cube.""" +_grad3 = [ + [1,1,0], [-1,1,0], [1,-1,0], [-1,-1,0], + [1,0,1], [-1,0,1], [1,0,-1], [-1,0,-1], + [0,1,1], [0,-1,1], [0,1,-1], [0,-1,-1] +] + +"""The gradients are the midpoints of the vertices of a cube.""" +_grad4 = [ + [0,1,1,1], [0,1,1,-1], [0,1,-1,1], [0,1,-1,-1], + [0,-1,1,1], [0,-1,1,-1], [0,-1,-1,1], [0,-1,-1,-1], + [1,0,1,1], [1,0,1,-1], [1,0,-1,1], [1,0,-1,-1], + [-1,0,1,1], [-1,0,1,-1], [-1,0,-1,1], [-1,0,-1,-1], + [1,1,0,1], [1,1,0,-1], [1,-1,0,1], [1,-1,0,-1], + [-1,1,0,1], [-1,1,0,-1], [-1,-1,0,1], [-1,-1,0,-1], + [1,1,1,0], [1,1,-1,0], [1,-1,1,0], [1,-1,-1,0], + [-1,1,1,0], [-1,1,-1,0], [-1,-1,1,0], [-1,-1,-1,0] +] + +"""Permutation table. The same list is repeated twice.""" +_perm = [ + 151,160,137,91,90,15,131,13,201,95,96,53,194,233,7,225,140,36,103,30,69,142, + 8,99,37,240,21,10,23,190,6,148,247,120,234,75,0,26,197,62,94,252,219,203,117, + 35,11,32,57,177,33,88,237,149,56,87,174,20,125,136,171,168,68,175,74,165,71, + 134,139,48,27,166,77,146,158,231,83,111,229,122,60,211,133,230,220,105,92,41, + 55,46,245,40,244,102,143,54,65,25,63,161,1,216,80,73,209,76,132,187,208,89, + 18,169,200,196,135,130,116,188,159,86,164,100,109,198,173,186,3,64,52,217,226, + 250,124,123,5,202,38,147,118,126,255,82,85,212,207,206,59,227,47,16,58,17,182, + 189,28,42,223,183,170,213,119,248,152,2,44,154,163,70,221,153,101,155,167,43, + 172,9,129,22,39,253,19,98,108,110,79,113,224,232,178,185,112,104,218,246,97, + 228,251,34,242,193,238,210,144,12,191,179,162,241,81,51,145,235,249,14,239, + 107,49,192,214,31,181,199,106,157,184,84,204,176,115,121,50,45,127,4,150,254, + 138,236,205,93,222,114,67,29,24,72,243,141,128,195,78,66,215,61,156,180, + + 151,160,137,91,90,15,131,13,201,95,96,53,194,233,7,225,140,36,103,30,69,142, + 8,99,37,240,21,10,23,190,6,148,247,120,234,75,0,26,197,62,94,252,219,203,117, + 35,11,32,57,177,33,88,237,149,56,87,174,20,125,136,171,168,68,175,74,165,71, + 134,139,48,27,166,77,146,158,231,83,111,229,122,60,211,133,230,220,105,92,41, + 55,46,245,40,244,102,143,54,65,25,63,161,1,216,80,73,209,76,132,187,208,89, + 18,169,200,196,135,130,116,188,159,86,164,100,109,198,173,186,3,64,52,217,226, + 250,124,123,5,202,38,147,118,126,255,82,85,212,207,206,59,227,47,16,58,17,182, + 189,28,42,223,183,170,213,119,248,152,2,44,154,163,70,221,153,101,155,167,43, + 172,9,129,22,39,253,19,98,108,110,79,113,224,232,178,185,112,104,218,246,97, + 228,251,34,242,193,238,210,144,12,191,179,162,241,81,51,145,235,249,14,239, + 107,49,192,214,31,181,199,106,157,184,84,204,176,115,121,50,45,127,4,150,254, + 138,236,205,93,222,114,67,29,24,72,243,141,128,195,78,66,215,61,156,180 +] + +"""A lookup table to traverse the simplex around a given point in 4D.""" +_simplex = [ + [0,1,2,3],[0,1,3,2],[0,0,0,0],[0,2,3,1],[0,0,0,0],[0,0,0,0],[0,0,0,0],[1,2,3,0], + [0,2,1,3],[0,0,0,0],[0,3,1,2],[0,3,2,1],[0,0,0,0],[0,0,0,0],[0,0,0,0],[1,3,2,0], + [0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0], + [1,2,0,3],[0,0,0,0],[1,3,0,2],[0,0,0,0],[0,0,0,0],[0,0,0,0],[2,3,0,1],[2,3,1,0], + [1,0,2,3],[1,0,3,2],[0,0,0,0],[0,0,0,0],[0,0,0,0],[2,0,3,1],[0,0,0,0],[2,1,3,0], + [0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0], + [2,0,1,3],[0,0,0,0],[0,0,0,0],[0,0,0,0],[3,0,1,2],[3,0,2,1],[0,0,0,0],[3,1,2,0], + [2,1,0,3],[0,0,0,0],[0,0,0,0],[0,0,0,0],[3,1,0,2],[0,0,0,0],[3,2,0,1],[3,2,1,0] +] From 68afd9a20c5afe7a3af535def4b2f2e3477b0381 Mon Sep 17 00:00:00 2001 From: Justin Warren Date: Fri, 27 Dec 2013 09:37:01 +1100 Subject: [PATCH 26/50] * Added option to twinkle.py to control amount of simplex noise, for greater or lesser 'twinkling'. --- examples/twinkle.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/examples/twinkle.py b/examples/twinkle.py index d9f1071..845cb42 100755 --- a/examples/twinkle.py +++ b/examples/twinkle.py @@ -86,6 +86,10 @@ def addOptions(self): self.add_option('', '--chase-direction', dest='chase_direction', help="Set direction of chase, if chase enabled [%default]", type="choice", choices=['forward', 'backward'], default='forward') + + self.add_option('', '--simplex-damper', dest='simplex_damper', + help="Amount of simplex noise dampening [%default]", + type="float", default=5.0) def parseOptions(self): """ @@ -164,7 +168,7 @@ def twinkle_holiday(hol, options, init_pattern, noise_array=None): # Choose globe update algorithm if options.twinkle_algo == 'simplex': - nv = raw_noise_2d(noise_array[idx], random.random()) / 5.0 + nv = raw_noise_2d(noise_array[idx], random.random()) / options.simplex_damper noise_array[idx] += nv if noise_array[idx] > 1.0: noise_array[idx] = 1.0 From 349f7bb3de44e04a80e02efe2e822aa98b07d1d1 Mon Sep 17 00:00:00 2001 From: Justin Warren Date: Fri, 27 Dec 2013 15:15:16 +1100 Subject: [PATCH 27/50] * Added switchback mode to phloston. * Added ability to define colors on the commandline * Added ability to reverse direction of the animation * Added ability to control speed with -a flag, same as other examples * Added Virtual Holiday class to abstract the switchback methods somewhat * Added ability to define the 'gap' between switchbacks * Added ability to disable reversing direction of switchbacks --- examples/phloston.py | 312 +++++++++++++++++++++++++++++++------------ 1 file changed, 230 insertions(+), 82 deletions(-) diff --git a/examples/phloston.py b/examples/phloston.py index 07df302..ec2bf9a 100755 --- a/examples/phloston.py +++ b/examples/phloston.py @@ -14,10 +14,14 @@ import time import logging import optparse +import math +import webcolors import holiday from secretapi.holidaysecretapi import HolidaySecretAPI +NUM_GLOBES = holiday.Holiday.NUM_GLOBES + # Simulator default address SIM_ADDR = "localhost:8080" @@ -40,20 +44,30 @@ def addOptions(self): help="Number of Holiday strings to use [%default]", type="int", default=1) - # Comms mode, TCP or UDP - self.add_option('-m', '--mode', dest='mode', - help="Communications mode, UDP or TCP [%default]", - type="choice", choices=['udp', 'tcp'], default='tcp') - - # Port to start at if we use multiple Holidays - self.add_option('-p', '--portstart', dest='portstart', - help="Port number to start at for strings [%default]", - type="int", default=8080) + self.add_option('-a', '--anim-sleep', dest='anim_sleep', + help="Sleep time between animation frames", + type="float", default=0.1) - self.add_option('-f', '--fps', dest='fps', - help="Frames per second, used to slow down data sending", - type="int", default=30) + self.add_option('-c', '--color', dest='colorset', + help="Color of the string(s)", + action='append', default=[]) + self.add_option('', '--reverse', dest='forwards', + help="Reverse direction of animation", + action="store_false", default=True) + + self.add_option('', '--switchback', dest='switchback', + help="'Switchback' strings, make a single string display like it's " + "more than one every m globes", + type="int") + + self.add_option('', '--sb-gap', dest='sb_gap', + help="Have a gap of n globes between each switchback", + type="int", default=0) + + self.add_option('', '--disable-swapdir', dest='disable_swapdir', + help="Disables swapping direction with each switchback", + action="store_true", default=False) def parseOptions(self): """ @@ -80,64 +94,161 @@ def postOptions(self): # strings we have, and overrides the -n setting if self.options.numstrings < len(self.args): self.options.numstrings = len(self.args) + + if self.options.colorset: + colorset = [ webcolors.hex_to_rgb(x) for x in self.options.colorset ] + self.options.colorset = colorset + +class vHoliday(object): + """ + A 'virtual' Holiday, used for implementing switchback mode and + other things on n real Holidays. + + A virtual Holiday can be shorter than a real Holiday, or exactly + as long; it cannot (for now) be longer than a real one. + + FIXME: Enable a virtual Holiday to be longer than a real Holiday + so we could span Holidays if we wanted to. + """ + def __init__(self, hols=None, start=0, length=holiday.Holiday.NUM_GLOBES, direction=True): + """ + Each parameter is a list of one or more physical Holidays + + @param addrlist: a list of (ipaddr, port, mode) tuples specifying + how to communicate with a Holiday (mode is TCP or UDP) + @param start: The starting offset for this virtual holiday + @param length: the length of this virtual Holiday + @param direction: If True, move from low to high idx, if False, from high to low idx + """ + if hols is None: + self.hols = [] + self.hols.append(HolidaySecretAPI('localhost', 9988)) + else: + self.hols = hols + pass + + self.start = start + if length > holiday.Holiday.NUM_GLOBES: + raise ValueError("Virtual Holidays cannot be longer than real ones.") + self.length = length + + self.direction = direction + + def map_globe_idx(self, srcidx): + """ + Map the 'virtual' globe index onto the 'real' holiday and globe index + """ + # positive direction + if self.direction: + dstidx = self.start + srcidx + else: + dstidx = self.start + self.length - srcidx - 1 + pass + + # Check we haven't gone off the end of our 'virtual' string + if abs(dstidx - self.start) > self.length: + raise ValueError("globe %d not valid on this vHoliday" % srcidx) + + holid = 0 + return (holid, dstidx) + + # Implement the standard Holiday API for the virtual Holiday + def getglobe(self, globenum): + holid, idx = self.map_globe_idx(globenum) + return self.hols[holid].getglobe(idx) + + def setglobe(self, globenum, color): + holid, idx = self.map_globe_idx(globenum) + #log.debug("set globe: %d %s [ %d, %d ]", globenum, color, holid, idx) + res = self.hols[holid].setglobe(idx, color[0], color[1], color[2]) + + def set_pattern(self, pattern): + for globenum, color in enumerate(pattern): + holid, idx = self.map_globe_idx(globenum) + self.hols[holid].setglobe(idx, color[0], color[1], color[2]) + pass pass -class PhlostonString(object): + def fill(self, color): + for hol in self.hols: + hol.fill(color[0], color[1], color[2]) + pass + pass + + def chase(self, direction=True): + raise NotImplementedError + + def rotate(self, direction=True): + raise NotImplementedError + + def render(self): + """ + Render all physical Holidays mapped to this virtual Holiday + """ + for hol in self.hols: + hol.render() + +class PhlostonString(vHoliday): """ A PhlostonString is a Holiday turned into a Phloston Paradise visual alarm light. Turns a Holiday into Phloston Paradise hotel evacuation lighting, from the movie The Fifth Element. """ - - def __init__(self, addr, - color=(0xff, 0xff, 0xff), - mode='tcp', - delay=0.02): + def __init__(self, addrlist=None, start=0, + length=holiday.Holiday.NUM_GLOBES, direction=True, + color=None, pattern=None): """ - Controls a single Holiday at addr - - @param addr: Address of the remote Holiday IoTAS controller - @param color: The color of the lights - @param delay: Time between lighting each globe + @param color: An (r,g,b) tuple of the lights colour + @param pattern: An optional pattern of light colours to use """ - if mode == 'tcp': - self.hol = holiday.Holiday(addr=addr, - remote=True) - elif mode == 'udp': - addr, port = addr.split(':') - self.hol = HolidaySecretAPI(addr, int(port)) - - self.color = color - - self.numlit = 0 + super(PhlostonString, self).__init__(addrlist, start, length, direction) - self.base_pattern = [ - (0x00, 0x00, 0x00), - ] * self.hol.NUM_GLOBES + if pattern is not None: + self.pattern = pattern + elif color is not None: + self.pattern = [ color, ] * length + else: + self.pattern = [ (0xaa, 0x00, 0x00), ] * length + pass - # Make a copy so we don't clobber the original - self.globe_pattern = self.base_pattern[:] + self.numlit = 0 - def animate(self): + def animate(self, forwards=True): # Animation sequence is to start blank, then light each # globe in sequence until all are lit, then start again. - self.numlit += 1 - if self.numlit > self.hol.NUM_GLOBES: - self.numlit = 0 - self.globe_pattern = self.base_pattern[:] - pass + # Run animation 'forwards' + if forwards: + # Light the globes that are lit + for i in range(0, self.numlit): + #log.debug("lit: %d", i) + self.setglobe(i, self.pattern[i]) + pass + + # Blank those that are not lit + for i in range(self.numlit, self.length+1): + #log.debug("unlit: %d", i) + self.setglobe(i, (0x00, 0x00, 0x00)) + pass else: - for i in range(self.numlit): - self.globe_pattern[i] = self.color + # Light the globes that are lit + for i in range(0, self.numlit): + #log.debug("lit: %d", i) + self.setglobe(self.length-i-1, self.pattern[i]) pass - #log.debug("Pattern is: %s", self.globe_pattern) - self.hol.set_pattern(self.globe_pattern) - self.hol.render() + + # Blank those that are not lit + for i in range(self.numlit, self.length+1): + #log.debug("unlit: %d", i) + self.setglobe(self.length-i-1, (0x00, 0x00, 0x00)) + pass + + self.numlit += 1 + if self.numlit > self.length: + self.numlit = 0 pass - pass - + if __name__ == '__main__': usage = "Usage: %prog [options] []" @@ -145,46 +256,83 @@ def animate(self): options, args = optparse.parseOptions() - phlostons = [] - - colorset = [ - (0x33, 0x88, 0x33), - (0x88, 0x33, 0x33), - (0x00, 0x33, 0x88), - (0x88, 0x88, 0x33), - (0x33, 0x88, 0x88), - ] - if options.numstrings > len(colorset): - colorset = colorset * ( int(options.numstrings/len(colorset))+1) - pass - - for i in range(options.numstrings): - if options.numstrings > len(args): - addr, port = args[0].split(':') - port = int(port) + i - ps_addr = "%s:%s" % (addr, port) - pass - else: - ps_addr = args[i] + hols = [] + if len(args) > 1: + for arg in args: + hol_addr, hol_port = arg.split(':') + hols.append(HolidaySecretAPI(addr=hol_addr, port=int(hol_port))) + else: + hol_addr, hol_port = args[0].split(':') + for i in range(options.numstrings): + hols.append(HolidaySecretAPI(addr=hol_addr, port=int(hol_port)+i)) pass - ps = PhlostonString(ps_addr, - mode=options.mode, - color=colorset[i],) - phlostons.append(ps) + # Figure out how many 'virtual' strings we have + vhols = [] + + # Track how many lights are lit + litnums = [] + if options.switchback: + length = options.switchback + pieces = int(math.floor(float(NUM_GLOBES) / options.switchback)) + else: + length = NUM_GLOBES + pieces = 1 pass + + num_vhols = pieces * options.numstrings + + if options.colorset: + # Use the last defined color to make up the full set + # This allows us to define one color for all strings, + # or up to n of m total string colors + if len(options.colorset) < num_vhols: + lastcolor = options.colorset[-1] + for i in range( num_vhols - len(options.colorset) ): + options.colorset.append(lastcolor) + pass + pass + + for i in range(num_vhols): + # Use the same physical holiday for each chunk of pieces + holid = int(i / pieces) + if options.disable_swapdir: + direction = True + else: + direction = not (i % 2) - # Time limiting bits to slow down the UDP firehose - # This is stupid, but functional. Should be event driven, probably. - sleeptime = 1.0/options.fps + start = length * (i % pieces) + if i > 0: + start += options.sb_gap + pass + + #log.debug("holid: %d, direction: %d, start: %d", holid, direction, start) + if options.colorset: + vhol = PhlostonString( [hols[holid], ], + start, + length, + direction, + options.colorset[i]) + else: + vhol = PhlostonString( [hols[holid], ], + start, + length, + direction) + vhols.append(vhol) + litnums.append(0) + pass while True: - for i in range(options.numstrings): - phlostons[i].animate() + for i, hol in enumerate(vhols): + vhols[i].animate(options.forwards) pass + + for hol in hols: + hol.render() + # Wait for next timetick - time.sleep(sleeptime) + time.sleep(options.anim_sleep) pass pass From 7ebda8b9c038492ecc9e8c4b54140ca18ded9137 Mon Sep 17 00:00:00 2001 From: Justin Warren Date: Tue, 31 Dec 2013 09:54:23 +1100 Subject: [PATCH 28/50] * Abstracted the Holiday API into a separate package that uses inheritance to make it easier to use and maintain. --- api/base.py | 134 +++++++++++++++++++++++++++++++++++++++++++++ api/restholiday.py | 50 +++++++++++++++++ api/udpholiday.py | 55 +++++++++++++++++++ 3 files changed, 239 insertions(+) create mode 100644 api/base.py create mode 100644 api/restholiday.py create mode 100644 api/udpholiday.py diff --git a/api/base.py b/api/base.py new file mode 100644 index 0000000..a6e2ed6 --- /dev/null +++ b/api/base.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python +""" +Base API classes for Holiday interfaces + +Copyright (c) 2013 Justin Warren +License: MIT (see LICENSE for details) +""" + +__author__ = "Justin Warren" +__version__ = '0.02-dev' +__license__ = "MIT" + +import os +import logging + +NUM_GLOBES = holiday.Holiday.NUM_GLOBES + +# Set up logging +log = logging.getLogger('base') +handler = logging.StreamHandler() +handler.setFormatter(logging.Formatter("%(asctime)s: %(name)s [%(levelname)s]: %(message)s")) +log.addHandler(handler) +log.setLevel(logging.DEBUG) + +class HolidayBase(object): + """ + The base Holiday class for the main API + """ + NUM_GLOBES = 50 + + # Local representation of globe state + globes = [ (0,0,0), ] * NUM_GLOBES + + def setglobe(self, globenum, r, g, b): + # FIXME: This should be (self, globenum, color) where color is + # a tuple of (r g, b). + """Set a globe""" + if (globenum < 0) or (globenum >= self.NUM_GLOBES): + return + self.globes[globenum] = (r, g, b) + + def fill(self, r, g, b): + """Sets the whole string to a particular colour""" + self.globes = [ (int(r), int(g), int(b)), ] * self.NUM_GLOBES + #for e in self.globes: + # e[0] = int(r) + # e[1] = int(g) + # e[2] = int(b) + + def getglobe(self, globenum): + """Return a tuple representing a globe's RGB color value""" + if (globenum < 0) or (globenum >= self.NUM_GLOBES): + # Fail hard, don't ignore errors + raise IndexError("globenum %d does not exist", globenum) + return self.globes[globenum] + + def set_pattern(self, pattern): + """ + Set the entire string in one go + """ + if len(pattern) != self.NUM_GLOBES: + raise ValueError("pattern length incorrect: %d != %d" % ( len(pattern), self.NUM_GLOBES) ) + self.globes = pattern[:] + + def chase(self, direction="True"): + """Rotate all of the globes around - up if TRUE, down if FALSE""" + return + + def rotate(self, newr, newg, newb, direction="True", ): + """Rotate all of the globes up if TRUE, down if FALSE + Set the new start of the string to the color values""" + return + + def render(self): + raise NotImplementedError + +class ButtonHoliday(HolidayBase): + """ + Used when running on a physical Holiday. + """ + def __init__(self): + super(ButtonHoliday, self).__init__() + self.pid = os.getpid() + self.pipename = '/run/compose.fifo' + try: + self.pipe = open(self.pipename, "wb") + except: + print "Couldn't open the pipe! Oh no!" + self.pipe = None + pass + + def render(self): + """ + Render globe colours to local pipe + """ + rend = [] + rend.append("0x000010") + rend.append("0x%06x" % self.pid) + for g in self.globes: + tripval = (g[0] << 16) + (g[1] << 8) + g[2] + rend.append("0x%06x" % tripval) + pass + self.pipe.write('\n'.join(rend)) + self.pipe.flush() + +class ButtonApp(object): + """ + A ButtonApp runs on a physical Holiday using the button interface. + """ + + def start(self): + """ + Do whatever is required to start up the app + """ + return + + def stop(self): + """ + Do whatever is required to stop the app + """ + return + + def up(self): + """ + Called when the Up button is pressed + """ + return + + def down(self): + """ + Called when the Down button is pressed + """ + return + diff --git a/api/restholiday.py b/api/restholiday.py new file mode 100644 index 0000000..0a321db --- /dev/null +++ b/api/restholiday.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python +""" +REST interface classes for Holiday + +Copyright (c) 2013 Justin Warren +License: MIT (see LICENSE for details) +""" + +__author__ = "Justin Warren" +__version__ = '0.02-dev' +__license__ = "MIT" + +import requests +import json + +from base import HolidayBase + +import logging + +# Set up logging +log = logging.getLogger('rest_holiday') +handler = logging.StreamHandler() +handler.setFormatter(logging.Formatter("%(asctime)s: %(name)s [%(levelname)s]: %(message)s")) +log.addHandler(handler) +log.setLevel(logging.DEBUG) + +class RESTHoliday(HolidayBase): + """ + A remote Holiday we talk to over the JSON REST web interface + """ + def __init__(self, addr, scheme='http'): + """ + Initialise the REST Holiday remote address + @param addr: A string of the remote address of form : + """ + super(RESTHoliday, self).__init__() + self.scheme = scheme + self.addr = addr + + def render(self): + """ + Render globe values via JSON to remote Holiday via REST interface + """ + hol_vals = [ "#%02x%02x%02x" % (x[0], x[1], x[2]) for x in self.globes ] + hol_msg = { "lights": hol_vals } + msg_str = json.dumps(hol_msg) + url_str = "%s://%s/device/light/setlights" % (self.scheme, self.addr) + r = requests.put(urlstr, data=msg_str) + pass + diff --git a/api/udpholiday.py b/api/udpholiday.py new file mode 100644 index 0000000..28302bf --- /dev/null +++ b/api/udpholiday.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python +""" +UDP interface classes for Holiday + +Copyright (c) 2013 Justin Warren +License: MIT (see LICENSE for details) +""" + +__author__ = "Justin Warren" +__version__ = '0.02-dev' +__license__ = "MIT" + +import sys +import socket +import array + +from base import HolidayBase + +import logging + +# Set up logging +log = logging.getLogger('udp_holiday') +handler = logging.StreamHandler() +handler.setFormatter(logging.Formatter("%(asctime)s: %(name)s [%(levelname)s]: %(message)s")) +log.addHandler(handler) +log.setLevel(logging.DEBUG) + +class UDPHoliday(HolidayBase): + """ + A remote Holiday we talk to over the fast UDP + """ + def __init__(self, addr, port=9988): + """ + Initialise the REST Holiday remote address + @param addr: A string of the remote address of form : + """ + super(UDPHoliday, self).__init__() + self.ipaddr = ipaddr + self.port = port + self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + + def render(self): + """ + Render globe values via UDP to remote Holiday + """ + packet = array.array('B', [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) # initialize basic packet, ignore first 10 bytes + for g in self.globes: + packet.append(g[0]) + packet.append(g[1]) + packet.append(g[2]) + pass + # Send the packet to the Holiday + self.sock.sendto(packet, (self.addr, self.port)) + pass + From a9c8e2309ab24aa42ee8196304de1b4047296ed5 Mon Sep 17 00:00:00 2001 From: Justin Warren Date: Tue, 31 Dec 2013 16:06:01 +1100 Subject: [PATCH 29/50] * Fixed incorrect compose.fifo formatting in API * Fixed broken references to API in phloston * phlostonapp is working on real hardware! --- api/__init__.py | 0 api/base.py | 13 ++--- api/udpholiday.py | 4 +- examples/holiscreen.py | 2 +- examples/phloston.py | 46 ++++++++------- examples/phlostonapp.py | 121 ++++++++++++++++++++++++++++++++++++++++ 6 files changed, 155 insertions(+), 31 deletions(-) create mode 100644 api/__init__.py create mode 100755 examples/phlostonapp.py diff --git a/api/__init__.py b/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/base.py b/api/base.py index a6e2ed6..2452004 100644 --- a/api/base.py +++ b/api/base.py @@ -13,8 +13,6 @@ import os import logging -NUM_GLOBES = holiday.Holiday.NUM_GLOBES - # Set up logging log = logging.getLogger('base') handler = logging.StreamHandler() @@ -86,6 +84,7 @@ def __init__(self): self.pipe = open(self.pipename, "wb") except: print "Couldn't open the pipe! Oh no!" + raise self.pipe = None pass @@ -94,13 +93,13 @@ def render(self): Render globe colours to local pipe """ rend = [] - rend.append("0x000010") - rend.append("0x%06x" % self.pid) + rend.append("0x000010\n") + rend.append("0x%06x\n" % self.pid) for g in self.globes: - tripval = (g[0] << 16) + (g[1] << 8) + g[2] - rend.append("0x%06x" % tripval) + tripval = (g[0] * 65536) + (g[1] * 256) + g[2] + rend.append("0x%06X\n" % tripval) pass - self.pipe.write('\n'.join(rend)) + self.pipe.write(''.join(rend)) self.pipe.flush() class ButtonApp(object): diff --git a/api/udpholiday.py b/api/udpholiday.py index 28302bf..1747b96 100644 --- a/api/udpholiday.py +++ b/api/udpholiday.py @@ -29,7 +29,7 @@ class UDPHoliday(HolidayBase): """ A remote Holiday we talk to over the fast UDP """ - def __init__(self, addr, port=9988): + def __init__(self, ipaddr, port=9988): """ Initialise the REST Holiday remote address @param addr: A string of the remote address of form : @@ -50,6 +50,6 @@ def render(self): packet.append(g[2]) pass # Send the packet to the Holiday - self.sock.sendto(packet, (self.addr, self.port)) + self.sock.sendto(packet, (self.ipaddr, self.port)) pass diff --git a/examples/holiscreen.py b/examples/holiscreen.py index f2e9d81..94ce903 100755 --- a/examples/holiscreen.py +++ b/examples/holiscreen.py @@ -174,7 +174,7 @@ def render_to_hols(globelists, hols, width, height, # Your guess is as good as mine. holglobes = [] for i in range(len(hols)): - holglobes.append( [[0x00,0x00,0x00]] * HolidaySecretAPI.NUM_GLOBES ) + holglobes.append( [(0x00,0x00,0x00)] * HolidaySecretAPI.NUM_GLOBES ) pass if orientation == 'vertical': diff --git a/examples/phloston.py b/examples/phloston.py index ec2bf9a..190542f 100755 --- a/examples/phloston.py +++ b/examples/phloston.py @@ -15,22 +15,20 @@ import logging import optparse import math -import webcolors +import colorsys -import holiday -from secretapi.holidaysecretapi import HolidaySecretAPI +from api.base import HolidayBase +#from api.restholiday import RESTHoliday +from api.udpholiday import UDPHoliday -NUM_GLOBES = holiday.Holiday.NUM_GLOBES - -# Simulator default address -SIM_ADDR = "localhost:8080" +NUM_GLOBES = HolidayBase.NUM_GLOBES log = logging.getLogger(sys.argv[0]) handler = logging.StreamHandler() handler.setFormatter(logging.Formatter("%(asctime)s: %(name)s [%(levelname)s]: %(message)s")) log.addHandler(handler) log.setLevel(logging.DEBUG) - + class PhlostonOptions(optparse.OptionParser): """ Command-line options parser @@ -96,6 +94,7 @@ def postOptions(self): self.options.numstrings = len(self.args) if self.options.colorset: + import webcolors colorset = [ webcolors.hex_to_rgb(x) for x in self.options.colorset ] self.options.colorset = colorset @@ -110,25 +109,24 @@ class vHoliday(object): FIXME: Enable a virtual Holiday to be longer than a real Holiday so we could span Holidays if we wanted to. """ - def __init__(self, hols=None, start=0, length=holiday.Holiday.NUM_GLOBES, direction=True): + def __init__(self, hols=None, start=0, length=NUM_GLOBES, direction=True): """ Each parameter is a list of one or more physical Holidays - @param addrlist: a list of (ipaddr, port, mode) tuples specifying - how to communicate with a Holiday (mode is TCP or UDP) + @param hols: a list of Holiday objects to talk to @param start: The starting offset for this virtual holiday @param length: the length of this virtual Holiday @param direction: If True, move from low to high idx, if False, from high to low idx """ if hols is None: self.hols = [] - self.hols.append(HolidaySecretAPI('localhost', 9988)) + self.hols.append(UDPHoliday('localhost', 9988)) else: self.hols = hols pass self.start = start - if length > holiday.Holiday.NUM_GLOBES: + if length > NUM_GLOBES: raise ValueError("Virtual Holidays cannot be longer than real ones.") self.length = length @@ -195,14 +193,14 @@ class PhlostonString(vHoliday): Turns a Holiday into Phloston Paradise hotel evacuation lighting, from the movie The Fifth Element. """ - def __init__(self, addrlist=None, start=0, - length=holiday.Holiday.NUM_GLOBES, direction=True, + def __init__(self, hols=None, start=0, + length=NUM_GLOBES, direction=True, color=None, pattern=None): """ @param color: An (r,g,b) tuple of the lights colour @param pattern: An optional pattern of light colours to use """ - super(PhlostonString, self).__init__(addrlist, start, length, direction) + super(PhlostonString, self).__init__(hols, start, length, direction) if pattern is not None: self.pattern = pattern @@ -249,6 +247,15 @@ def animate(self, forwards=True): self.numlit = 0 pass + def set_pattern(self, pattern): + if len(pattern) > self.length: + self.pattern = pattern[:self.length] + elif len(pattern) < self.length: + # blank pad out short patterns + self.pattern = pattern + ( [(0,0,0),] * (self.length - len(self.pattern)) ) + else: + self.pattern = pattern + if __name__ == '__main__': usage = "Usage: %prog [options] []" @@ -260,18 +267,16 @@ def animate(self, forwards=True): if len(args) > 1: for arg in args: hol_addr, hol_port = arg.split(':') - hols.append(HolidaySecretAPI(addr=hol_addr, port=int(hol_port))) + hols.append(UDPHoliday(ipaddr=hol_addr, port=int(hol_port))) else: hol_addr, hol_port = args[0].split(':') for i in range(options.numstrings): - hols.append(HolidaySecretAPI(addr=hol_addr, port=int(hol_port)+i)) + hols.append(UDPHoliday(ipaddr=hol_addr, port=int(hol_port)+i)) pass # Figure out how many 'virtual' strings we have vhols = [] - # Track how many lights are lit - litnums = [] if options.switchback: length = options.switchback pieces = int(math.floor(float(NUM_GLOBES) / options.switchback)) @@ -320,7 +325,6 @@ def animate(self, forwards=True): length, direction) vhols.append(vhol) - litnums.append(0) pass while True: diff --git a/examples/phlostonapp.py b/examples/phlostonapp.py new file mode 100755 index 0000000..13ae25e --- /dev/null +++ b/examples/phlostonapp.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python +""" +Phloston Paradise Bomb-in-the-hotel style display for MooresCloud Holiday +ButtonApp version + +Copyright (c) 2013 Justin Warren +License: MIT (see LICENSE for details) +""" + +__author__ = "Justin Warren" +__version__ = '0.02-dev' +__license__ = "MIT" + +import time +import logging +import colorsys +import threading + +from api.base import ButtonHoliday, ButtonApp + +from phloston import PhlostonString + +log = logging.getLogger('phloston_app') +handler = logging.StreamHandler() +handler.setFormatter(logging.Formatter("%(asctime)s: %(name)s [%(levelname)s]: %(message)s")) +log.addHandler(handler) +log.setLevel(logging.DEBUG) + +# Amount of time between frames of animation +SNOOZETIME = 0.05 + +# Amount of hue to change when Up/Down button is pressed (0.0 - 1.0) +HUESTEP = 0.05 + +# What color to start as. (r,g,b) tuple +START_COLOR = (100,0,0) + +class PhlostonApp(ButtonHoliday, ButtonApp): + """ + An app for a physical Holiday + """ + def start(self): + self.t = PhlostonThread() + self.t.start() + + def stop(self): + self.t.terminate = True + + def change_hue(self, hdelta): + """ + Change the color hue by hdelta, a number between -1.0 and +0.1 + """ + (r,g,b) = self.t.get_color() + # Shift the colour hue by hdelta + (h,s,v) = colorsys.rgb_to_hsv(r/255.0, g/255.0, b/255.0) + h += hdelta + # Wraparound hue + if h < 0.0: + h = 1.0 + elif h > 1.0: + h = 0.0 + pass + + (r,g,b) = colorsys.hsv_to_rgb(h, s, v) + # Convert to 0-255 range integers + self.t.set_color( (int(r*255), int(g*255), int(b*255)) ) + + def up(self): + """ + Make the colour hue shift upwards + """ + self.change_hue(HUESTEP) + + def down(self): + self.change_hue(-HUESTEP) + +class PhlostonThread(threading.Thread): + """ + Singleton thread that runs the animation + """ + def run(self): + self.terminate = False + + # A Phloston string on 1 ButtonHoliday + self.phloston = PhlostonString([ButtonHoliday(),], color=START_COLOR) + + while True: + if self.terminate: + return + + # animate + self.phloston.animate() + self.phloston.render() + + # sleep + time.sleep(SNOOZETIME) + + pass + + def set_color(self, newcolor): + """ + Change the color of the string + """ + if not hasattr(self, 'phloston'): + return + self.phloston.set_pattern( [ newcolor, ] * self.phloston.length ) + + def get_color(self): + if not hasattr(self, 'phloston'): + return (0,0,0) + color = self.phloston.pattern[0] + return color + +if __name__ == '__main__': + app = PhlostonApp() + app.start() + #app.up() + #app.up() + time.sleep(5) + app.stop() + From 05c7fead1c7b6bebb032dea8bf9fc5c82f68a0a7 Mon Sep 17 00:00:00 2001 From: Justin Warren Date: Wed, 1 Jan 2014 09:10:36 +1100 Subject: [PATCH 30/50] * Fixed hue wraparound bug in phlostonapp --- examples/phlostonapp.py | 42 ++++++++++++++++++++++++++++++++++------- 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/examples/phlostonapp.py b/examples/phlostonapp.py index 13ae25e..0e2a1cb 100755 --- a/examples/phlostonapp.py +++ b/examples/phlostonapp.py @@ -56,14 +56,15 @@ def change_hue(self, hdelta): h += hdelta # Wraparound hue if h < 0.0: - h = 1.0 + h += 1.0 elif h > 1.0: - h = 0.0 + h -= 1.0 pass - + (r,g,b) = colorsys.hsv_to_rgb(h, s, v) # Convert to 0-255 range integers - self.t.set_color( (int(r*255), int(g*255), int(b*255)) ) + newcol = (int(r*255.0), int(g*255.0), int(b*255.0)) + self.t.set_color(newcol) def up(self): """ @@ -114,8 +115,35 @@ def get_color(self): if __name__ == '__main__': app = PhlostonApp() app.start() - #app.up() - #app.up() - time.sleep(5) + time.sleep(1) + + app.up() + time.sleep(0.1) + app.up() + + time.sleep(0.1) + app.down() + + time.sleep(0.1) + app.down() + + time.sleep(0.1) + app.down() + + time.sleep(0.1) + app.down() + + time.sleep(0.1) + app.down() + + time.sleep(0.1) + app.down() + + time.sleep(0.1) + app.down() + + + time.sleep(3) + app.stop() From 2fdb8835e95f67b6078e136007ff58211932f33b Mon Sep 17 00:00:00 2001 From: Justin Warren Date: Wed, 1 Jan 2014 13:37:53 +1100 Subject: [PATCH 31/50] * Tweaked led_scroller to work better on switchback'd Holidays --- examples/holiscreen.py | 23 +++++++------- examples/led_scroller.py | 67 +++++++++++++++++++++++++++++++--------- 2 files changed, 65 insertions(+), 25 deletions(-) diff --git a/examples/holiscreen.py b/examples/holiscreen.py index 94ce903..064a100 100755 --- a/examples/holiscreen.py +++ b/examples/holiscreen.py @@ -14,13 +14,14 @@ import sys import logging -from secretapi.holidaysecretapi import HolidaySecretAPI +from api.udpholiday import UDPHoliday +NUM_GLOBES = UDPHoliday.NUM_GLOBES -# Height of display == number of globes in a string -DEFAULT_HEIGHT = HolidaySecretAPI.NUM_GLOBES +# Height of display +DEFAULT_HEIGHT = NUM_GLOBES -# Width of display == number of strings we want to use -DEFAULT_WIDTH = HolidaySecretAPI.NUM_GLOBES +# Width of display +DEFAULT_WIDTH = NUM_GLOBES log = logging.getLogger(sys.argv[0]) handler = logging.StreamHandler() @@ -174,7 +175,7 @@ def render_to_hols(globelists, hols, width, height, # Your guess is as good as mine. holglobes = [] for i in range(len(hols)): - holglobes.append( [(0x00,0x00,0x00)] * HolidaySecretAPI.NUM_GLOBES ) + holglobes.append( [(0x00,0x00,0x00)] * NUM_GLOBES ) pass if orientation == 'vertical': @@ -185,7 +186,7 @@ def render_to_hols(globelists, hols, width, height, # If switchback mode is enabled, reverse order # of every second line, so they display the right # way on zigzag Holidays - pieces = int(math.floor(float(HolidaySecretAPI.NUM_GLOBES) / orientsize)) + pieces = int(math.floor(float(NUM_GLOBES) / orientsize)) # The globelist is a set of y values. List [0] is all the vertical globes # for x = 0, list [1] is the vertical x=1 globes, etc. @@ -281,19 +282,19 @@ def render_to_hols(globelists, hols, width, height, if options.switchback: if options.orientation == 'vertical': height = options.switchback - pieces = int(math.floor(float(HolidaySecretAPI.NUM_GLOBES) / height)) + pieces = int(math.floor(float(NUM_GLOBES) / height)) width = options.numstrings * pieces else: width = options.switchback - pieces = int(math.floor(float(HolidaySecretAPI.NUM_GLOBES) / width)) + pieces = int(math.floor(float(NUM_GLOBES) / width)) height = options.numstrings * pieces else: if options.orientation == 'vertical': - height = HolidaySecretAPI.NUM_GLOBES + height = NUM_GLOBES pieces = options.numstrings width = options.numstrings else: - width = HolidaySecretAPI.NUM_GLOBES + width = NUM_GLOBES pieces = options.numstrings height = options.numstrings pass diff --git a/examples/led_scroller.py b/examples/led_scroller.py index 2f9de5c..d20f3c6 100755 --- a/examples/led_scroller.py +++ b/examples/led_scroller.py @@ -9,10 +9,11 @@ import sys import math -from secretapi.holidaysecretapi import HolidaySecretAPI - +from api.udpholiday import UDPHoliday from holiscreen import render_to_hols +NUM_GLOBES = UDPHoliday.NUM_GLOBES + # Initialise the font module pygame.font.init() @@ -57,11 +58,15 @@ def addOptions(self): self.add_option('', '--fontsize', dest='fontsize', help="Size of the font, in pixels [%default]", - type="int", default="7") + type="int") self.add_option('', '--color', dest='color', help="Color of the font [%default]", type="string", default="0xffffff") + + self.add_option('-o', '--orientation', dest='orientation', + help="Orientation of the strings [%default]", + type="choice", choices=['vertical', 'horizontal'], default='vertical') self.add_option('', '--list-fonts', dest='listfonts', help="List fonts available for use", @@ -85,11 +90,34 @@ def parseOptions(self): return self.options, self.args def postOptions(self): + + if self.options.listfonts: + fontlist = pygame.font.get_fonts() + fontlist.sort() + print '\n'.join(fontlist) + sys.exit(0) + if len(self.args) < 1: self.error("Specify address and port of remote Holiday(s)") pass pass +def pick_fontsize(fontname, height, text): + """ + Figure out the appropriate font size based on the font chosen + and the height available in the display. + """ + fontsize = 2 + for fontsize in range(1000): + font = pygame.font.SysFont(fontname, fontsize) + size_req = font.size(text) + if size_req[1] > height+4: + fontsize -= 1 + break + pass + pass + return fontsize + def text_to_globes(text, width, height, color=(255,255,255), offset_left=0): @@ -108,7 +136,12 @@ def text_to_globes(text, width, height, """ #font = pygame.font.SysFont("None", 10) - font = pygame.font.SysFont(options.fontname, options.fontsize) + if options.fontsize is None: + fontsize = pick_fontsize(options.fontname, height, text) + else: + fontsize = options.fontsize + + font = pygame.font.SysFont(options.fontname, fontsize) color = pygame.Color(color) surface = font.render(text, options.antialias, color, (0,0,0) ) @@ -117,7 +150,7 @@ def text_to_globes(text, width, height, # Now fetch the pixels as an array pa = pygame.PixelArray(surface) - for i in range(len(pa[0])): + for i in range(len(pa[1])): globes = [] pixels = pa[:,i] pixvals = [ pa.surface.unmap_rgb(x) for x in pixels ] @@ -147,28 +180,34 @@ def text_to_globes(text, width, height, optparse = LEDScrollerOptions(usage=usage) options, args = optparse.parseOptions() + + # How big is our canvas window? if options.switchback: width = options.switchback - pieces = int(math.floor(float(HolidaySecretAPI.NUM_GLOBES) / width)) - height = pieces * options.numstrings + pieces = int(math.floor(float(NUM_GLOBES) / width)) + height = pieces else: height = options.numstrings - width = HolidaySecretAPI.NUM_GLOBES + width = NUM_GLOBES + pass - if options.listfonts: - print '\n'.join(pygame.font.get_fonts()) - sys.exit(0) + # Orientation setup + if options.orientation == 'vertical': + old_w = width + width = height + height = old_w + pass hols = [] if len(args) > 1: for arg in args: hol_addr, hol_port = arg.split(':') - hols.append(HolidaySecretAPI(addr=hol_addr, port=int(hol_port))) + hols.append(UDPHoliday(ipaddr=hol_addr, port=int(hol_port))) else: hol_addr, hol_port = args[0].split(':') for i in range(options.numstrings): - hols.append(HolidaySecretAPI(addr=hol_addr, port=int(hol_port)+i)) + hols.append(UDPHoliday(ipaddr=hol_addr, port=int(hol_port)+i)) pass # The text to render is passed in from stdin @@ -198,7 +237,7 @@ def text_to_globes(text, width, height, pass #print "renderlist:", render_glist render_to_hols(render_glist, hols, width, height, - orientation='horizontal', + orientation=options.orientation, switchback=options.switchback) offset += 1 if offset > len(glist[0]): From 3d7e3f8b6e674625a0a809fb0eebe387e3aa8a19 Mon Sep 17 00:00:00 2001 From: Justin Warren Date: Wed, 1 Jan 2014 17:09:09 +1100 Subject: [PATCH 32/50] * Added working twinkleapp code, including some sample animation patterns. * Updated base API with chase() and rotate() implementations. * Minor tweaks to phloston and twinkle.py --- api/base.py | 35 ++- examples/patterns/greenandgold.json | 27 +++ examples/{ => patterns}/xmas.json | 0 examples/{ => patterns}/xmas2.json | 0 examples/{ => patterns}/xmas3.json | 0 examples/{ => patterns}/xmas4.json | 0 examples/phlostonapp.py | 2 +- examples/twinkle.py | 16 +- examples/twinkleapp.py | 329 ++++++++++++++++++++++++++++ 9 files changed, 394 insertions(+), 15 deletions(-) create mode 100644 examples/patterns/greenandgold.json rename examples/{ => patterns}/xmas.json (100%) rename examples/{ => patterns}/xmas2.json (100%) rename examples/{ => patterns}/xmas3.json (100%) rename examples/{ => patterns}/xmas4.json (100%) create mode 100644 examples/twinkleapp.py diff --git a/api/base.py b/api/base.py index 2452004..eee6891 100644 --- a/api/base.py +++ b/api/base.py @@ -60,14 +60,37 @@ def set_pattern(self, pattern): raise ValueError("pattern length incorrect: %d != %d" % ( len(pattern), self.NUM_GLOBES) ) self.globes = pattern[:] - def chase(self, direction="True"): - """Rotate all of the globes around - up if TRUE, down if FALSE""" + def chase(self, direction=True): + """ + Rotate all globes around one step. + @param direction: Direction to rotate: up if True, down if False + """ + if direction: + # Rotate all globes around by one place + oldglobes = self.globes[:] + self.globes = oldglobes[1:] + self.globes.append(oldglobes[0]) + pass + else: + oldglobes = self.globes[:] + self.globes = oldglobes[:-1] + self.globes.insert(0, oldglobes[-1]) + pass return - def rotate(self, newr, newg, newb, direction="True", ): - """Rotate all of the globes up if TRUE, down if FALSE - Set the new start of the string to the color values""" - return + def rotate(self, newr, newg, newb, direction=True): + """ + Rotate all globes, just like chase, but replace the 'first' + globe with new color. + """ + self.chase(direction) + if direction: + self.globes[-1] = (newr, newg, newb) + pass + else: + self.globes[0] = (newr, newg, newb) + pass + pass def render(self): raise NotImplementedError diff --git a/examples/patterns/greenandgold.json b/examples/patterns/greenandgold.json new file mode 100644 index 0000000..4efe00a --- /dev/null +++ b/examples/patterns/greenandgold.json @@ -0,0 +1,27 @@ +{ "lights": [ +"#008721", "#008721", "#008721", +"#008721", "#008721", +"#FCD116", "#FCD116", "#FCD116", +"#FCD116", "#FCD116", + +"#008721", "#008721", "#008721", +"#008721", "#008721", +"#FCD116", "#FCD116", "#FCD116", +"#FCD116", "#FCD116", + +"#008721", "#008721", "#008721", +"#008721", "#008721", +"#FCD116", "#FCD116", "#FCD116", +"#FCD116", "#FCD116", + +"#008721", "#008721", "#008721", +"#008721", "#008721", +"#FCD116", "#FCD116", "#FCD116", +"#FCD116", "#FCD116", + +"#008721", "#008721", "#008721", +"#008721", "#008721", +"#FCD116", "#FCD116", "#FCD116", +"#FCD116", "#FCD116" + + ] } diff --git a/examples/xmas.json b/examples/patterns/xmas.json similarity index 100% rename from examples/xmas.json rename to examples/patterns/xmas.json diff --git a/examples/xmas2.json b/examples/patterns/xmas2.json similarity index 100% rename from examples/xmas2.json rename to examples/patterns/xmas2.json diff --git a/examples/xmas3.json b/examples/patterns/xmas3.json similarity index 100% rename from examples/xmas3.json rename to examples/patterns/xmas3.json diff --git a/examples/xmas4.json b/examples/patterns/xmas4.json similarity index 100% rename from examples/xmas4.json rename to examples/patterns/xmas4.json diff --git a/examples/phlostonapp.py b/examples/phlostonapp.py index 0e2a1cb..74f1609 100755 --- a/examples/phlostonapp.py +++ b/examples/phlostonapp.py @@ -35,7 +35,7 @@ # What color to start as. (r,g,b) tuple START_COLOR = (100,0,0) -class PhlostonApp(ButtonHoliday, ButtonApp): +class PhlostonApp(ButtonApp): """ An app for a physical Holiday """ diff --git a/examples/twinkle.py b/examples/twinkle.py index 845cb42..7921d3c 100755 --- a/examples/twinkle.py +++ b/examples/twinkle.py @@ -1,6 +1,6 @@ #!/usr/bin/python """ -Twinkling pretty lights on the Holiday tree +Twinkling pretty lights on the Holiday """ import optparse @@ -79,14 +79,14 @@ def addOptions(self): help="Maximum value difference from basecolor [%default]", type="float", default=1.0 ) - self.add_option('', '--chase', dest='chase', + self.add_option('', '--chase-forwards', dest='chase', help="Lights chase around the string", - action="store_true", default=False) - - self.add_option('', '--chase-direction', dest='chase_direction', - help="Set direction of chase, if chase enabled [%default]", - type="choice", choices=['forward', 'backward'], default='forward') + action="store_true") + self.add_option('', '--chase-backwards', dest='chase', + help="Lights chase around the string", + action="store_false") + self.add_option('', '--simplex-damper', dest='simplex_damper', help="Amount of simplex noise dampening [%default]", type="float", default=5.0) @@ -268,7 +268,7 @@ def twinkle_holiday(hol, options, init_pattern, noise_array=None): # Chase mode? if options.chase: - if options.chase_direction == 'forward': + if options.chase_direction: # Rotate all globes around by one place oldglobes = hol.globes[:] hol.globes = oldglobes[1:] diff --git a/examples/twinkleapp.py b/examples/twinkleapp.py new file mode 100644 index 0000000..6ad0f2e --- /dev/null +++ b/examples/twinkleapp.py @@ -0,0 +1,329 @@ +#!/usr/bin/env python +""" +ButtonApp version of twinkle.py +Makes a Holiday 'twinkle' with various light patterns/styles + +Copyright (c) 2013 Justin Warren +License: MIT (see LICENSE for details) +""" + +import threading +import random +import json +import time +import os.path +import colorsys + +import webcolors + +from api.base import ButtonHoliday, ButtonApp + +from simplexnoise import raw_noise_2d + +BASEPATH = os.path.abspath(os.path.dirname(__file__)) +PATTERN_DIR = os.path.join(BASEPATH, 'patterns') + +def load_patternfile(filename): + with open(filename) as fp: + pattern = [ webcolors.hex_to_rgb(x) for x in json.load(fp)['lights'] ] + return pattern + pass + +MODES = [ + # candela mode + {'twinkle_algo': 'simplex', + 'simplex_damper': 5.0, + 'init_pattern': [(176, 119, 31),] * ButtonHoliday.NUM_GLOBES, + 'chase': None, + 'snoozetime': 0.1, + }, + + # Xmas twinkle + {'twinkle_algo': 'simplex', + 'simplex_damper': 5.0, + 'init_pattern': load_patternfile(os.path.join(PATTERN_DIR, 'xmas.json')), + 'chase': None, + 'snoozetime': 0.1, + }, + + # Xmas chase + {'twinkle_algo': 'chase_only', + 'init_pattern': load_patternfile(os.path.join(PATTERN_DIR, 'xmas2.json')), + 'chase': True, + 'snoozetime': 0.5, + }, + + # Australian Green and Gold twinkle + {'twinkle_algo': 'simplex', + 'simplex_damper': 5.0, + 'init_pattern': load_patternfile(os.path.join(PATTERN_DIR, 'greenandgold.json')), + 'chase': None, + 'snoozetime': 0.1, + }, + + # Australian Green and Gold chaser + {'twinkle_algo': 'chase_only', + 'init_pattern': load_patternfile(os.path.join(PATTERN_DIR, 'greenandgold.json')), + 'simplex_damper': 5.0, + 'chase': True, + 'snoozetime': 0.1, + }, + + # Simple random mode + {'twinkle_algo': 'random_shift', + 'init_pattern': [ (random.randint(0, 130), + random.randint(0, 130), + random.randint(0, 130)) for x in range(ButtonHoliday.NUM_GLOBES) ], + 'change_chance': 0.5, + 'huestep_max': 0.1, + 'satstep_max': 0.1, + 'valstep_max': 0.1, + 'chase': None, + 'snoozetime': 0.1, + }, + + # Random mode with limits + {'twinkle_algo': 'random_limits', + 'init_pattern': [ (random.randint(0, 130), + random.randint(0, 130), + random.randint(0, 130)) for x in range(ButtonHoliday.NUM_GLOBES) ], + 'change_chance': 0.2, + 'huestep_max': 0.05, + 'satstep_max': 0.05, + 'valstep_max': 0.05, + + 'huediff_max': 0.3, + 'satdiff_max': 0.3, + 'valdiff_max': 0.3, + 'chase': None, + 'snoozetime': 0.1, + }, + + ] + +class Twinkler(object): + + def __init__(self, hol, options): + self.hol = hol + self.options = options + self.noise_array = [ 0, ] * self.hol.NUM_GLOBES + + # initialise the holiday with the options.pattern + self.set_pattern(options['init_pattern']) + pass + + def set_pattern(self, pattern): + self.hol.set_pattern(pattern) + pass + + def render(self): + self.hol.render() + + def twinkle_simplex(self, idx): + nv = raw_noise_2d(self.noise_array[idx], + random.random()) / self.options.get('simplex_damper', 5.0) + self.noise_array[idx] += nv + if self.noise_array[idx] > 1.0: + self.noise_array[idx] = 1.0 + pass + elif self.noise_array[idx] < -1.0: + self.noise_array[idx] = -1.0 + pass + + ranger = (self.noise_array[idx] + 1.0) / 2.0 + + # Adjust colour. + (base_r, base_g, base_b) = self.options['init_pattern'][idx] + #log.debug("adjusting from orig: %d %d %d", base_r, base_g, base_b) + r = int(base_r * ranger) + g = int(base_g * ranger) + b = int(base_b * ranger) + self.hol.setglobe(idx, r, g, b) + pass + + def rand_change_colcomp(self, val, stepmax, baseval=None, diffmax=None): + """ + Change a colour component value from a baseline using a random perturbation + """ + if baseval is None: + baseval = val + pass + + valstep = random.random() * stepmax + # 50% chance of positive or negative step + if random.randint(0, 1): + val += valstep + if diffmax and abs(baseval - val) > diffmax: + val = baseval + diffmax + pass + if val > 1.0: + val -= 1.0 + else: + val -= valstep + if diffmax and abs(baseval - val) > diffmax: + val = baseval - diffmax + pass + if val < 0.0: + val += 1.0 + pass + return val + + def twinkle_random_shift(self, idx): + """ + Randomly change a bulb from its current value + """ + # % chance of updating a given globe + if random.random() < self.options['change_chance']: + + r, g, b = self.hol.getglobe(idx) + (h, s, v) = colorsys.rgb_to_hsv(r/255.0, g/255.0, b/255.0) + #log.debug("start h s v: %f %f %f", h, s, v) + + # Adjust color components by a random amount + h = self.rand_change_colcomp(h, self.options['huestep_max']) + s = self.rand_change_colcomp(s, self.options['satstep_max']) + v = self.rand_change_colcomp(v, self.options['valstep_max']) + + (r, g, b) = colorsys.hsv_to_rgb(h, s, v) + #log.debug("r g b: %f %f %f", r, g, b) + self.hol.setglobe(idx, int(255*r), int(255*g), int(255*b)) + pass + pass + + def twinkle_random_limits(self, idx): + """ + Randomly change a bulb from its baseline value within limits + """ + (base_r, base_g, base_b) = self.options['init_pattern'][idx] + (base_h, base_s, base_v) = colorsys.rgb_to_hsv(base_r/255.0, + base_g/255.0, + base_b/255.0) + + # % chance of updating a given globe + if random.random() < self.options['change_chance']: + + r, g, b = self.hol.getglobe(idx) + (h, s, v) = colorsys.rgb_to_hsv(r/255.0, g/255.0, b/255.0) + #log.debug("start h s v: %f %f %f", h, s, v) + + # Adjust color components by a random amount + h = self.rand_change_colcomp(h, self.options['huestep_max'], base_h, self.options['huediff_max']) + s = self.rand_change_colcomp(s, self.options['satstep_max'], base_s, self.options['satdiff_max']) + v = self.rand_change_colcomp(v, self.options['valstep_max'], base_v, self.options['valdiff_max']) + + (r, g, b) = colorsys.hsv_to_rgb(h, s, v) + #log.debug("r g b: %f %f %f", r, g, b) + self.hol.setglobe(idx, int(255*r), int(255*g), int(255*b)) + pass + pass + + def twinkle(self): + """ + Change globe colours using some algorithm + """ + for idx in range(0, self.hol.NUM_GLOBES): + + + if self.options['twinkle_algo'] == 'simplex': + self.twinkle_simplex(idx) + pass + + elif self.options['twinkle_algo'] == 'random_limits': + self.twinkle_random_limits(idx) + pass + + # No globe colour updates, just chase + elif self.options['twinkle_algo'] == 'chase_only': + pass + + else: + self.twinkle_random_shift(idx) + pass + pass + + # Chase enabled? + if self.options['chase'] is not None: + self.hol.chase(self.options['chase']) + pass + + self.hol.render() + pass + +class TwinkleApp(ButtonApp): + + def start(self): + self.t = TwinkleThread() + self.modenum = 0 + self.t.set_mode(self.modenum) + self.t.start() + + def stop(self): + self.t.terminate = True + + def up(self): + self.modenum += 1 + if self.modenum > len(MODES)-1: + self.modenum = 0 + pass + self.t.set_mode(self.modenum) + pass + + def down(self): + self.modenum -= 1 + if self.modenum < 0: + self.modenum = len(MODES)-1 + pass + self.t.set_mode(self.modenum) + pass + +class TwinkleThread(threading.Thread): + """ + Singleton thread that runs the animation + """ + def run(self): + self.terminate = False + + self.hol = ButtonHoliday() + self.modenum = 0 + self.twinkler = Twinkler(self.hol, MODES[self.modenum]) + + while True: + if self.terminate: + return + + self.twinkler.twinkle() + time.sleep(MODES[self.modenum]['snoozetime']) + pass + pass + + def set_mode(self, modenum): + self.modenum = modenum + if hasattr(self, 'twinkler'): + self.twinkler.options = MODES[modenum] + + # Reset the initialisation pattern + self.twinkler.set_pattern(MODES[modenum]['init_pattern']) + pass + pass + pass + +if __name__ == '__main__': + app = TwinkleApp() + app.start() + time.sleep(1) + app.down() + time.sleep(1) + app.up() + time.sleep(1) + + for i in range(len(MODES)-1): + app.up() + print "running in mode", app.modenum + time.sleep(2) + pass + + app.up() + print "running in mode", app.modenum + time.sleep(2) + + app.stop() From 2dbb74a7f4e3faeea59768d947f3fc433e2f7a8b Mon Sep 17 00:00:00 2001 From: Justin Warren Date: Thu, 2 Jan 2014 13:51:09 +1100 Subject: [PATCH 33/50] * Lowered simplex noise damper setting in twinkle so it twinkles more. --- examples/twinkleapp.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/twinkleapp.py b/examples/twinkleapp.py index 6ad0f2e..7f54961 100644 --- a/examples/twinkleapp.py +++ b/examples/twinkleapp.py @@ -32,7 +32,7 @@ def load_patternfile(filename): MODES = [ # candela mode {'twinkle_algo': 'simplex', - 'simplex_damper': 5.0, + 'simplex_damper': 4.0, 'init_pattern': [(176, 119, 31),] * ButtonHoliday.NUM_GLOBES, 'chase': None, 'snoozetime': 0.1, @@ -40,7 +40,7 @@ def load_patternfile(filename): # Xmas twinkle {'twinkle_algo': 'simplex', - 'simplex_damper': 5.0, + 'simplex_damper': 4.0, 'init_pattern': load_patternfile(os.path.join(PATTERN_DIR, 'xmas.json')), 'chase': None, 'snoozetime': 0.1, @@ -55,7 +55,7 @@ def load_patternfile(filename): # Australian Green and Gold twinkle {'twinkle_algo': 'simplex', - 'simplex_damper': 5.0, + 'simplex_damper': 4.0, 'init_pattern': load_patternfile(os.path.join(PATTERN_DIR, 'greenandgold.json')), 'chase': None, 'snoozetime': 0.1, @@ -64,7 +64,7 @@ def load_patternfile(filename): # Australian Green and Gold chaser {'twinkle_algo': 'chase_only', 'init_pattern': load_patternfile(os.path.join(PATTERN_DIR, 'greenandgold.json')), - 'simplex_damper': 5.0, + 'simplex_damper': 4.0, 'chase': True, 'snoozetime': 0.1, }, @@ -121,7 +121,7 @@ def render(self): def twinkle_simplex(self, idx): nv = raw_noise_2d(self.noise_array[idx], - random.random()) / self.options.get('simplex_damper', 5.0) + random.random()) / self.options.get('simplex_damper', 4.0) self.noise_array[idx] += nv if self.noise_array[idx] > 1.0: self.noise_array[idx] = 1.0 From 8693e83633335d4912a8962f85068ba8747c4103 Mon Sep 17 00:00:00 2001 From: Justin Warren Date: Sun, 26 Jan 2014 08:21:49 +1100 Subject: [PATCH 34/50] * Tweaked the timing variables to make the display more interesting. --- examples/phlostonapp.py | 2 +- examples/twinkleapp.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/phlostonapp.py b/examples/phlostonapp.py index 74f1609..0d2cefe 100755 --- a/examples/phlostonapp.py +++ b/examples/phlostonapp.py @@ -27,7 +27,7 @@ log.setLevel(logging.DEBUG) # Amount of time between frames of animation -SNOOZETIME = 0.05 +SNOOZETIME = 0.02 # Amount of hue to change when Up/Down button is pressed (0.0 - 1.0) HUESTEP = 0.05 diff --git a/examples/twinkleapp.py b/examples/twinkleapp.py index 7f54961..62debb3 100644 --- a/examples/twinkleapp.py +++ b/examples/twinkleapp.py @@ -35,7 +35,7 @@ def load_patternfile(filename): 'simplex_damper': 4.0, 'init_pattern': [(176, 119, 31),] * ButtonHoliday.NUM_GLOBES, 'chase': None, - 'snoozetime': 0.1, + 'snoozetime': 0.04, }, # Xmas twinkle @@ -43,7 +43,7 @@ def load_patternfile(filename): 'simplex_damper': 4.0, 'init_pattern': load_patternfile(os.path.join(PATTERN_DIR, 'xmas.json')), 'chase': None, - 'snoozetime': 0.1, + 'snoozetime': 0.04, }, # Xmas chase @@ -58,7 +58,7 @@ def load_patternfile(filename): 'simplex_damper': 4.0, 'init_pattern': load_patternfile(os.path.join(PATTERN_DIR, 'greenandgold.json')), 'chase': None, - 'snoozetime': 0.1, + 'snoozetime': 0.04, }, # Australian Green and Gold chaser From 170fd87a17de411b732125fd6684f9ff06e713e7 Mon Sep 17 00:00:00 2001 From: Justin Warren Date: Sun, 26 Jan 2014 08:29:50 +1100 Subject: [PATCH 35/50] * Added the 'oz' green and gold colour scheme for Australia Day 2014. --- examples/holibeats.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/examples/holibeats.py b/examples/holibeats.py index b0b3583..2161379 100755 --- a/examples/holibeats.py +++ b/examples/holibeats.py @@ -193,9 +193,16 @@ def get_val_color(val, scheme='default'): elif scheme == 'yellow': r, g, b = 30 + 225 * val, 30 + 225 * val, 0 - pass + elif scheme == 'oz': + if val < 0.4: + r, g, b = 0, 30 + 225 * val, 0 + else: + r, g, b = 30 + 225 * val, 30 + 225 * val, 0 + pass + pass + r = int(r) g = int(g) b = int(b) @@ -221,7 +228,7 @@ def addOptions(self): self.add_option('-c', '--colorscheme', dest='colorscheme', help=" [%default]", - type="choice", choices = ['default', 'blue', 'green', 'red', 'yellow'], + type="choice", choices = ['default', 'blue', 'green', 'red', 'yellow', 'oz'], default='default') self.add_option('-b', '--buckets', dest='numbuckets', From fe6a47aa10f1ee2108628599d75a1dc1404c66e5 Mon Sep 17 00:00:00 2001 From: Justin Warren Date: Fri, 14 Feb 2014 13:12:47 +1100 Subject: [PATCH 36/50] * Added the 'throb' animation to twinkle. Pulses entires string up and down in brightness for either a pattern or a single colour. Configurable duration and smoothness via --anim_sleep and --throb-speed --- examples/patterns/love01.json | 32 +++++ examples/twinkle.py | 231 ++++++++++++++++++++-------------- 2 files changed, 171 insertions(+), 92 deletions(-) create mode 100644 examples/patterns/love01.json diff --git a/examples/patterns/love01.json b/examples/patterns/love01.json new file mode 100644 index 0000000..4531471 --- /dev/null +++ b/examples/patterns/love01.json @@ -0,0 +1,32 @@ +{ "lights": [ +"#aa0000", "#aaaaaa", +"#aa0000", "#aaaaaa", +"#aa0000", "#aaaaaa", +"#aa0000", "#aaaaaa", +"#aa0000", "#aaaaaa", + +"#aa0000", "#aaaaaa", +"#aa0000", "#aaaaaa", +"#aa0000", "#aaaaaa", +"#aa0000", "#aaaaaa", +"#aa0000", "#aaaaaa", + +"#aa0000", "#aaaaaa", +"#aa0000", "#aaaaaa", +"#aa0000", "#aaaaaa", +"#aa0000", "#aaaaaa", +"#aa0000", "#aaaaaa", + +"#aa0000", "#aaaaaa", +"#aa0000", "#aaaaaa", +"#aa0000", "#aaaaaa", +"#aa0000", "#aaaaaa", +"#aa0000", "#aaaaaa", + +"#aa0000", "#aaaaaa", +"#aa0000", "#aaaaaa", +"#aa0000", "#aaaaaa", +"#aa0000", "#aaaaaa", +"#aa0000", "#aaaaaa" + + ] } diff --git a/examples/twinkle.py b/examples/twinkle.py index 7921d3c..cf4fb24 100755 --- a/examples/twinkle.py +++ b/examples/twinkle.py @@ -53,7 +53,7 @@ def addOptions(self): self.add_option('-t', '--twinkle-algo', dest='twinkle_algo', help="Algorithm to use for twinkling [%default]", - type="choice", choices=['random', 'simplex'], default='random') + type="choice", choices=['random', 'simplex', 'throb'], default='random') self.add_option('-H', '--HUESTEP', dest='huestep_max', help="Maximum step between hues [%default]", @@ -90,7 +90,19 @@ def addOptions(self): self.add_option('', '--simplex-damper', dest='simplex_damper', help="Amount of simplex noise dampening [%default]", type="float", default=5.0) - + + self.add_option('', '--throb-speed', dest='throb_speed', + help="Speed of throb animation [%default]", + type="float", default=2.0) + + self.add_option('', '--throb-up', dest='throb_dir', + help="Start throbbing up", + action="store_true") + + self.add_option('', '--throb-down', dest='throb_dir', + help="Start throbbing down", + action="store_false") + def parseOptions(self): """ Emulate twistedmatrix options parser API @@ -163,109 +175,144 @@ def twinkle_holiday(hol, options, init_pattern, noise_array=None): if noise_array is None: noise_array = [ 0, ] * HolidaySecretAPI.NUM_GLOBES pass - - for idx in range(0, HolidaySecretAPI.NUM_GLOBES): - - # Choose globe update algorithm - if options.twinkle_algo == 'simplex': - nv = raw_noise_2d(noise_array[idx], random.random()) / options.simplex_damper - noise_array[idx] += nv - if noise_array[idx] > 1.0: - noise_array[idx] = 1.0 - pass - elif noise_array[idx] < -1.0: - noise_array[idx] = -1.0 - pass - ranger = (noise_array[idx] + 1.0) / 2.0 - - # Adjust colour. If basecolor, adjust from basecolor - if options.basecolor: - (base_r, base_g, base_b) = webcolors.hex_to_rgb(options.basecolor) - r = int(base_r * ranger) - g = int(base_g * ranger) - b = int(base_b * ranger) - pass + if options.twinkle_algo == 'throb': + + # Increase brightness by throb_speed / anim_sleep + # until max brightness, and then decrease + r, g, b = hol.getglobe(0) + (h, s, v) = colorsys.rgb_to_hsv(r/255.0, g/255.0, b/255.0) + + # Check direction for throb. The limits are set to be visually + # interesting, because value above about 0.6 doesn't look much + # different. + if v > 0.6: + options.throb_dir = False + elif v < 0.02: + options.throb_dir = True + + throb_amount = (1.0 / options.throb_speed * options.anim_sleep) + for idx in range(0, HolidaySecretAPI.NUM_GLOBES): + r, g, b = hol.getglobe(idx) + (h, s, v) = colorsys.rgb_to_hsv(r/255.0, g/255.0, b/255.0) + if options.throb_dir: + v += throb_amount + if v > 0.91: + v = 0.91 else: - # adjust from original color - (base_r, base_g, base_b) = init_pattern[idx] - #log.debug("adjusting from orig: %d %d %d", base_r, base_g, base_b) - r = int(base_r * ranger) - g = int(base_g * ranger) - b = int(base_b * ranger) + v -= throb_amount + if v < 0.02: + v = 0.02 pass - hol.setglobe(idx, r, g, b) - - else: - # % chance of updating a given globe - if random.random() < options.change_chance: - - r, g, b = hol.getglobe(idx) - (h, s, v) = colorsys.rgb_to_hsv(r/255.0, g/255.0, b/255.0) - #log.debug("start h s v: %f %f %f", h, s, v) - # Adjust hue by a random amount - huestep = random.random() * options.huestep_max - # 50% chance of positive or negative step - if random.randint(0, 1): - h += huestep - if options.basecolor and abs(base_hsv[0] - h) > options.huediff_max: - h = base_hsv[0] + options.huediff_max - if h > 1.0: - h = h - 1.0 - else: - h -= huestep - if options.basecolor and abs(h - base_hsv[0]) > options.huediff_max: - h = base_hsv[0] - options.huediff_max + (r, g, b) = colorsys.hsv_to_rgb(h, s, v) + hol.setglobe(idx, int(255*r), int(255*g), int(255*b)) + pass + pass - if h < 0.0: - h = 1.0 + h + elif options.twinkle_algo in ['simplex', 'random']: + + for idx in range(0, HolidaySecretAPI.NUM_GLOBES): + + # Choose globe update algorithm + if options.twinkle_algo == 'simplex': + nv = raw_noise_2d(noise_array[idx], random.random()) / options.simplex_damper + noise_array[idx] += nv + if noise_array[idx] > 1.0: + noise_array[idx] = 1.0 + pass + elif noise_array[idx] < -1.0: + noise_array[idx] = -1.0 pass - satstep = random.random() * options.satstep_max - if random.randint(0, 1): - s += satstep - if options.basecolor and abs(base_hsv[1] - s) > options.satdiff_max: - s = base_hsv[1] + options.satdiff_max + ranger = (noise_array[idx] + 1.0) / 2.0 - if s > 1.0: - s = 1.0 - else: - s -= satstep - if options.basecolor and abs(s - base_hsv[1]) > options.satdiff_max: - s = base_hsv[1] - options.satdiff_max - - # Make sure things stay bright and colorful! - if s < 0.0: - s = 0.0 - - # Adjust value by a random amount - valstep = random.random() * options.valstep_max - # 50% chance of positive or negative step - if random.randint(0, 1): - v += valstep - if options.basecolor and abs(base_hsv[2] - v) > options.valdiff_max: - v = base_hsv[2] + options.valdiff_max - - if v > 1.0: - v = 1.0 + # Adjust colour. If basecolor, adjust from basecolor + if options.basecolor: + (base_r, base_g, base_b) = webcolors.hex_to_rgb(options.basecolor) + r = int(base_r * ranger) + g = int(base_g * ranger) + b = int(base_b * ranger) + pass else: - v -= valstep - if options.basecolor and abs(v - base_hsv[2]) > options.valdiff_max: - v = base_hsv[2] - options.valdiff_max - - if v < 0.2: - v = 0.2 + # adjust from original color + (base_r, base_g, base_b) = init_pattern[idx] + #log.debug("adjusting from orig: %d %d %d", base_r, base_g, base_b) + r = int(base_r * ranger) + g = int(base_g * ranger) + b = int(base_b * ranger) pass + hol.setglobe(idx, r, g, b) - #log.debug("end h s v: %f %f %f", h, s, v) - - (r, g, b) = colorsys.hsv_to_rgb(h, s, v) - #log.debug("r g b: %f %f %f", r, g, b) - hol.setglobe(idx, int(255*r), int(255*g), int(255*b)) + else: + # % chance of updating a given globe + if random.random() < options.change_chance: + + r, g, b = hol.getglobe(idx) + (h, s, v) = colorsys.rgb_to_hsv(r/255.0, g/255.0, b/255.0) + #log.debug("start h s v: %f %f %f", h, s, v) + # Adjust hue by a random amount + huestep = random.random() * options.huestep_max + # 50% chance of positive or negative step + if random.randint(0, 1): + h += huestep + if options.basecolor and abs(base_hsv[0] - h) > options.huediff_max: + h = base_hsv[0] + options.huediff_max + if h > 1.0: + h = h - 1.0 + else: + h -= huestep + if options.basecolor and abs(h - base_hsv[0]) > options.huediff_max: + h = base_hsv[0] - options.huediff_max + + if h < 0.0: + h = 1.0 + h + pass + + satstep = random.random() * options.satstep_max + if random.randint(0, 1): + s += satstep + if options.basecolor and abs(base_hsv[1] - s) > options.satdiff_max: + s = base_hsv[1] + options.satdiff_max + + if s > 1.0: + s = 1.0 + else: + s -= satstep + if options.basecolor and abs(s - base_hsv[1]) > options.satdiff_max: + s = base_hsv[1] - options.satdiff_max + + # Make sure things stay bright and colorful! + if s < 0.0: + s = 0.0 + + # Adjust value by a random amount + valstep = random.random() * options.valstep_max + # 50% chance of positive or negative step + if random.randint(0, 1): + v += valstep + if options.basecolor and abs(base_hsv[2] - v) > options.valdiff_max: + v = base_hsv[2] + options.valdiff_max + + if v > 1.0: + v = 1.0 + else: + v -= valstep + if options.basecolor and abs(v - base_hsv[2]) > options.valdiff_max: + v = base_hsv[2] - options.valdiff_max + + if v < 0.2: + v = 0.2 + pass + + #log.debug("end h s v: %f %f %f", h, s, v) + + (r, g, b) = colorsys.hsv_to_rgb(h, s, v) + #log.debug("r g b: %f %f %f", r, g, b) + hol.setglobe(idx, int(255*r), int(255*g), int(255*b)) + pass pass pass pass - # Chase mode? if options.chase: if options.chase_direction: From 37c514097ae1cc1c2746b6f522d1b037f9608e3a Mon Sep 17 00:00:00 2001 From: Justin Warren Date: Fri, 21 Feb 2014 16:03:06 +1100 Subject: [PATCH 37/50] * simgame now uses a PyGame event to handle refresh rate, so it doesn't chew CPU. --- simgame/simgame.py | 60 ++++++++++++++++++++++++---------------------- 1 file changed, 31 insertions(+), 29 deletions(-) diff --git a/simgame/simgame.py b/simgame/simgame.py index 4bdf2ca..1f4b7f1 100755 --- a/simgame/simgame.py +++ b/simgame/simgame.py @@ -26,7 +26,7 @@ def __init__(self, *args, **kwargs): def addOptions(self): self.add_option('-f', '--fps', dest='fps', help="Frames per second, used to slow down simulator", - type="int") + type="int", default=30) self.add_option('-n', '--numstrings', dest='numstrings', help="Number of Holiday strings to simulate [%default]", type="int", default=1) @@ -167,43 +167,24 @@ def run(self): screen = pygame.display.set_mode([self.screen_x, self.screen_y]) screen.fill((0,0,0)) - mainloop, x, y, color, delta, fps = True, 25 , 0, (32,32,32), 1, 1000 + running, x, y, color, delta, fps = True, 25 , 0, (32,32,32), 1, 1000 Clock = pygame.time.Clock() - while mainloop: - if self.options.fps: - tickFPS = Clock.tick(self.options.fps) - pass - # Move the simulator forward one tick - if not paused: - pass + DISPLAY_REFRESH = pygame.USEREVENT + pygame.time.set_timer(DISPLAY_REFRESH, int(1000.0/self.options.fps)) + color = (255,255,255) - pygame.display.set_caption("Press Esc or q to quit. p to pause. r to reset. FPS: %.2f" % (Clock.get_fps())) + while running: - color = (255,255,255) - - # Black screen and update timer - screen.fill((0,0,0)) - - screen.blit(self.myFont.render("Holiday by MooresCloud Simulator", True, (color)), (x,y)) - screen.blit(self.myFont.render("(c) Justin Warren ", True, (color)), (x,y+self.myFont.get_height())) - - # Let the strings receive and process data - self.recv_data() - - # Draw the Holiday strings - self.draw_strings(screen) - #sys.exit(1) - for event in pygame.event.get(): if event.type == pygame.QUIT: - mainloop = False + running = False elif event.type == pygame.KEYDOWN: # If quite key pressed, flag for exit if event.key == pygame.K_ESCAPE or event.key == pygame.K_q: - mainloop = False + running = False # Toggle pause elif event.key == pygame.K_p: @@ -217,9 +198,30 @@ def run(self): # reset all strings to blank elif event.key == pygame.K_r: self.blank_strings() - pass + pass + pass + + elif event.type == DISPLAY_REFRESH: + + pygame.display.set_caption("Press Esc or q to quit. p to pause. r to reset. FPS: %.2f" % (Clock.get_fps())) - pygame.display.update() + # Black screen and update timer + screen.fill((0,0,0)) + + screen.blit(self.myFont.render("Holiday by MooresCloud Simulator", True, (color)), (x,y)) + screen.blit(self.myFont.render("(c) Justin Warren ", True, (color)), (x,y+self.myFont.get_height())) + + # Let the strings receive and process data + self.recv_data() + + # Draw the Holiday strings + self.draw_strings(screen) + + # Update the screen + pygame.display.update() + pass + pass + pygame.time.wait(0) pass pygame.quit() From 5faf577d55d53b720ad79af6ad7bb55dd9f9d99b Mon Sep 17 00:00:00 2001 From: Justin Warren Date: Wed, 26 Feb 2014 15:43:52 +1100 Subject: [PATCH 38/50] * Added holivid.py, which displays videos on an array of Holidays --- examples/holiscreen.py | 2 - examples/holivid.py | 148 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 148 insertions(+), 2 deletions(-) create mode 100755 examples/holivid.py diff --git a/examples/holiscreen.py b/examples/holiscreen.py index 064a100..7b05045 100755 --- a/examples/holiscreen.py +++ b/examples/holiscreen.py @@ -220,10 +220,8 @@ def render_to_hols(globelists, hols, width, height, else: basenum = (i % pieces) * orientsize - if switchback: holid = i / pieces - #log.debug("basenum: %d", basenum) else: holid = i basenum = 0 diff --git a/examples/holivid.py b/examples/holivid.py new file mode 100755 index 0000000..b232c7d --- /dev/null +++ b/examples/holivid.py @@ -0,0 +1,148 @@ +#!/usr/bin/python +""" +Display video on a set of Holidays +""" +import numpy as np +import cv2 +import math +import optparse + +from api.udpholiday import UDPHoliday +from holiscreen import render_to_hols + +NUM_GLOBES = UDPHoliday.NUM_GLOBES + +class HolividOptions(optparse.OptionParser): + """ + Command-line options parser + """ + def __init__(self, *args, **kwargs): + optparse.OptionParser.__init__(self, **kwargs) + self.addOptions() + + def addOptions(self): + self.add_option('-n', '--numstrings', dest='numstrings', + help="Number of Holiday strings to simulate [%default]", + type="int", default=25) + + self.add_option('-f', '--file', dest='filename', + help="Video file to display.", + type="string" ) + + # Listen on multiple TCP/UDP ports, one for each Holiday we simulate + self.add_option('-p', '--portstart', dest='portstart', + help="Port number to start at for UDP listeners [%default]", + type="int", default=9988) + + self.add_option('-o', '--orientation', dest='orientation', + help="Orientation of the strings [%default]", + type="choice", choices=['vertical', 'horizontal'], default='vertical') + + self.add_option('', '--switchback', dest='switchback', + help="'Switchback' strings, make a single string display like its " + "more than one every m globes", + type="int") + + self.add_option('', '--fps', dest='fps', + help="Set video playback frames-per-second. [%default]", + type="int", default=25) + + pass + + + def parseOptions(self): + """ + Emulate twistedmatrix options parser API + """ + options, args = self.parse_args() + self.options = options + self.args = args + + self.postOptions() + + return self.options, self.args + + def postOptions(self): + if len(self.args) < 1: + self.error("Specify address and port of remote Holiday(s)") + pass + + if not self.options.filename: + self.error("Video filename not given.") + pass + pass + +if __name__ == '__main__': + + usage = "Usage: %prog [options] [ ... ]" + optparse = HolividOptions(usage=usage) + options, args = optparse.parseOptions() + + hols = [] + if len(args) > 1: + for arg in args: + hol_addr, hol_port = arg.split(':') + hols.append(UDPHoliday(ipaddr=hol_addr, port=int(hol_port))) + else: + hol_addr, hol_port = args[0].split(':') + for i in range(options.numstrings): + hols.append(UDPHoliday(ipaddr=hol_addr, port=int(hol_port)+i)) + pass + pass + + if options.switchback: + if options.orientation == 'vertical': + height = options.switchback + pieces = int(math.floor(float(NUM_GLOBES) / height)) + width = options.numstrings * pieces + else: + width = options.switchback + pieces = int(math.floor(float(NUM_GLOBES) / width)) + height = options.numstrings * pieces + else: + if options.orientation == 'vertical': + height = NUM_GLOBES + pieces = options.numstrings + width = options.numstrings + else: + width = NUM_GLOBES + pieces = options.numstrings + height = options.numstrings + pass + pass + + cap = cv2.VideoCapture(options.filename) + newsize = (width, height) + + skipframe = False + + while True: + ret, frame = cap.read() + + if ret != True: + print "No valid frame." + break + + # Resize the frame into the resolution of our Holiday array + frame = cv2.resize(frame, newsize, interpolation=cv2.INTER_AREA) + + # The colours are in the wrong format, so convert them + frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) + + #cv2.imshow('frame', frame) + + # A frame is just a Numpy array of pixel values, i.e. globelists. We need to take + # these values and map them onto our holiday array. + render_to_hols(frame, hols, width, height, + options.orientation, options.switchback) + + # Wait period between keycapture (in milliseconds) + # This gives us approximately the right number of frames per second + wait_time = int(1000/options.fps) + if cv2.waitKey(wait_time) & 0xFF == ord('q'): + break + pass + + cap.release() + cv2.destroyAllWindows() + From 7389e30080fafcfbb7ea59d4480c04696e28c621 Mon Sep 17 00:00:00 2001 From: Justin Warren Date: Wed, 26 Feb 2014 17:30:25 +1100 Subject: [PATCH 39/50] * Using CUBIC interpolation in holivid for improved colour. * Dual display in holivid provides a monitor of the live video feed. * simgame now supports turning off TCP or UDP listeners --- examples/holivid.py | 11 +++++---- simgame/holiday.py | 59 ++++++++++++++++++++++++++------------------- simgame/simgame.py | 19 ++++++++++++--- 3 files changed, 55 insertions(+), 34 deletions(-) diff --git a/examples/holivid.py b/examples/holivid.py index b232c7d..f2ccb11 100755 --- a/examples/holivid.py +++ b/examples/holivid.py @@ -124,16 +124,17 @@ def postOptions(self): break # Resize the frame into the resolution of our Holiday array - frame = cv2.resize(frame, newsize, interpolation=cv2.INTER_AREA) + holframe = cv2.resize(frame, newsize, interpolation=cv2.INTER_CUBIC) # The colours are in the wrong format, so convert them - frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) - - #cv2.imshow('frame', frame) + holframe = cv2.cvtColor(holframe, cv2.COLOR_BGR2RGB) + + # Display the original frame, for the demo + cv2.imshow('holivid monitor display', frame) # A frame is just a Numpy array of pixel values, i.e. globelists. We need to take # these values and map them onto our holiday array. - render_to_hols(frame, hols, width, height, + render_to_hols(holframe, hols, width, height, options.orientation, options.switchback) # Wait period between keycapture (in milliseconds) diff --git a/simgame/holiday.py b/simgame/holiday.py index 95aaf1f..e0fb6d2 100644 --- a/simgame/holiday.py +++ b/simgame/holiday.py @@ -27,43 +27,51 @@ class HolidayRemote(object): NUM_GLOBES = 50 def __init__(self, remote=False, addr='', tcpport=None, - udpport=None): + udpport=None, + notcp=False, + noudp=False): # Initialise globes to zero self.globes = [ [0x00, 0x00, 0x00] ] * self.NUM_GLOBES + self.notcp = notcp + self.noudp = noudp + if not remote: self.addr = addr - if udpport is None: - bound_port = False - for udpport in range(9988, 10100): - try: - self.bind_udp(udpport) - bound_port = True - break - except socket.error, e: - num, s = e - # Try again if port is in use, else raise - if num != 98: - raise + if not noudp: + if udpport is None: + bound_port = False + for udpport in range(9988, 10100): + try: + self.bind_udp(udpport) + bound_port = True + break + except socket.error, e: + num, s = e + # Try again if port is in use, else raise + if num != 98: + raise + pass pass + # If we get this far, bail out + if not bound_port: + raise ValueError("Can't find available UDP port in range 9988 to 10100") + + else: + self.bind_udp(udpport) pass - # If we get this far, bail out - if not bound_port: - raise ValueError("Can't find available UDP port in range 9988 to 10100") + print "UDP listening on (%s, %s)" % (self.addr, self.udpport) + pass - else: - self.bind_udp(udpport) - - print "UDP listening on (%s, %s)" % (self.addr, self.udpport) - # Set up REST API on a TCP port - self.q = Queue() + if not notcp: + self.q = Queue() - self.iop = Process(target=iotas.run, kwargs={ 'port': 8080, + self.iop = Process(target=iotas.run, kwargs={ 'port': 8080, 'queue': self.q }) - self.iop.start() + self.iop.start() else: raise NotImplementedError("Listening Simulator only. Does not send to remote devices.") @@ -71,7 +79,8 @@ def exit(self): """ Force shutdown of processes """ - self.iop.terminate() + if hasattr(self, 'iop'): + self.iop.terminate() def __del__(self): self.exit() diff --git a/simgame/simgame.py b/simgame/simgame.py index 1f4b7f1..e337bd7 100755 --- a/simgame/simgame.py +++ b/simgame/simgame.py @@ -59,8 +59,16 @@ def addOptions(self): help="'Switchback' strings, make a single string display like its " "more than one every m globes", type="int") - pass + self.add_option('', '--no-tcp', dest='notcp', + help="Disable TCP listener.", + action="store_true", default=False) + + self.add_option('', '--no-udp', dest='noudp', + help="Disable UDP listener.", + action="store_true", default=False) + pass + def parseOptions(self): """ Emulate twistedmatrix options parser API @@ -114,7 +122,8 @@ def setup(self): self.HolidayList = [ ] for i in range(0, self.numstrings): - self.HolidayList.append( HolidayRemote() ) + self.HolidayList.append( HolidayRemote(notcp=self.options.notcp, + noudp=self.options.noudp) ) pass if self.options.switchback: @@ -235,8 +244,10 @@ def recv_data(self): Process any data each simulated string may have received """ for hol in self.HolidayList: - hol.recv_udp() - hol.recv_tcp() + if not self.options.noudp: + hol.recv_udp() + if not self.options.notcp: + hol.recv_tcp() pass def blank_strings(self): From 487afb6511abf24eff2b3a4820393cd5ff6a1491 Mon Sep 17 00:00:00 2001 From: Justin Warren Date: Thu, 27 Feb 2014 19:05:30 +1100 Subject: [PATCH 40/50] * Updated simgame with 'flipped' mode, for reversing the install direction of vertically hung Holidays (base at bottom). * Updated holiscreen to support 'flipped' mode. * Updated holivid to support 'flipped' mode. --- examples/holiscreen.py | 39 +++++++++++++++++++++++++++------------ examples/holivid.py | 13 ++++++++++++- simgame/simgame.py | 21 +++++++++++++++++---- 3 files changed, 56 insertions(+), 17 deletions(-) diff --git a/examples/holiscreen.py b/examples/holiscreen.py index 7b05045..83f6020 100755 --- a/examples/holiscreen.py +++ b/examples/holiscreen.py @@ -120,6 +120,10 @@ def addOptions(self): self.add_option('-o', '--orientation', dest='orientation', help="Orientation of the strings [%default]", type="choice", choices=['vertical', 'horizontal'], default='vertical') + + self.add_option('', '--flipped', dest='flipped', + help="Flip direction of the strings [%default]", + action="store_true", default=False) self.add_option('', '--switchback', dest='switchback', help="'Switchback' strings, make a single string display like its " @@ -152,7 +156,7 @@ def postOptions(self): def render_image(img, hols, width, height, orientation='vertical', - switchback=None): + switchback=None, flipped=False): """ Render an image to a set of remote Holidays @@ -161,10 +165,11 @@ def render_image(img, hols, width, height, #log.debug("w x h: %d x %d", width, height) globelists = image_to_globes(img, width, height) #log.debug("len globelists: %d", len(globelists)) - render_to_hols(globelists, hols, width, height, orientation, switchback) + render_to_hols(globelists, hols, width, height, orientation, switchback, flipped) def render_to_hols(globelists, hols, width, height, - orientation='vertical', switchback=None): + orientation='vertical', switchback=None, + flipped=True): """ Render a set of globe values to a set of Holidays """ @@ -220,6 +225,7 @@ def render_to_hols(globelists, hols, width, height, else: basenum = (i % pieces) * orientsize + if switchback: holid = i / pieces else: @@ -227,13 +233,19 @@ def render_to_hols(globelists, hols, width, height, basenum = 0 if not (i % pieces) % 2: - globe_idx = basenum + l + if not flipped: + globe_idx = basenum + l + else: + globe_idx = basenum + (orientsize-l) - 1 else: - globe_idx = basenum + (orientsize-l) - 1 + if not flipped: + globe_idx = basenum + (orientsize-l) - 1 + else: + globe_idx = basenum + l pass try: - #log.debug("holid: %d, globeidx: %d", holid, globe_idx) + #log.debug("holid: %d, l: %d, i: %d, basenum: %d, globeidx: %d", holid, l, i, basenum, globe_idx) hol = hols[holid] except IndexError: log.error("Not enough Holidays for number of screen lines. Need at least %d." % (holid+1,)) @@ -242,7 +254,7 @@ def render_to_hols(globelists, hols, width, height, try: holglobes[holid][globe_idx] = [r,g,b] except IndexError: - log.debug("Error at holid %d, globeidx %d", holid, globe_idx) + log.error("Error at holid %d, globeidx %d", holid, globe_idx) raise #print "line: %d, holid: %d, globe: %d, val: (%d, %d, %d)" % (l, holid, globe_idx, r,g,b) @@ -267,11 +279,11 @@ def render_to_hols(globelists, hols, width, height, if len(args) > 1: for arg in args: hol_addr, hol_port = arg.split(':') - hols.append(HolidaySecretAPI(addr=hol_addr, port=int(hol_port))) + hols.append(UDPHoliday(ipaddr=hol_addr, port=int(hol_port))) else: hol_addr, hol_port = args[0].split(':') for i in range(options.numstrings): - hols.append(HolidaySecretAPI(addr=hol_addr, port=int(hol_port)+i)) + hols.append(UDPHoliday(ipaddr=hol_addr, port=int(hol_port)+i)) pass pass @@ -312,7 +324,8 @@ def render_to_hols(globelists, hols, width, height, if isanimated and options.animate: # render first frame - render_image(img, hols, width, height, options.orientation, options.switchback) + render_image(img, hols, width, height, options.orientation, + options.switchback, options.flipped) while True: # get the next frame after a short delay time.sleep(options.anim_sleep) @@ -322,9 +335,11 @@ def render_to_hols(globelists, hols, width, height, except EOFError: img.seek(0) pass - render_image(img, hols, width, height, options.orientation, options.switchback) + render_image(img, hols, width, height, options.orientation, + options.switchback, options.flipped) pass pass else: - render_image(img, hols, width, height, options.orientation, options.switchback) + render_image(img, hols, width, height, options.orientation, + options.switchback, options.flipped) pass diff --git a/examples/holivid.py b/examples/holivid.py index f2ccb11..d169533 100755 --- a/examples/holivid.py +++ b/examples/holivid.py @@ -6,6 +6,7 @@ import cv2 import math import optparse +import time from api.udpholiday import UDPHoliday from holiscreen import render_to_hols @@ -117,6 +118,7 @@ def postOptions(self): skipframe = False while True: + loopstart = time.time() ret, frame = cap.read() if ret != True: @@ -139,7 +141,16 @@ def postOptions(self): # Wait period between keycapture (in milliseconds) # This gives us approximately the right number of frames per second - wait_time = int(1000/options.fps) + wait_time = 1000/options.fps + + # Figure out how long the wait_time would be without the + # processing time + + loopend = time.time() + # Adjust waiting based on how long it takes us to process + process_time = (loopend - loopstart) * 1000 + + wait_time = int(wait_time - process_time) if cv2.waitKey(wait_time) & 0xFF == ord('q'): break pass diff --git a/simgame/simgame.py b/simgame/simgame.py index e337bd7..db51baa 100755 --- a/simgame/simgame.py +++ b/simgame/simgame.py @@ -51,6 +51,10 @@ def addOptions(self): help="Orientation of the strings [%default]", type="choice", choices=['vertical', 'horizontal'], default='vertical') + self.add_option('', '--flipped', dest='flipped', + help="Flip direction of the strings [%default]", + action="store_true", default=False) + self.add_option('', '--spacing', dest='spacing', help="Spacing between strings, in pixels. Overrides dynamic even spacing", type="int") @@ -67,6 +71,8 @@ def addOptions(self): self.add_option('', '--no-udp', dest='noudp', help="Disable UDP listener.", action="store_true", default=False) + + pass def parseOptions(self): @@ -291,15 +297,22 @@ def draw_strings(self, screen): min((piece+1)*self.bulbs_per_piece, hol.NUM_GLOBES) ) ): - # If even, start at the top and go down + # If even, start at the top and go down, unless flipped if not piece % 2: - bulb_y = self.start_y + self.string_header + (m * offset_per_bulb) + if self.options.flipped: + bulb_y = self.start_y + self.string_header + ( (self.bulbs_per_piece-1-m) * offset_per_bulb) + else: + bulb_y = self.start_y + self.string_header + (m * offset_per_bulb) # if odd, go from bottom to top else: - bulb_y = self.start_y + self.string_header + ( (self.bulbs_per_piece-1-m) * offset_per_bulb) - pass + if self.options.flipped: + bulb_y = self.start_y + self.string_header + (m * offset_per_bulb) + else: + bulb_y = self.start_y + self.string_header + ( (self.bulbs_per_piece-1-m) * offset_per_bulb) + pass + # Fetch the globe color for globe j r, g, b = hol.globes[j] pygame.draw.circle(screen, From ec763342f870d3de6feb90622de5e99a33352b2b Mon Sep 17 00:00:00 2001 From: Justin Warren Date: Sun, 2 Mar 2014 09:20:49 +1100 Subject: [PATCH 41/50] * Tuned the holibeats frequency bucket algorithm to provide a more visually interesting set of buckets. --- examples/holibeats.py | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/examples/holibeats.py b/examples/holibeats.py index 2161379..7f2c6bf 100755 --- a/examples/holibeats.py +++ b/examples/holibeats.py @@ -313,15 +313,15 @@ def chunks(l, n): nyq_rate = sample_rate / 2.0 # A Hamming window, to help with putting frequencies into - # different buckets. + # different buckets. Use twice as many as strings, because everything high is boring. window = numpy.hamming(BUFSIZE*2) if options.switchback: pieces = int(math.floor(float(HolidaySecretAPI.NUM_GLOBES) / options.switchback)) - numbuckets = pieces * options.numstrings + numbuckets = pieces * options.numstrings * 2 else: pieces = 1 - numbuckets = options.numstrings + numbuckets = options.numstrings * 2 pass # Allow manual override of number of buckets @@ -343,25 +343,34 @@ def chunks(l, n): hols.append(HolidaySecretAPI(addr=hol_addr, port=int(hol_port)+i)) pass - # Build the list of cutoff frequences as powers of 10 + # Build the list of cutoff frequences as powers of 2 cutoffs = [] - exp = [2, 3, 4] + base = 20 + + maxfreq = 20000 # anything higher than this is super boring + steps = int(math.log(maxfreq, base)) + exp = range(1, steps, 1) + + sublist = range(0, base) + for x in exp: - for i in range(1,10): - for sub in [0, 1, 2, 3]: - cutoffs.append( i*(10**x) + sub*(10**x/4) ) + for i in range(1, base, 1): + for sub in sublist: + cutoffs.append( i*(base**x) + sub*(base**x/len(sublist)) ) pass pass pass # ignore anything higher than 10kHz because it's boring - cutoffs = [ x for x in cutoffs if x <= 10000 ] + cutoffs = [ x for x in cutoffs if x <= maxfreq ] #print cutoffs - + # chunk cutoffs based on number of strings, # and use the max freq val of the chunk for our new cutoff cutoffs = [ max(c) for c in chunks(cutoffs, len(cutoffs)/numbuckets) ] - + #print cutoffs + #sys.exit(1) + # Used for auto-ranging of display maxval = 0 maxtime = datetime.datetime.now() From 19eb084ce152a0918799ba8aeafdddb0c7edbc23 Mon Sep 17 00:00:00 2001 From: Justin Warren Date: Sun, 2 Mar 2014 12:31:59 +1100 Subject: [PATCH 42/50] * holibeats now uses a configurable moving average for the max value when autoscaling the frequency buckets. * Removed the now redundant --quietseconds option for holibeats. --- examples/holibeats.py | 37 +++++++++++++++++-------------------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/examples/holibeats.py b/examples/holibeats.py index 7f2c6bf..a045fae 100755 --- a/examples/holibeats.py +++ b/examples/holibeats.py @@ -260,10 +260,9 @@ def addOptions(self): help="Manually set the maximum height value for buckets", type="float") - self.add_option('', '--quietseconds', dest='quiet_seconds', - help="Period of relative quiet to reset autoranging [%default]", - type="int", default=5) - + self.add_option('', '--avgvals', dest='num_maxvals', + help="Number of samples to calculate moving average for maxval over.", + type="int", default=50) def parseOptions(self): """ @@ -372,8 +371,8 @@ def chunks(l, n): #sys.exit(1) # Used for auto-ranging of display - maxval = 0 - maxtime = datetime.datetime.now() + maxvals = [] + current_maxval = 0 # Time limiting bits to slow down the UDP firehose # This is stupid, but functional. Should be event driven, probably. @@ -460,29 +459,27 @@ def chunks(l, n): # particularly for anything not classical music. visdata[0] = visdata[0] / 1.8 + bucket_maxval = 0 for i in range(0, numbuckets): # Update auto-ranging information - if visdata[i] > maxval: - maxval = visdata[i] - #log.debug("maxval reset: %f", maxval) - maxtime = datetime.datetime.now() + if visdata[i] > bucket_maxval: + bucket_maxval = visdata[i] pass pass - # If the maxval was set more than n seconds ago, start - # reducing the maxval gradually by x% per loop until - # we have to set the max again - if datetime.datetime.now() - maxtime > datetime.timedelta(seconds=options.quiet_seconds): - #log.debug("maxval %f is old. decreasing...", maxval) - maxval = maxval - (maxval * 0.01) - if maxval < 0: - maxval = 0 - pass + # new maxval is the average of the last n maxvals + maxvals.insert(0, bucket_maxval) + maxvals = maxvals[:options.num_maxvals] + + current_maxval = numpy.average(maxvals) + #log.debug("maxval reset: %f", maxval) + + #print "current max:", current_maxval # Send data to Holidays send_blinken(hols, visdata, pieces, switchback=options.switchback, - maxval=maxval, + maxval=current_maxval, maxheight=options.maxheight, autorange=options.autorange, colorscheme=options.colorscheme) From 7710eadeb11cbed3cfb6c491b1cb9da54d8a626d Mon Sep 17 00:00:00 2001 From: Justin Warren Date: Mon, 3 Mar 2014 14:54:49 +1100 Subject: [PATCH 43/50] * Tweaked default number of buckets to fit with where music frequencies tend to be. --- examples/holibeats.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/holibeats.py b/examples/holibeats.py index a045fae..fc70a52 100755 --- a/examples/holibeats.py +++ b/examples/holibeats.py @@ -317,10 +317,10 @@ def chunks(l, n): if options.switchback: pieces = int(math.floor(float(HolidaySecretAPI.NUM_GLOBES) / options.switchback)) - numbuckets = pieces * options.numstrings * 2 + numbuckets = int(pieces * options.numstrings * 1.5) else: pieces = 1 - numbuckets = options.numstrings * 2 + numbuckets = int(options.numstrings * 1.5) pass # Allow manual override of number of buckets @@ -347,7 +347,7 @@ def chunks(l, n): base = 20 maxfreq = 20000 # anything higher than this is super boring - steps = int(math.log(maxfreq, base)) + steps = int(math.log(maxfreq, base)) + 1 exp = range(1, steps, 1) sublist = range(0, base) From 78e8bf587322d935ff813dbc621d47fba3aa3adf Mon Sep 17 00:00:00 2001 From: Justin Warren Date: Sat, 3 Dec 2016 21:09:13 +1100 Subject: [PATCH 44/50] Fixed incorrect --chase parameters. Made the addr:port parsing more robust. --- examples/twinkle.py | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/examples/twinkle.py b/examples/twinkle.py index cf4fb24..e7432c7 100755 --- a/examples/twinkle.py +++ b/examples/twinkle.py @@ -22,6 +22,8 @@ log.addHandler(handler) log.setLevel(logging.DEBUG) +DEFAULT_HOL_PORT = 9988 + class TwinkleOptions(optparse.OptionParser): """ Command-line options parser @@ -53,7 +55,11 @@ def addOptions(self): self.add_option('-t', '--twinkle-algo', dest='twinkle_algo', help="Algorithm to use for twinkling [%default]", - type="choice", choices=['random', 'simplex', 'throb'], default='random') + type="choice", choices=['random', 'simplex', 'throb'], default='simplex') + + self.add_option('-i', '--init-only', dest='initonly', + help="Initialize string(s) and exit", + action="store_true") self.add_option('-H', '--HUESTEP', dest='huestep_max', help="Maximum step between hues [%default]", @@ -315,7 +321,7 @@ def twinkle_holiday(hol, options, init_pattern, noise_array=None): pass # Chase mode? if options.chase: - if options.chase_direction: + if options.chase: # Rotate all globes around by one place oldglobes = hol.globes[:] hol.globes = oldglobes[1:] @@ -344,12 +350,23 @@ def twinkle_holiday(hol, options, init_pattern, noise_array=None): # List of holiday noise patterns hol_noise = [] + + def split_addr_args(arg): + split_args = arg.split(':') + if len(split_args) == 1: + hol_addr = split_args[0] + hol_port = DEFAULT_HOL_PORT + else: + hol_addr = split_args[0] + hol_port = split_args[1] + return hol_addr, hol_port + if len(args) > 1: for arg in args: - hol_addr, hol_port = arg.split(':') + hol_addr, hol_port = split_addr_args(arg) hols.append(HolidaySecretAPI(addr=hol_addr, port=int(hol_port))) else: - hol_addr, hol_port = args[0].split(':') + hol_addr, hol_port = split_addr_args(args[0]) for i in range(options.numstrings): hols.append(HolidaySecretAPI(addr=hol_addr, port=int(hol_port)+i)) pass @@ -360,6 +377,9 @@ def twinkle_holiday(hol, options, init_pattern, noise_array=None): hol_noise.append(None) pass + if options.initonly: + sys.exit(0) + while True: for i, hol in enumerate(hols): noise = twinkle_holiday(hol, options, hol_inits[i], hol_noise[i]) From 17f8a43c074ff7e5a28efa483704bd6598c9bb53 Mon Sep 17 00:00:00 2001 From: Justin Warren Date: Sat, 3 Dec 2016 21:27:35 +1100 Subject: [PATCH 45/50] Added candela.sh as an example twinkling candles. --- examples/candela.sh | 3 +++ 1 file changed, 3 insertions(+) create mode 100755 examples/candela.sh diff --git a/examples/candela.sh b/examples/candela.sh new file mode 100755 index 0000000..6ad5d8f --- /dev/null +++ b/examples/candela.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +./twinkle.py $1 -H 0.01 -S 0.1 -V 0.00 -c 0.5 -b '#b0771f' --satdiff-max 0.05 --huediff-max 0.05 From 3056f4d6b969504a4bed674e365119a87a82f3a8 Mon Sep 17 00:00:00 2001 From: Justin Warren Date: Sun, 11 Dec 2016 13:13:59 +1100 Subject: [PATCH 46/50] * Fixed chase to be available as a twinkle algo option. * Fixed ranging of simplex twinkle to be relative to the original colour, not half the int of the (r,g,b) form of the colour. --- examples/twinkle.py | 35 +++++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/examples/twinkle.py b/examples/twinkle.py index e7432c7..5f95aee 100755 --- a/examples/twinkle.py +++ b/examples/twinkle.py @@ -55,7 +55,7 @@ def addOptions(self): self.add_option('-t', '--twinkle-algo', dest='twinkle_algo', help="Algorithm to use for twinkling [%default]", - type="choice", choices=['random', 'simplex', 'throb'], default='simplex') + type="choice", choices=['random', 'simplex', 'throb', 'chase'], default='simplex') self.add_option('-i', '--init-only', dest='initonly', help="Initialize string(s) and exit", @@ -75,15 +75,15 @@ def addOptions(self): self.add_option('', '--huediff-max', dest='huediff_max', help="Maximum hue difference from basecolor [%default]", - type="float", default=1.0 ) + type="float", default=0.0 ) self.add_option('', '--satdiff-max', dest='satdiff_max', help="Maximum saturation difference from basecolor [%default]", - type="float", default=1.0 ) + type="float", default=0.0 ) self.add_option('', '--valdiff-max', dest='valdiff_max', help="Maximum value difference from basecolor [%default]", - type="float", default=1.0 ) + type="float", default=0.0 ) self.add_option('', '--chase-forwards', dest='chase', help="Lights chase around the string", @@ -95,7 +95,7 @@ def addOptions(self): self.add_option('', '--simplex-damper', dest='simplex_damper', help="Amount of simplex noise dampening [%default]", - type="float", default=5.0) + type="float", default=2.0) self.add_option('', '--throb-speed', dest='throb_speed', help="Speed of throb animation [%default]", @@ -230,11 +230,12 @@ def twinkle_holiday(hol, options, init_pattern, noise_array=None): noise_array[idx] = -1.0 pass - ranger = (noise_array[idx] + 1.0) / 2.0 - + ranger = (noise_array[idx] + 1.0) / 1.0 + #log.debug("ranger: %f", ranger) # Adjust colour. If basecolor, adjust from basecolor if options.basecolor: (base_r, base_g, base_b) = webcolors.hex_to_rgb(options.basecolor) + #log.debug("adjusting from base: %d %d %d", base_r, base_g, base_b) r = int(base_r * ranger) g = int(base_g * ranger) b = int(base_b * ranger) @@ -242,13 +243,23 @@ def twinkle_holiday(hol, options, init_pattern, noise_array=None): else: # adjust from original color (base_r, base_g, base_b) = init_pattern[idx] + #log.debug("init pattern: %s", init_pattern[idx]) #log.debug("adjusting from orig: %d %d %d", base_r, base_g, base_b) r = int(base_r * ranger) + #log.debug("adjusted red from orig: %d %d %d", base_r, base_g, base_b) g = int(base_g * ranger) b = int(base_b * ranger) pass - hol.setglobe(idx, r, g, b) + if r > 255: + r = 255 + if g > 255: + g = 255 + if b > 255: + b = 255 + + hol.setglobe(idx, r, g, b) + #log.debug("init pattern: %s", init_pattern[idx]) else: # % chance of updating a given globe if random.random() < options.change_chance: @@ -319,8 +330,8 @@ def twinkle_holiday(hol, options, init_pattern, noise_array=None): pass pass pass - # Chase mode? - if options.chase: + elif options.twinkle_algo in ['chase']: + # Chase mode? if options.chase: # Rotate all globes around by one place oldglobes = hol.globes[:] @@ -328,11 +339,11 @@ def twinkle_holiday(hol, options, init_pattern, noise_array=None): hol.globes.append(oldglobes[0]) pass else: - log.debug("old: %s", hol.globes) + #log.debug("old: %s", hol.globes) oldglobes = hol.globes[:] hol.globes = oldglobes[:-1] hol.globes.insert(0, oldglobes[-1]) - log.debug("new: %s", hol.globes) + #log.debug("new: %s", hol.globes) pass hol.render() From f23267812ce7afbbe60ab0e4186b79b0a1aac7b8 Mon Sep 17 00:00:00 2001 From: Justin Warren Date: Wed, 21 Dec 2016 20:44:18 +1100 Subject: [PATCH 47/50] version bump --- iotas/version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iotas/version b/iotas/version index 0c1cef8..53f805d 100644 --- a/iotas/version +++ b/iotas/version @@ -1 +1 @@ -1.0a5 +1.1b1 From b8a1ef3019141113d635608996cdb63cc5504fac Mon Sep 17 00:00:00 2001 From: Justin Warren Date: Wed, 21 Dec 2016 21:09:08 +1100 Subject: [PATCH 48/50] Added changes from production Holiday missing from repo. --- iotas/devices/moorescloud/holiday/driver.py | 87 +++++++++++++-------- iotas/iotas.py | 5 ++ iotas/www/apps/rainbow/rainbow.js | 1 - iotas/www/index.html | 10 ++- iotas/www/js/iotas.js | 17 ++-- 5 files changed, 78 insertions(+), 42 deletions(-) diff --git a/iotas/devices/moorescloud/holiday/driver.py b/iotas/devices/moorescloud/holiday/driver.py index 8ac908f..6f02b6d 100644 --- a/iotas/devices/moorescloud/holiday/driver.py +++ b/iotas/devices/moorescloud/holiday/driver.py @@ -9,8 +9,8 @@ License: MIT (see LICENSE for details) """ -__author__ = 'Mark Pesce' -__version__ = '1.0b3' +__author__ = 'Justin Warren' +__version__ = '1.1b1' __license__ = 'MIT' import subprocess, time, os @@ -20,35 +20,6 @@ from bottle import request, abort class Holiday: -# def old__init__(self, remote=False, address='sim', name='nameless'): -# self.numleds = 50 -# self.leds = [] # Array of LED values. This may actually exist elsewhere eventually. -# self.address = '' -# self.name = name -# self.isSim = False -# -# if remote == False: -# self.remote = False -# if address == 'sim': -# self.pipename = os.path.join(os.path.expanduser('~'), 'pipelights.fifo') -# self.address = address -# else: -# self.pipename = "/run/pipelights.fifo" -# self.address = address -# try: -# self.pipe = open(self.pipename,"wb") -# except: -# print "Couldn't open the pipe, there's gonna be trouble!" -# ln = 0 -# else: -# self.address = address -# self.remote = True -# -# for ln in range(self.numleds): -# self.leds.append([0x00, 0x00, 0x00]) # Create and clear an array of RGB LED values -# -# return - def __init__(self, remote=False, address='sim', name='nameless', queue=None): self.numleds = 50 self.leds = [] # Array of LED values. This may actually exist elsewhere eventually. @@ -239,6 +210,54 @@ def do_rainbow(): return json.dumps({"success": success}) + @theapp.put(routebase + 'stopapps') + def do_stopapps(): + try: + fp = open('/run/pipebuttons.fifo', 'a') + fp.write('O\n') + fp.close() + success = True + except: + success = False + + return json.dumps({"success": success}) + + @theapp.put(routebase + 'button/mode') + def do_button_mode(): + try: + fp = open('/run/pipebuttons.fifo', 'a') + fp.write('M\n') + fp.close() + success = True + except: + success = False + + return json.dumps({"success": success}) + + @theapp.put(routebase + 'button/up') + def do_button_up(): + try: + fp = open('/run/pipebuttons.fifo', 'a') + fp.write('+\n') + fp.close() + success = True + except: + success = False + + return json.dumps({"success": success}) + + @theapp.put(routebase + 'button/down') + def do_button_down(): + try: + fp = open('/run/pipebuttons.fifo', 'a') + fp.write('-\n') + fp.close() + success = True + except: + success = False + + return json.dumps({"success": success}) + @theapp.put(routebase + 'runapp') def do_runapp(): """Starts/stops the named app""" @@ -273,7 +292,9 @@ def do_runapp(): else: print "stopping %s app" % dj['appname'] try: - c = subprocess.call(['/home/holiday/scripts/stop-app.sh'], shell=True) + fp = open('/run/pipebuttons.fifo', 'a') + fp.write('O\n') + fp.close() print "%s app stopped" % dj['appname'] success = True except subprocess.CalledProcessError: @@ -288,7 +309,7 @@ def get_version(): @theapp.get(routebase + 'swift_version') def get_swift_version(): - return json.dumps({ "version": "1.0b3" }) + return json.dumps({ "version": "1.1b1" }) @theapp.get(routebase) diff --git a/iotas/iotas.py b/iotas/iotas.py index 2b24a09..44ec9db 100755 --- a/iotas/iotas.py +++ b/iotas/iotas.py @@ -62,6 +62,11 @@ def redirect_404(error): else: return server_root() +@app.error(401) +def access_error(error): + print 'Access error on: %s' % request.url + return 'access error' + @app.route('/') def server_root(): global docroot diff --git a/iotas/www/apps/rainbow/rainbow.js b/iotas/www/apps/rainbow/rainbow.js index e9ef00b..3d17e95 100755 --- a/iotas/www/apps/rainbow/rainbow.js +++ b/iotas/www/apps/rainbow/rainbow.js @@ -17,7 +17,6 @@ function rainbow() { function appStart() { console.log("rainbow.appStart"); $("head").append(''); - loadTest(); } // Quit App diff --git a/iotas/www/index.html b/iotas/www/index.html index d2b333d..414763a 100755 --- a/iotas/www/index.html +++ b/iotas/www/index.html @@ -45,7 +45,7 @@ console.log(addr); // Setup IoTAS - console.log('instancing iotas') + console.log('creating iotas object'); var iotasrv = new iotas(document.URL); iotasrv.get_status(); @@ -53,6 +53,12 @@ // This is what we use to communicate with the simulator. // A bit of a hack, but it seems to work. var loc = window.location.hostname + ":" + window.location.port; + if (window.location.port.length == 0) { + var loc = window.location.hostname; + } else { + var loc = window.location.hostname + ":" + window.location.port; + } + console.log(loc); var defaultholidays = [ ['sim', loc ] ]; @@ -163,4 +169,4 @@

Your Holiday Name

- \ No newline at end of file + diff --git a/iotas/www/js/iotas.js b/iotas/www/js/iotas.js index 01c7f52..7a4daa8 100755 --- a/iotas/www/js/iotas.js +++ b/iotas/www/js/iotas.js @@ -7,7 +7,7 @@ // Requires a string 'address' (i.e. IP address 192.168.0.20) or resolvable name (i.e. 'light.local') function iotas(address) { - console.log("Instancing iotas"); + console.log("Instancing iotas with ", address); this.address = address; if (address.length == 0) { @@ -20,17 +20,22 @@ function iotas(address) { this.ip_addr = null; this.vers = null; this.hostnm = null; - this.apis = null; + this.apis = null; + this.local_device = null; + this.local_name = null; + this.device_url = null; this.get_status = get_status; // Get the status of IoTAS, config information, etc. // Bit sneaky in its use of global variables, but whatevs. function get_status() { - $.ajax({ + console.log('get_status: ', iotasrv.urlbase + 'iotas'); + $.ajax({ type: "GET", async: false, url: iotasrv.urlbase + 'iotas', - success: function(data) { + success: function(data) { + console.log('get_status success'); jd = JSON.parse(data) console.log(jd); iotasrv.ip_addr = jd.ip; @@ -43,8 +48,8 @@ function iotas(address) { console.log("iotas.device_url is " + iotasrv.device_url); console.log(iotasrv); }, - error: function() { - console.log("iotas.get_status failed"); + error: function(jqXHR, textStatus, errorThrown) { + console.log(textStatus, errorThrown); } }); } From 016b7617bdd8a56f08cb88946a1541bb90f901a4 Mon Sep 17 00:00:00 2001 From: Justin Warren Date: Thu, 22 Dec 2016 13:09:46 +1100 Subject: [PATCH 49/50] Added API support for starting a named buttonapp. --- iotas/devices/moorescloud/holiday/driver.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/iotas/devices/moorescloud/holiday/driver.py b/iotas/devices/moorescloud/holiday/driver.py index 6f02b6d..19841ae 100644 --- a/iotas/devices/moorescloud/holiday/driver.py +++ b/iotas/devices/moorescloud/holiday/driver.py @@ -279,16 +279,15 @@ def do_runapp(): return if (dj['isStart'] == True): - print "starting app %s" % dj['appname'] - app_path = os.path.join(self.appbase, dj['appname']) - print 'app_path: %s' % app_path - try: - c = subprocess.call(['/home/holiday/scripts/start-app.sh', app_path], shell=False) - print "%s app started" % dj['appname'] - success = True - except subprocess.CalledProcessError: - print "Error starting process" - success = False + appname = dj['appname'] + try: + fp = open('/run/pipebuttons.fifo', 'a') + fp.write('R%s\n' % appname) + fp.close() + success = True + print "%s app started" % appname + except: + success = False else: print "stopping %s app" % dj['appname'] try: From 4fd39f5cf5bd3a1749d5c57f1a73ea70fd9a8f49 Mon Sep 17 00:00:00 2001 From: Justin Warren Date: Thu, 22 Dec 2016 16:27:21 +1100 Subject: [PATCH 50/50] Added support to simgame for a local named pipe for local testing of buttonapp output, as it would look on a physical Holiday. --- simgame/holiday.py | 53 ++++++++++++++++++++++++++++++++++++++++------ simgame/simgame.py | 13 ++++++------ 2 files changed, 54 insertions(+), 12 deletions(-) diff --git a/simgame/holiday.py b/simgame/holiday.py index e0fb6d2..0691087 100644 --- a/simgame/holiday.py +++ b/simgame/holiday.py @@ -1,7 +1,7 @@ """ Simulated Holiday Server """ -import select, socket +import select, socket, os import array from Queue import Empty @@ -19,8 +19,6 @@ class HolidayRemote(object): This object pretends to be a physical Holiday light, which listens for instructions via both TCP (for the WebAPI) and also via UDP (using the SecretLabsAPI). - - FIXME: Only the UDP interface is implemented for now. """ # FIXME: The core code should all be in one place, not duplicated # in every subdirectory the way it is now. @@ -29,14 +27,17 @@ def __init__(self, remote=False, addr='', tcpport=None, udpport=None, notcp=False, - noudp=False): + noudp=False, + nofifo=False, + ): # Initialise globes to zero self.globes = [ [0x00, 0x00, 0x00] ] * self.NUM_GLOBES self.notcp = notcp self.noudp = noudp - + self.nofifo = nofifo + if not remote: self.addr = addr @@ -72,6 +73,13 @@ def __init__(self, remote=False, addr='', self.iop = Process(target=iotas.run, kwargs={ 'port': 8080, 'queue': self.q }) self.iop.start() + + # Set up a named pipe for local single string simulation + if not nofifo: + fd = os.open('/run/compose.fifo', os.O_RDONLY | os.O_NONBLOCK) + self.fifofp = os.fdopen(fd, 'r') + self.fifobuf = '' + self.fifobytes = (18) + (9 * self.NUM_GLOBES) else: raise NotImplementedError("Listening Simulator only. Does not send to remote devices.") @@ -155,4 +163,37 @@ def recv_tcp(self): pass pass pass - + + def recv_fifo(self): + """ + Try to read data from the FIFO. + Data starts with a header of 0x000010 followed by the hex encoded + PID value of the sending process. + Each globe value is encoded as 3 x 2-char hex values, + with a 0x prefix. + It is a total of 468 bytes long for a 50 globe string. + """ + try: + self.fifobuf += self.fifofp.read(self.fifobytes) + except IOError, e: + # Error 11 is when data isn't available on the pipe + # so we ignore it. + if e.errno == 11: + pass + + # Only process when there is enough data + while len(self.fifobuf) >= self.fifobytes: + data = self.fifobuf[:self.fifobytes] + self.fifobuf = self.fifobuf[self.fifobytes:] + header = data[:9] + pid = data[9:18] + globeraw = data[18:] + globedata = globeraw.split('\n') + + for i, vals in enumerate(globedata[:self.NUM_GLOBES]): + if len(vals) < 8: + break + r = int(vals[2:4], 16) + g = int(vals[4:6], 16) + b = int(vals[6:8], 16) + self.globes[i] = [ r, g, b ] diff --git a/simgame/simgame.py b/simgame/simgame.py index db51baa..15f4897 100755 --- a/simgame/simgame.py +++ b/simgame/simgame.py @@ -40,11 +40,6 @@ def addOptions(self): help="Port number to start at for UDP listeners [%default]", type="int", default=9988) - # Listener mode, TCP or UDP - #self.add_option('-m', '--mode', dest='mode', - # help="Mode of the simulator, UDP or TCP [%default]", - # type="choice", choices=['udp', 'tcp'], default='udp') - # Which way to draw multiple strings? Horizontally, like in the browser, # or vertically, like a string curtain? self.add_option('-o', '--orientation', dest='orientation', @@ -72,6 +67,9 @@ def addOptions(self): help="Disable UDP listener.", action="store_true", default=False) + self.add_option('', '--no-fifo', dest='nofifo', + help="Disable FIFO listener.", + action="store_true", default=False) pass @@ -129,7 +127,8 @@ def setup(self): self.HolidayList = [ ] for i in range(0, self.numstrings): self.HolidayList.append( HolidayRemote(notcp=self.options.notcp, - noudp=self.options.noudp) ) + noudp=self.options.noudp, + nofifo=self.options.nofifo) ) pass if self.options.switchback: @@ -254,6 +253,8 @@ def recv_data(self): hol.recv_udp() if not self.options.notcp: hol.recv_tcp() + if not self.options.nofifo: + hol.recv_fifo() pass def blank_strings(self):