Changeset - 92ffad96fd8a
[Not reviewed]
default
0 3 0
drewp@bigasterisk.com - 11 years ago 2014-06-15 18:04:55
drewp@bigasterisk.com
more error detail on bad expressions
Ignore-this: 684cbe10927c70152bd280e3982028d8
3 files changed with 8 insertions and 1 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('/usr/lib/pymodules/python2.7/') # for numpy, on rpi
 
sys.path.append('/usr/lib/python2.7/dist-packages') # For numpy
 
from light9 import networking, showconfig, Submaster, dmxclient
 
from light9.curvecalc import musicaccess
 
from light9.curvecalc.curve import CurveResource
 
from light9.effecteval.effect import EffectNode
 
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):
 
        graph = self.settings.graph
 
        uri = URIRef(self.get_argument('uri'))
 
        with graph.currentState(tripleFilter=(None, L9['effect'], uri)) as g:
 
            song = ctx = list(g.subjects(L9['effect'], uri))[0]
 
        self.settings.graph.patch(Patch(delQuads=[
 
            (song, L9['effect'], uri, ctx),
 
            ]))
 
        
 
def clamp(x, lo, hi):
 
    return max(lo, min(hi, x))
 

	
 
@inlineCallbacks
 
def getMusicStatus():
 
    returnValue(json.loads((yield cyclone.httpclient.fetch(
 
        networking.musicPlayer.path('time'), timeout=.5)).body))
 
    
 
@inlineCallbacks
 
def newEnvelopeCurve(graph, ctx, uri, label):
 
    """this does its own patch to the graph"""
 
    
 
    cr = CurveResource(graph, uri)
 
    cr.newCurve(ctx, label=Literal(label))
 
    yield insertEnvelopePoints(cr.curve)
 
    cr.saveCurve()
 

	
 
@inlineCallbacks
 
def insertEnvelopePoints(curve):
 
    # wrong: we might not be adding to the currently-playing song.
 
    musicStatus = yield getMusicStatus()
 
    songTime=musicStatus['t']
 
    songDuration=musicStatus['duration']
 
    
 
    fade = 2
 
    t1 = clamp(songTime - fade, .1, songDuration - .1 * 2) + fade
 
    t2 = clamp(songTime + 20, t1 + .1, songDuration)
 
    
 
    curve.insert_pt((t1 - fade, 0))
 
    curve.insert_pt((t1, 1))
 
    curve.insert_pt((t2, 1))
 
    curve.insert_pt((t2 + fade, 0))
 
    
 
    
 
def newEffect(graph, song, ctx):
 
    effect = graph.sequentialUri(song + "/effect-")
 
    quads = [
 
        (song, L9['effect'], effect, ctx),
 
        (effect, RDF.type, L9['Effect'], ctx),
 
    ]
 
    print "newEffect", effect, quads
 
    return effect, quads
 
    
 
def musicCurveForSong(uri):
 
    return URIRef(uri + 'music')
 

	
 
def maybeAddMusicLine(quads, effect, song, ctx):
 
    """
 
    add a line getting the current music into 'music' if any code might
 
    be mentioning that var
 
    """
 
    
 
    for spoc in quads:
 
        if spoc[1] == L9['code'] and 'music' in spoc[2]:
 
            quads.extend([
 
                (effect, L9['code'],
 
                 Literal('music = %s' % musicCurveForSong(song).n3()), ctx)
 
                ])
 
            break
 

	
 
@inlineCallbacks
 
def currentSong():
 
    s = (yield getMusicStatus())['song']
 
    if s is None:
 
        raise ValueError("no current song")
 
    returnValue(URIRef(s))
 

	
 
def songHasEffect(graph, song, uri):
 
    """does this song have an effect of class uri or a sub curve for sub
 
    uri? this should be simpler to look up."""
 
    return False # todo
 
    
 
