Changeset - 0bfaeaa40cfe
[Not reviewed]
default
0 2 0
drewp@bigasterisk.com - 10 years ago 2015-06-14 20:05:18
drewp@bigasterisk.com
debug effects
Ignore-this: 74e4b63b84eb58d37d1402400d77e8b4
2 files changed with 6 insertions and 1 deletions:
0 comments (0 inline, 0 general)
light9/Effects.py
Show inline comments
 
from __future__ import division
 
import random as random_mod
 
import math
 
import logging, colorsys
 
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
 

	
 
@register
 
class Strip(object):
 
    """list of r,g,b tuples for sending to an LED strip"""
 
    which = 'L' # LR means both. W is the wide one
 
    pixels = []
 

	
 
    def __repr__(self):
 
        return '<Strip which=%r px0=%r>' % (self.which, self.pixels[0,:])
 
    
 
    @classmethod
 
    def solid(cls, which='L', color=(1,1,1), hsv=None):
 
        """hsv overrides color"""
 
        if hsv is not None:
 
            color = colorsys.hsv_to_rgb(hsv[0] % 1.0, hsv[1], hsv[2])
 
        x = cls()
 
        x.which = which
 
        x.pixels = [tuple(color)] * 50
 
        return x
 

	
 
    def __mul__(self, f):
 
        if not isinstance(f, (int, float)):
 
            raise TypeError
 
            
 
        s = Strip()
 
        s.which = self.which
 
        s.pixels = [(r*f, g*f, b*f) for r,g,b in self.pixels]
 
        return s
 

	
 
    __rmul__ = __mul__
 

	
 
@register
 
class Blacklight(float):
 
    """a level for the blacklight PWM output"""
 
    def __mul__(self, f):
 
        return Blacklight(float(self) * f)
 
    __rmul__ = __mul__
 
    
 
@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 = {}
 
    for uri, value in chase_vals.items():
 
        try:
 
            dmx = Patch.dmx_from_uri(uri)
 
        except KeyError:
 
            log.info(("chase includes %r, which doesn't resolve to a dmx chan" %
 
                   uri))
 
            continue
 
        lev[dmx] = value
 

	
 
    return Submaster.Submaster(name="chase" ,levels=lev)
 

	
 
@register
 
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 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:
 
                dmx = Patch.dmx_from_uri(uri)
 
            except KeyError:
 
                log.info(("stack includes %r, which doesn't resolve to a dmx chan"%
 
                       uri))
 
                continue
 
            lev[dmx] = 1
 
        else:
 
            break
 
    
 
    return Submaster.Submaster(name="stack", levels=lev)
 

	
 
@register
 
def smoove(x):
 
    return -2 * (x ** 3) + 3 * (x ** 2)
 
    
 
def configExprGlobals():
 
    graph = showconfig.getGraph()
 
    ret = {}
 

	
 
    for chaseUri in graph.subjects(RDF.type, L9['Chase']):
 
        shortName = chaseUri.rsplit('/')[-1]
 
        chans = graph.value(chaseUri, L9['channels'])
 
        ret[shortName] = list(graph.items(chans))
 
        print "%r is a chase" % shortName
 

	
 
    for f in registered:
 
        ret[f.__name__] = f
 

	
 
    ret['nsin'] = lambda x: (math.sin(x * (2 * math.pi)) + 1) / 2
 
    ret['ncos'] = lambda x: (math.cos(x * (2 * math.pi)) + 1) / 2
 
    def nsquare(t, on=.5):
 
        return (t % 1.0) < on
 
    ret['nsquare'] = nsquare
 

	
 
    _smooth_random_items = [random_mod.random() for x in range(100)]
 

	
 
    # suffix '2' to keep backcompat with the versions that magically knew time
 
    def smooth_random2(t, speed=1):
 
        """1 = new stuff each second, <1 is slower, fade-ier"""
 
        x = (t * speed) % len(_smooth_random_items)
 
        x1 = int(x)
 
        x2 = (int(x) + 1) % len(_smooth_random_items)
 
        y1 = _smooth_random_items[x1]
 
        y2 = _smooth_random_items[x2]
 
        return y1 + (y2 - y1) * ((x - x1))
 

	
 
    def notch_random2(t, speed=1):
 
        """1 = new stuff each second, <1 is slower, notch-ier"""
 
        x = (t * speed) % len(_smooth_random_items)
 
        x1 = int(x)
 
        y1 = _smooth_random_items[x1]
 
        return y1
 

	
 
    ret['noise2'] = smooth_random2
 
    ret['notch2'] = notch_random2
 

	
 

	
 

	
 
    
 
    return ret
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, threads
 
