#!bin/python from run_local import log from twisted.internet import reactor, task from twisted.internet.defer import inlineCallbacks import cyclone.web, cyclone.websocket, cyclone.httpclient import sys, optparse, logging, subprocess, json, re, time from rdflib import URIRef, RDF sys.path.append(".") from light9 import networking, showconfig, Submaster, dmxclient from light9.rdfdb.syncedgraph import SyncedGraph from light9.curvecalc.curve import Curve from light9.namespaces import L9, DCTERMS sys.path.append("/my/proj/homeauto/lib") sys.path.append("/home/drewp/projects/homeauto/lib") from cycloneerr import PrettyErrorHandler class EffectEdit(cyclone.web.RequestHandler): def get(self): self.write(open("light9/effecteval/effect.html").read()) class EffectData(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 self.sendMessage({'code': self.graph.value(self.uri, L9['code'])}) 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 uriFromCode(s): # i thought this was something a graph could do with its namespace manager if s.startswith('sub:'): return URIRef('http://light9.bigasterisk.com/show/dance2014/sub/' + s[4:]) if s.startswith('song1:'): return URIRef('http://ex/effect/song1/' + s[6:]) raise NotImplementedError class EffectNode(object): def __init__(self, graph, uri): self.graph, self.uri = graph, uri self.graph.addHandler(self.prepare) def prepare(self): self.code = self.graph.value(self.uri, L9['code']) m = re.match(r'^out = sub\((.*?), intensity=(.*?)\)', self.code) if not m: raise NotImplementedError subUri = uriFromCode(m.group(1)) subs = Submaster.get_global_submasters(self.graph) self.sub = subs.get_sub_by_uri(subUri) intensityCurve = uriFromCode(m.group(2)) self.curve = Curve() self.curve.set_from_string(self.graph.value(intensityCurve, L9['points'])) def eval(self, songTime): # consider http://waxeye.org/ for a parser that can be used in py and js level = self.curve.eval(songTime) scaledSubs = self.sub * level return scaledSubs 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 # Or, we could own that loop, like this: @inlineCallbacks def effectLoop(graph): t1 = time.time() response = json.loads((yield cyclone.httpclient.fetch( networking.musicPlayer.path('time'))).body) song = URIRef(response['song']) songTime = response['t'] # Possibilities to make this shut up about graph copies: # - implement the cheap readonly currentState response # - do multiple little currentState calls (in this code) over just # the required triples # - use addHandler instead and only fire dmx when there is a data # change (and also somehow call it when there is a time change) outSubs = [] with graph.currentState(tripleFilter=(song, L9['effect'], None)) as g: for effectUri in g.objects(song, L9['effect']): node = EffectNode(graph, effectUri) outSubs.append(node.eval(songTime)) out = Submaster.sub_maxes(*outSubs) # out.get_levels() for a more readable view dmx = out.get_dmx_list() if log.isEnabledFor(logging.DEBUG): log.debug("send dmx: %r", out.get_levels()) yield dmxclient.outputlevels(dmx, twisted=True) loopTime = time.time() - t1 log.debug('loopTime %.1f ms', 1000 * loopTime) class App(object): def __init__(self, show): self.show = show self.graph = SyncedGraph("effectEval") 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'/(knockout-2\.2\.1\.js)', SFH, {'path': 'light9/subserver/'}), (r'/effect\.js', StaticCoffee, {'src': 'light9/effecteval/effect.coffee'}), (r'/effectData', EffectData), (r'/static/(.*)', SFH, {'path': 'static/'}), (r'/effect/eval', EffectEval), (r'/songEffects/eval', SongEffectsEval), ], debug=True, graph=self.graph) self.graph.initiallySynced.addCallback(self.launch) def launch(self, *args): task.LoopingCall(effectLoop, self.graph).start(1) 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") (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)) if options.twistedlog: from twisted.python import log as twlog twlog.startLogging(sys.stderr) reactor.listenTCP(networking.effectEval.port, app.cycloneApp) log.info("listening on %s" % networking.effectEval.port) reactor.run()