diff --git a/bin/effecteval b/bin/effecteval --- a/bin/effecteval +++ b/bin/effecteval @@ -7,11 +7,11 @@ import cyclone.web, cyclone.websocket, c import sys, optparse, logging, subprocess, json, time, traceback, itertools from rdflib import URIRef, Literal -sys.path.append(".") +sys.path.append('/usr/lib/python2.7/dist-packages') # For numpy from light9 import networking, showconfig, Submaster, dmxclient from light9.curvecalc.curve import CurveResource from light9.effecteval.effect import EffectNode -from light9.effecteval.effectloop import EffectLoop +from light9.effecteval.effectloop import makeEffectLoop from light9.greplin_cyclone import StatsForCyclone from light9.namespaces import L9, RDF, RDFS from light9.rdfdb.patch import Patch @@ -195,7 +195,8 @@ class App(object): ) def launch(self, *args): - self.loop = EffectLoop(self.graph, self.stats) + self.loop = makeEffectLoop(self.graph, self.stats, self.outputWhere) + self.loop.startLoop() SFH = cyclone.web.StaticFileHandler self.cycloneApp = cyclone.web.Application(handlers=[ @@ -236,14 +237,14 @@ if __name__ == "__main__": help="logging.DEBUG") parser.add_option("--twistedlog", action="store_true", help="twisted logging") + parser.add_option("--output", metavar="WHERE", help="dmx or leds") (options, args) = parser.parse_args() - log.setLevel(logging.DEBUG if options.verbose else logging.INFO) if not options.show: raise ValueError("missing --show http://...") - app = App(URIRef(options.show)) + app = App(URIRef(options.show), options.output) if options.twistedlog: from twisted.python import log as twlog twlog.startLogging(sys.stderr) diff --git a/light9/Effects.py b/light9/Effects.py --- a/light9/Effects.py +++ b/light9/Effects.py @@ -15,6 +15,15 @@ def register(f): registered.append(f) return f +class ColorStrip(object): + """list of r,g,b tuples for sending to an LED strip""" + which = 'L' + pixels = [] + +class Blacklight(float): + """a level for the blacklight PWM output""" + + @register def chase(t, ontime=0.5, offset=0.2, onval=1.0, offval=0.0, names=None, combiner=max, random=False): @@ -51,6 +60,13 @@ def hsv(h, s, v, light='all', centerScal return Submaster.Submaster(name='hsv', levels=lev) @register +def colorDemo(): + s = ColorStrip() + s.which = 'L' + s.pixels = [(0,0,1)] * 50 + return s + +@register def stack(t, names=None, fade=0): """names is list of URIs. returns a submaster that stacks the the inputs diff --git a/light9/effecteval/effectloop.py b/light9/effecteval/effectloop.py --- a/light9/effecteval/effectloop.py +++ b/light9/effecteval/effectloop.py @@ -1,11 +1,14 @@ from __future__ import division import time, json, logging, traceback +import numpy +import serial from twisted.internet import reactor -from twisted.internet.defer import inlineCallbacks, returnValue +from twisted.internet.defer import inlineCallbacks, returnValue, succeed from rdflib import URIRef, Literal import cyclone.httpclient from light9.namespaces import L9, RDF, RDFS from light9.effecteval.effect import EffectNode +from light9 import Effects from light9 import networking from light9 import Submaster from light9 import dmxclient @@ -27,6 +30,13 @@ class EffectLoop(object): self.songTimeFromRequest = 0 self.requestTime = 0 # unix sec for when we fetched songTime + self.initOutput() + + def initOutput(self): + pass + + def startLoop(self): + log.info("startLoop") reactor.callLater(self.period, self.sendLevels) def setEffects(self): @@ -73,7 +83,7 @@ class EffectLoop(object): outputs = self.allEffectOutputs(songTime) combined = self.combineOutputs(outputs) self.logLevels(t1, combined) - self.sendOutput(combined) + yield self.sendOutput(combined) elapsed = time.time() - t1 dt = max(0, self.period - elapsed) @@ -87,6 +97,7 @@ class EffectLoop(object): def combineOutputs(self, outputs): """pick usable effect outputs and reduce them into one for sendOutput""" outputs = [x for x in outputs if isinstance(x, Submaster.Submaster)] + log.info('outputs %r', outputs) out = Submaster.sub_maxes(*outputs) return out @@ -125,4 +136,45 @@ class EffectLoop(object): return ("send dmx: {%s}" % ", ".join("%r: %.3g" % (str(k), v) for k,v in out.get_levels().items())) + +class LedLoop(EffectLoop): + def initOutput(self): + kw = dict(baudrate=115200) + self.boards = { + 'L': serial.Serial('/dev/serial/by-id/usb-FTDI_FT232R_USB_UART_A7027NYX-if00-port0', **kw), + 'R': serial.Serial('/dev/serial/by-id/usb-FTDI_FT232R_USB_UART_A7027JI6-if00-port0', **kw), + } + def combineOutputs(self, outputs): + combined = [] + for out in outputs: + if isinstance(out, (Effects.ColorStrip, Effects.Blacklight)): + # todo: take the max of all matching outputs, not just the last one + pixels = numpy.array(out.pixels, dtype=numpy.float16) + px255 = (numpy.clip(pixels, 0, 1) * 255).astype(numpy.uint8) + combined.append((out.which, px255)) + + return combined + + @inlineCallbacks + def sendOutput(self, combined): + for which, px255 in combined: + board = self.boards[which] + msg = '\x60\x00' + px255.reshape((-1,)).tostring() + board.write(msg) + board.flush() + + yield succeed(None) + + def logMessage(self, out): + return str([(w, p.tolist()) for w,p in out]) + +def makeEffectLoop(graph, stats, outputWhere): + if outputWhere == 'dmx': + return EffectLoop(graph, stats) + elif outputWhere == 'leds': + return LedLoop(graph, stats) + else: + raise NotImplementedError + +