from twisted.internet.defer import inlineCallbacks, returnValue, succeed
 
from twisted.internet.error import TimeoutError
 
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
 
from light9 import prof
 
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.currentEffects = [] # EffectNodes for the current song plus the submaster ones
 
        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.songTimeFetch = 0
 
        self.songIsPlaying = False
 
        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")
 
        self.lastSendLevelsTime = 0
 
        reactor.callLater(self.period, self.sendLevels)
 
        reactor.callLater(self.period, self.updateTimeFromMusic)
 

	
 
    def setEffects(self):
 
        self.currentEffects = []
 
        log.info('setEffects currentSong=%s', self.currentSong)
 
        if self.currentSong is None:
 
            return
 
        
 
        for effectUri in self.graph.objects(self.currentSong, L9['effect']):
 
            self.currentEffects.append(EffectNode(self.graph, effectUri))
 

	
 

	
 
        for sub in self.graph.subjects(RDF.type, L9['Submaster']):
 
            for effectUri in self.graph.objects(sub, L9['drivesEffect']):
 
                self.currentEffects.append(EffectNode(self.graph, effectUri))
 
                
 
        log.info('now we have %s effects', len(self.currentEffects))
 
        
 
    @inlineCallbacks
 
    def getSongTime(self):
 
        now = time.time()
 
        old = now - self.requestTime
 
        if old > self.coastSecs:
 
            try:
 
                response = json.loads((yield cyclone.httpclient.fetch(
 
                    networking.musicPlayer.path('time'), timeout=.5)).body)
 
            except TimeoutError:
 
                log.warning("TimeoutError: using stale time from %.1f ago", old)
 
            else:
 
                self.requestTime = now
 
                self.currentPlaying = response['playing']
 
                self.songTimeFromRequest = response['t']
 
                returnValue(
 
                    (response['t'], (response['song'] and URIRef(response['song']))))
 

	
 
        estimated = self.songTimeFromRequest
 
        if self.currentSong is not None and self.currentPlaying:
 
            estimated += now - self.requestTime
 
        returnValue((estimated, self.currentSong))
 

	
 

	
 
    @inlineCallbacks
 
    def updateTimeFromMusic(self):
 
        t1 = time.time()
 
        with self.stats.getMusic.time():
 
            self.songTime, song = yield self.getSongTime()
 
            self.songTimeFetch = time.time()
 

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

	
 
        elapsed = time.time() - t1
 
        reactor.callLater(max(0, self.period - elapsed), self.updateTimeFromMusic)
 

	
 
    def estimatedSongTime(self):
 
        now = time.time()
 
        t = self.songTime
 
        if self.currentPlaying:
 
            t += max(0, now - self.songTimeFetch)
 
        return t
 

	
 
    @inlineCallbacks
 
    def sendLevels(self):
 
        print ''
 
        t1 = time.time()
 
        log.debug("time since last call: %.1f ms" % (1000 * (t1 - self.lastSendLevelsTime)))
 
        self.lastSendLevelsTime = t1
 
        try:
 
            with self.stats.sendLevels.time():
 
                if self.currentSong is not None:
 
                    log.debug('allEffectOutputs')
 
                    with self.stats.evals.time():
 
                        outputs = self.allEffectOutputs(self.estimatedSongTime())
 
                    log.debug('combineOutputs')
 
                    combined = self.combineOutputs(outputs)
 
                    self.logLevels(t1, combined)
 
                    log.debug('sendOutput')
 
                    with self.stats.sendOutput.time():
 
                        yield self.sendOutput(combined)
 
                
 
                elapsed = time.time() - t1
 
                dt = max(0, self.period - elapsed)
 
        except Exception:
 
            self.stats.errors += 1
 
            traceback.print_exc()
 
            dt = .5
 

	
 
        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)]
 
        out = Submaster.sub_maxes(*outputs)
 

	
 
        return out
 
        
 
    @inlineCallbacks
 
    def sendOutput(self, combined):
 
        dmx = combined.get_dmx_list()
 
        yield dmxclient.outputlevels(dmx, twisted=True)
 
        
 
    def allEffectOutputs(self, songTime):
 
        outputs = []
 
        for e in self.currentEffects:
 
            try:
 
                out = e.eval(songTime)
 
                if isinstance(out, (list, tuple)):
 
                    outputs.extend(out)
 
                else:
 
                    outputs.append(out)
 
            except Exception as exc:
 
                now = time.time()
 
                if now > self.lastErrorLog + 5:
 
                    if hasattr(exc, 'expr'):
 
                        log.exception('in expression %r', exc.expr)
 
                    log.error("effect %s: %s" % (e.uri, exc))
 
                    self.lastErrorLog = now
 
        log.debug('eval %s effects, got %s outputs', len(self.currentEffects), len(outputs))
 
                    
 
        return outputs
 
        
 
    def logLevels(self, now, out):
 
        # this would look nice on the top of the effecteval web pages too
 
        if log.isEnabledFor(logging.DEBUG):
 
            log.debug(self.logMessage(out))
 
        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()))
 

	
 
