#!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.set_header('Content-Type', 'text/html') 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([ (song, L9['curve'], curve, ctx), (effect, RDFS.label, droppedLabel, ctx), (effect, L9['code'], Literal('env = %s' % curve.n3()), ctx), ]) if L9['EffectClass'] in droppedTypes: quads.extend([ (effect, RDF.type, dropped, ctx), ] + [(effect, L9['code'], c, ctx) for c in droppedCodes]) elif L9['Submaster'] in droppedTypes: quads.extend([ (effect, L9['code'], Literal('out = %s * env' % dropped.n3()), ctx), ]) else: raise NotImplementedError( "don't know how to add an effect from %r (types=%r)" % (dropped, droppedTypes)) maybeAddMusicLine(quads, effect, song, ctx) print "adding" for qq in quads: print qq graph.patch(Patch(addQuads=quads)) class SongEffectsUpdates(cyclone.websocket.WebSocketHandler): def connectionMade(self, *args, **kwargs): self.graph = self.settings.graph self.graph.addHandler(self.updateClient) def updateClient(self): # todo: abort if client is gone playlist = self.graph.value(showconfig.showUri(), L9['playList']) songs = list(self.graph.items(playlist)) out = [] for s in songs: out.append({'uri': s, 'label': self.graph.label(s)}) out[-1]['effects'] = [{'uri': uri, 'label': self.graph.label(uri)} for uri in sorted(self.graph.objects(s, L9['effect']))] self.sendMessage({'songs': out}) class EffectUpdates(cyclone.websocket.WebSocketHandler): """ stays alive for the life of the effect page """ def connectionMade(self, *args, **kwargs): log.info("websocket opened") self.uri = URIRef(self.get_argument('uri')) self.sendMessage({'hello': repr(self)}) self.graph = self.settings.graph self.graph.addHandler(self.updateClient) def updateClient(self): # todo: if client has dropped, abort and don't get any more # graph updates # EffectNode knows how to put them in order. Somehow this is # not triggering an update when the order changes. en = EffectNode(self.graph, self.uri) codeLines = [c.code for c in en.codes] self.sendMessage({'codeLines': codeLines}) def connectionLost(self, reason): log.info("websocket closed") def messageReceived(self, message): log.info("got message %s" % message) # write a patch back to the graph def replaceObjects(graph, c, s, p, newObjs): patch = graph.getObjectPatch( context=c, subject=s, predicate=p, newObject=newObjs[0]) moreAdds = [] for line in newObjs[1:]: moreAdds.append((s, p, line, c)) fullPatch = Patch(delQuads=patch.delQuads, addQuads=patch.addQuads + moreAdds) graph.patch(fullPatch) class Code(PrettyErrorHandler, cyclone.web.RequestHandler): def put(self): effect = URIRef(self.get_argument('uri')) codeLines = [] for i in itertools.count(0): k = 'codeLines[%s][text]' % i v = self.get_argument(k, None) if v is not None: codeLines.append(Literal(v)) else: break if not codeLines: log.info("no codelines received on PUT /code") return with self.settings.graph.currentState( tripleFilter=(None, L9['effect'], effect)) as g: song = g.subjects(L9['effect'], effect).next() replaceObjects(self.settings.graph, song, effect, L9['code'], codeLines) # right here we could tell if the code has a python error and return it self.send_error(202) class EffectEval(PrettyErrorHandler, cyclone.web.RequestHandler): @inlineCallbacks def get(self): # return dmx list for that effect uri = URIRef(self.get_argument('uri')) response = yield cyclone.httpclient.fetch( networking.musicPlayer.path('time')) songTime = json.loads(response.body)['t'] node = EffectNode(self.settings.graph, uri) outSub = node.eval(songTime) self.write(json.dumps(outSub.get_dmx_list())) # Completely not sure where the effect background loop should # go. Another process could own it, and get this request repeatedly: class SongEffectsEval(PrettyErrorHandler, cyclone.web.RequestHandler): def get(self): song = URIRef(self.get_argument('song')) effects = effectsForSong(self.settings.graph, song) raise NotImplementedError self.write(maxDict(effectDmxDict(e) for e in effects)) # return dmx dict for all effects in the song, already combined class App(object): def __init__(self, show, outputWhere): self.show = show 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('evals'), scales.PmfStat('sendOutput'), scales.IntStat('errors'), ) def launch(self, *args): 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'/effect\.js', StaticCoffee, {'src': 'light9/effecteval/effect.coffee'}), (r'/(effect-components\.html)', SFH, {'path': 'light9/effecteval'}), (r'/effectUpdates', EffectUpdates), (r'/code', Code), (r'/songEffectsUpdates', SongEffectsUpdates), (r'/effect/eval', EffectEval), (r'/songEffects', SongEffects), (r'/songEffects/eval', SongEffectsEval), (r'/stats', StatsForCyclone), ], debug=True, graph=self.graph, stats=self.stats) reactor.listenTCP(networking.effectEval.port, self.cycloneApp) log.info("listening on %s" % networking.effectEval.port) class StaticCoffee(PrettyErrorHandler, cyclone.web.RequestHandler): def initialize(self, src): super(StaticCoffee, self).initialize() self.src = src def get(self): self.set_header('Content-Type', 'application/javascript') self.write(subprocess.check_output([ '/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), options.output) if options.twistedlog: from twisted.python import log as twlog twlog.startLogging(sys.stderr) reactor.run()