class SongEffects(PrettyErrorHandler, cyclone.web.RequestHandler):
 
    def wideOpenCors(self):
 
        self.set_header('Access-Control-Allow-Origin', '*')
 
        self.set_header('Access-Control-Allow-Methods', 'GET, PUT, POST, DELETE, OPTIONS')
 
        self.set_header('Access-Control-Max-Age', '1000')
 
        self.set_header('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With')
 
    
 
    def options(self):
 
        self.wideOpenCors()
 
        self.write('')
 

	
 
    @inlineCallbacks
 
    def post(self):
 
        self.wideOpenCors()
 
        dropped = URIRef(self.get_argument('drop'))
 

	
 
        try:
 
            song = URIRef(self.get_argument('uri'))
 
        except Exception: # which?
 
            song = yield currentSong()
 
            
 
        log.info("adding to %s", song)
 

	
 
        ctx = song
 
        graph = self.settings.graph
 
        
 
        with graph.currentState(
 
                tripleFilter=(dropped, None, None)) as g:
 
            droppedTypes = list(g.objects(dropped, RDF.type))
 
            droppedLabel = g.label(dropped)
 
            droppedCodes = list(g.objects(dropped, L9['code']))
 

	
 
        quads = []
 

	
 
        if songHasEffect(graph, song, dropped):
 
            # bump the existing curve
 
            pass
 
        else:
 
            effect, q = newEffect(graph, song, ctx)
 
            quads.extend(q)
 

	
 
            curve = graph.sequentialUri(song + "/curve-")
 
            yield newEnvelopeCurve(graph, ctx, curve, droppedLabel)
 
            quads.extend([
light9/effecteval/effect.py
Show inline comments
 
@@ -40,102 +40,106 @@ class CodeLine(object):
 
            return i >= len(prefix) and s[i-len(prefix):i] == prefix
 

	
 
        def repl(m):
 
            v = '_res%s' % self.uriCounter
 
            self.uriCounter += 1
 
            r = resources[v] = URIRef(m.group(1))
 
            if self._uriIsCurve(r):
 
                if not alreadyInCurveFunc(m.string, m.start()):
 
                    return 'curve(%s, t)' % v
 
            return v
 
        outExpr = re.sub(r'<(http\S*?)>', repl, expr)
 
        return lname, expr, outExpr, resources
 

	
 
    def findVars(self, expr):
 
        """may return some more strings than just the vars"""
 
        withoutUris = re.sub(r'<(http\S*?)>', 'None', expr)
 
        tokens = set(re.findall(r'\b([a-zA-Z_]\w*)\b', withoutUris))
 
        tokens.discard('None')
 
        return tokens
 
        
 
    def _uriIsCurve(self, uri):
 
        # this result could vary with graph changes (rare)
 
        return self.graph.contains((uri, RDF.type, L9['Curve']))
 
        
 
    @prof.logTime
 
    def _resourcesAsPython(self, resources):
 
        """
 
        mapping of the local names for uris in the code to high-level
 
        objects (Submaster, Curve)
 
        """
 
        out = {}
 
        subs = prof.logTime(Submaster.get_global_submasters)(self.graph)
 
        for localVar, uri in resources.items():
 
            
 
            for rdfClass in self.graph.objects(uri, RDF.type):
 
                if rdfClass == L9['Curve']:
 
                    cr = CurveResource(self.graph, uri)
 
                    # this is slow- pool these curves somewhere, maybe just with curveset
 
                    prof.logTime(cr.loadCurve)()
 
                    out[localVar] = cr.curve
 
                    break
 
                elif rdfClass == L9['Submaster']:
 
                    out[localVar] = subs.get_sub_by_uri(uri)
 
                    break
 
                else:
 
                    out[localVar] = CouldNotConvert(uri)
 
                    break
 
            else:
 
                out[localVar] = CouldNotConvert(uri)
 

	
 
        return out
 
        
 
class EffectNode(object):
 
    def __init__(self, graph, uri):
 
        self.graph, self.uri = graph, uri
 
        # this is not expiring at the right time, when an effect goes away
 
        self.graph.addHandler(self.prepare)
 

	
 
    @prof.logTime
 
    def prepare(self):
 
        log.info("prepare effect %s", self.uri)
 
        # maybe there can be multiple lines of code as multiple
 
        # objects here, and we sort them by dependencies
 
        codeStrs = list(self.graph.objects(self.uri, L9['code']))
 
        if not codeStrs:
 
            raise ValueError("effect %s has no code" % self.uri)
 

	
 
        self.codes = [CodeLine(self.graph, s) for s in codeStrs]
 

	
 
        self.sortCodes()
 

	
 
        #reload(Effects)
 
        self.otherFuncs = prof.logTime(Effects.configExprGlobals)()
 

	
 
    def sortCodes(self):
 
        """put self.codes in a working evaluation order"""
 
        codeFromOutput = dict((c.outName, c) for c in self.codes)
 
        deps = {}
 
        for c in self.codes:
 
            outName = c.outName
 
            inNames = c.possibleVars.intersection(codeFromOutput.keys())
 
            inNames.discard(outName)
 
            deps[outName] = inNames
 
        self.codes = [codeFromOutput[n] for n in toposort.toposort_flatten(deps)]
 
        
 
    def eval(self, songTime):
 
        ns = {'t': songTime}
 
        ns.update(self.otherFuncs)
 

	
 
        ns.update(dict(
 
            curve=lambda c, t: c.eval(t),
 
            ))
 

	
 
        for c in self.codes:
 
            codeNs = ns.copy()
 
            codeNs.update(c.pyResources)
 
            lineOut = eval(c.expr, codeNs)
 
            try:
 
                lineOut = eval(c.expr, codeNs)
 
            except Exception as e:
 
                e.expr = c.expr
 
                raise e
 
            ns[c.outName] = lineOut
 
        if 'out' not in ns:
 
            log.error("ran code for %s, didn't make an 'out' value", self.uri)
 
        return ns['out']
 

	
light9/effecteval/effectloop.py
Show inline comments
 
@@ -54,176 +54,178 @@ class EffectLoop(object):
 
            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):
 
        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:
 
                    with self.stats.evals.time():
 
                        outputs = self.allEffectOutputs(self.estimatedSongTime())
 
                    combined = self.combineOutputs(outputs)
 
                    self.logLevels(t1, combined)
 
                    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.error('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.uint8)
 

	
 
class LedLoop(EffectLoop):
 
    def initOutput(self):
 
        kw = dict(baudrate=115200)
 
        self.boards = {
 
            'L': serial.Serial('/dev/serial/by-id/usb-FTDI_FT232R_USB_UART_A7027JI6-if00-port0', **kw),
 
            'R': serial.Serial('/dev/serial/by-id/usb-FTDI_FT232R_USB_UART_A7027NYX-if00-port0', **kw),
 
        }
 
        self.lastSentBacklight = None
 
        
 
    def combineOutputs(self, outputs):
 
        combined = {'L': Z, 'R': Z, 'blacklight': 0}
 
        
 
        for out in outputs:
 
            if isinstance(out, Effects.Blacklight):
 
                combined['blacklight'] = max(combined['blacklight'], int(out * 255))
 
            elif isinstance(out, Effects.Strip):
 
                pixels = numpy.array(out.pixels, dtype=numpy.float16)
 
                px255 = (numpy.clip(pixels, 0, 1) * 255).astype(numpy.uint8)
 
                for w in out.which:
 
                    combined[w] = numpy.maximum(combined[w], px255)
 
                
 
        return combined
 

	
 
    @inlineCallbacks
 
    def sendOutput(self, combined):
 
        for which, px255 in combined.items():
 
            if which == 'blacklight':
 
                if px255 != self.lastSentBacklight:
 
                    b = min(255, max(0, px255))
 
                    yield succeed(self.serialWrite(self.boards['L'],
 
                                                   '\x60\x01' + chr(b)))
 
                    self.lastSentBacklight = px255
 
            else:
 
                board = self.boards[which]
 
                msg = '\x60\x00' + px255.reshape((-1,)).tostring()
 
                # may be stuttering more, and not smoother
 
                #yield threads.deferToThread(self.serialWrite, board, msg)
 
                yield succeed(self.serialWrite(board, msg))
 

	
 
    def serialWrite(self, serial, msg):
 
        serial.write(msg)
 
        serial.flush()
 
        
 
    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)