Z = numpy.zeros((50, 3), dtype=numpy.float16)
 

	
 
class ControlBoard(object):
 
    def __init__(self, dev='/dev/serial/by-id/usb-FTDI_FT232R_USB_UART_A7027NYX-if00-port0'):
 
        self._dev = serial.Serial(dev, baudrate=115200)
 

	
 
    def _8bitMessage(self, floatArray):
 
        px255 = (numpy.clip(floatArray, 0, 1) * 255).astype(numpy.uint8)
 
        return px255.reshape((-1,)).tostring()
 
        
 
    def setStrip(self, which, pixels):
 
        """
 
        which: 0 or 1 to pick the strip
 
        pixels: (50, 3) array of 0..1 floats
 
        """
 
        command = {0: '\x00', 1: '\x01'}[which]
 
        if pixels.shape != (50, 3):
 
            raise ValueError("pixels was %s" % pixels.shape)
 
        self._dev.write('\x60' + command + self._8bitMessage(pixels))
 
        self._dev.flush()
 

	
 
    def setUv(self, which, level):
 
        """
 
        which: 0 or 1
 
        level: 0 to 1
 
        """
 
        command = {0: '\x02', 1: '\x03'}[which]
 
        self._dev.write('\x60' + command + chr(int(max(0, min(1, level)) * 255)))
 
        self._dev.flush()
 

	
 
    def setRgb(self, color):
 
        """
 
        color: (1, 3) array of 0..1 floats
 
        """
 
        if color.shape != (1, 3):
 
            raise ValueError("color was %s" % color.shape)
 
        self._dev.write('\x60\x04' + self._8bitMessage(color))
 
        self._dev.flush()
 

	
 
        
 
class LedLoop(EffectLoop):
 
    def initOutput(self):
 
        self.board = ControlBoard()
 
        self.lastSent = {} # what's in arduino's memory
 
        
 
    def combineOutputs(self, outputs):
 
        combined = {'L': Z, 'R': Z,
 
                    'blacklight0': 0, 'blacklight1': 0,
 
                    'W': numpy.zeros((1, 3), dtype=numpy.float16)}
 
        
 
        for out in outputs:
 
            log.debug('combine output %r', out)
 
            if isinstance(out, Effects.Blacklight):
 
                # no picking yet
 
                #key = 'blacklight%s' % out.which
 
                for key in ['blacklight0', 'blacklight1']:
 
                    combined[key] = max(combined[key], out)
 
            elif isinstance(out, Effects.Strip):
 
                pixels = numpy.array(out.pixels, dtype=numpy.float16)
 
                for w in out.which:
 
                    combined[w] = numpy.maximum(
 
                        combined[w], pixels[:1,:] if w == 'W' else pixels)
 
                
 
        return combined
 

	
 
    @inlineCallbacks
 
    def sendOutput(self, combined):
 
        for meth, selectArgs, value in [
 
                ('setStrip', (0,), combined['L']),
 
                ('setStrip', (1,), combined['R']),
 
                ('setUv', (0,), combined['blacklight0']),
 
                ('setUv', (1,), combined['blacklight1']),
 
                ('setRgb', (), combined['W']),
 
            ]:
 
            key = (meth, selectArgs)
 
            compValue = value.tolist() if isinstance(value, numpy.ndarray) else value
 
            
 
            if self.lastSent.get(key) == compValue:
 
                continue
 

	
 
            log.debug('value changed: %s(%s %s)', meth, selectArgs, value)
 
            
 
            getattr(self.board, meth)(*(selectArgs + (value,)))
 
            self.lastSent[key] = compValue
 
                
 
        yield succeed(None) # there was an attempt at an async send
 
        
 
    def logMessage(self, out):
 
        return str([(w, p.tolist() if isinstance(p, numpy.ndarray) else p) for w,p in out.items()])
 

	
 
def makeEffectLoop(graph, stats, outputWhere):
 
    if outputWhere == 'dmx':
 
        return EffectLoop(graph, stats)
 
    elif outputWhere == 'leds':
 
        return LedLoop(graph, stats)
 
    else:
 
        raise NotImplementedError("unknown output system %r" % outputWhere)
 

	
 
        
0 comments (0 inline, 0 general)