Changeset - 818275850003
[Not reviewed]
default
0 3 0
Drew Perttula - 11 years ago 2014-06-13 05:27:46
drewp@bigasterisk.com
effecteval screens for only the kinds of outputs that it can send, so i can run a second instance just for LEDs
Ignore-this: e1b1fdd4564096ab9e214bb5e52db765
3 files changed with 76 insertions and 7 deletions:
0 comments (0 inline, 0 general)
bin/effecteval
Show inline comments
 
#!bin/python
 
from __future__ import division
 
from run_local import log
 
from twisted.internet import reactor
 
from twisted.internet.defer import inlineCallbacks, returnValue
 
import cyclone.web, cyclone.websocket, cyclone.httpclient
 
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
 
from light9.rdfdb.syncedgraph import SyncedGraph
 
from greplin import scales
 

	
 
from lib.cycloneerr import PrettyErrorHandler
 

	
 
class EffectEdit(PrettyErrorHandler, cyclone.web.RequestHandler):
 
    def get(self):
 
        self.write(open("light9/effecteval/effect.html").read())
 
    def delete(self):
 
@@ -186,25 +186,26 @@ class App(object):
 
        self.outputWhere = outputWhere
 
        self.graph = SyncedGraph(networking.rdfdb.url, "effectEval")
 
        self.graph.initiallySynced.addCallback(self.launch)
 

	
 
        self.stats = scales.collection('/',
 
                                       scales.PmfStat('sendLevels'),
 
                                       scales.PmfStat('getMusic'),
 
                                       scales.PmfStat('writeDmx'),
 
                                       scales.IntStat('errors'),
 
                                       )
 

	
 
    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=[
 
            (r'/()', SFH,
 
             {'path': 'light9/effecteval', 'default_filename': 'index.html'}),
 
            (r'/effect', EffectEdit),
 
            (r'/(websocket\.js)', SFH, {'path': 'light9/rdfdb/web/'}),
 
            (r'/effect\.js', StaticCoffee, {'src': 'light9/effecteval/effect.coffee'}),
 
            (r'/index\.js', StaticCoffee, {'src': 'light9/effecteval/index.coffee'}),
 
            (r'/effectUpdates', EffectUpdates),
 
            (r'/code', Code),
 
            (r'/songEffectsUpdates', SongEffectsUpdates),
 
@@ -227,24 +228,24 @@ class StaticCoffee(PrettyErrorHandler, c
 
            '/usr/bin/coffee', '--compile', '--print', self.src]))
 

	
 
        
 
if __name__ == "__main__":
 
    parser = optparse.OptionParser()
 
    parser.add_option('--show',
 
        help='show URI, like http://light9.bigasterisk.com/show/dance2008',
 
                      default=showconfig.showUri())
 
    parser.add_option("-v", "--verbose", action="store_true",
 
                      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)
 
    reactor.run()
light9/Effects.py
Show inline comments
 
@@ -6,24 +6,33 @@ import light9.Submaster as Submaster
 
from chase import chase as chase_logic
 
import showconfig
 
from rdflib import RDF
 
from light9 import Patch
 
from light9.namespaces import L9
 
log = logging.getLogger()
 

	
 
registered = []
 
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):
 
    """names is list of URIs. returns a submaster that chases through
 
    the inputs"""
 
    if random:
 
        r = random_mod.Random(random)
 
        names = names[:]
 
        r.shuffle(names)
 

	
 
    chase_vals = chase_logic(t, ontime, offset, onval, offval, names, combiner)
 
    lev = {}
 
@@ -42,24 +51,31 @@ def chase(t, ontime=0.5, offset=0.2, onv
 
def hsv(h, s, v, light='all', centerScale=.5):
 
    r,g,b = colorsys.hsv_to_rgb(h % 1.0, s, v)
 
    lev = {}
 
    if light in ['left', 'all']:
 
        lev[73], lev[74], lev[75] = r,g,b
 
    if light in ['right', 'all']:
 
        lev[80], lev[81], lev[82] = r,g,b
 
    if light in ['center', 'all']:
 
        lev[88], lev[89], lev[90] = r*centerScale,g*centerScale,b*centerScale
 
    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
 

	
 
    fade=0 makes steps, fade=1 means each one gets its full fraction
 
    of the time to fade in. Fades never...
 
    """
 
    frac = 1.0 / len(names)
 

	
 
    lev = {}
 
    for i, uri in enumerate(names):
 
        if t >= (i + 1) * frac:
 
            try:
light9/effecteval/effectloop.py
Show inline comments
 
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
 
log = logging.getLogger('effectloop')
 

	
 
class EffectLoop(object):
 
    """maintains a collection of the current EffectNodes, gets time from
 
    music player, sends dmx"""
 
    def __init__(self, graph, stats):
 
        self.graph, self.stats = graph, stats
 
        self.currentSong = None
 
        self.currentEffects = []
 
        self.lastLogTime = 0
 
        self.lastLogMsg = ""
 
        self.lastErrorLog = 0
 
        self.graph.addHandler(self.setEffects)
 
        self.period = 1 / 30
 
        self.coastSecs = .3 # main reason to keep this low is to notice play/pause
 

	
 
        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):
 
        self.currentEffects = []
 
        if self.currentSong is None:
 
            return
 
        
 
        for effectUri in self.graph.objects(self.currentSong, L9['effect']):
 
            self.currentEffects.append(EffectNode(self.graph, effectUri))
 
        
 
    @inlineCallbacks
 
    def getSongTime(self):
 
@@ -64,38 +74,39 @@ class EffectLoop(object):
 

	
 
                if song != self.currentSong:
 
                    self.currentSong = song
 
                    # this may be piling on the handlers
 
                    self.graph.addHandler(self.setEffects)
 

	
 
                if song is None:
 
                    return
 

	
 
                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)
 
        except Exception:
 
            self.stats.errors += 1
 
            traceback.print_exc()
 
            dt = 1
 

	
 
        reactor.callLater(dt, self.sendLevels)
 

	
 
    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
 
        
 
    @inlineCallbacks
 
    def sendOutput(self, combined):
 
        dmx = combined.get_dmx_list()
 
        with self.stats.writeDmx.time():
 
            yield dmxclient.outputlevels(dmx, twisted=True)
 
        
 
    def allEffectOutputs(self, songTime):
 
        outputs = []
 
@@ -116,13 +127,54 @@ class EffectLoop(object):
 
        else:
 
            if now > self.lastLogTime + 5:
 
                msg = self.logMessage(out)
 
                if msg != self.lastLogMsg:
 
                    log.info(msg)
 
                    self.lastLogMsg = msg
 
                self.lastLogTime = now
 
                
 
    def logMessage(self, out):
 
        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
 

	
 
        
0 comments (0 inline, 0 general)