#!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, 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 from light9.effecteval.effect import EffectNode from light9.effect.edit import getMusicStatus, songNotePatch from light9.effecteval.effectloop import makeEffectLoop from light9.greplin_cyclone import StatsForCyclone from light9.namespaces import L9 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), ])) @inlineCallbacks def currentSong(): s = (yield getMusicStatus())['song'] if s is None: raise ValueError("no current song") returnValue(URIRef(s)) 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() event = self.get_argument('event', default='default') log.info("adding to %s", song) p = yield songNotePatch(self.settings.graph, dropped, song, event, ctx=song) self.settings.graph.patch(p) self.settings.graph.suggestPrefixes({'song': URIRef(song + '/')}) 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).addErrback(log.error) self.stats = scales.collection('/', scales.PmfStat('sendLevels'), scales.PmfStat('getMusic'), scales.PmfStat('evals'), scales.PmfStat('sendOutput'), scales.IntStat('errors'), ) def launch(self, *args): log.info('launch') if self.outputWhere: 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()