Changeset - aa45e5379c5a
[Not reviewed]
default
0 4 1
Drew Perttula - 11 years ago 2014-05-28 05:57:08
drewp@bigasterisk.com
effecteval improvements. displays current song+effect tree
Ignore-this: 94a007b1206c45483d74878fb35248d
5 files changed with 59 insertions and 13 deletions:
0 comments (0 inline, 0 general)
bin/effecteval
Show inline comments
 
@@ -11,25 +11,41 @@ from light9 import networking, showconfi
 
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):
 
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'] = 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):
 
@@ -59,26 +75,32 @@ class EffectNode(object):
 

	
 
    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()
 

	
 
        # read from disk ok? how do we know to reread? start with
 
        # mtime. the mtime check could be done occasionally so on
 
        # average we read at most one curve's mtime per effectLoop.       
 
        
 
        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'))
 
@@ -98,71 +120,75 @@ class SongEffectsEval(PrettyErrorHandler
 
        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)
 
    if response['song'] is not None:
 
    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']):
 
                # these should be built once, not per (frequent) update
 
            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")
 
        self.graph.initiallySynced.addCallback(self.launch)
 

	
 
    def launch(self, *args):
 
        task.LoopingCall(effectLoop, self.graph).start(1)    
 
        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'/effect\.js', StaticCoffee, {'src': 'light9/effecteval/effect.coffee'}),
 
            (r'/index\.js', StaticCoffee, {'src': 'light9/effecteval/index.coffee'}),
 
            (r'/effectUpdates', EffectUpdates),
 
            (r'/songEffectsUpdates', SongEffectsUpdates),
 
            (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)    
 
        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__":
 
@@ -176,15 +202,13 @@ if __name__ == "__main__":
 
                      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()
light9/effecteval/effect.coffee
Show inline comments
 
qs = new QueryString()
 
model =
 
  uri: ko.observable(qs.value('uri'))
 
  code: ko.observable()
 
reconnectingWebSocket "ws://localhost:8070/effectData" + window.location.search, (msg) ->
 
  
 
reconnectingWebSocket "ws://localhost:8070/effectUpdates" + window.location.search, (msg) ->
 
  console.log('effectData ' + JSON.stringify(msg))
 
  # there's a shorter unpack thing
 
    
 
  model.code(msg.code)
 
  
 
  writeBack = ko.computed ->
 
    console.log('sendback' ,{code: model.code()})
 
    
 
  model.code(msg.code)
 
  ko.applyBindings(model)
 
  
 
\ No newline at end of file
light9/effecteval/effect.html
Show inline comments
 
<!doctype html>
 
<html>
 
  <head>
 
    <title>effect</title>
 
    <meta charset="utf-8" />
 
  </head>
 
  <body>
 
    effect page for URI (pull from param)
 
    <a href="./">Effects</a> / <a data-bind="attr: {href: uri}, text: uri"></a>
 

	
 
    <div>code: <input type="text" size="100" data-bind="value: code"></input></div>
 
    
 
    <script src="static/jquery-2.1.1.min.js"></script>
 
    <script src="static/knockout-3.1.0.js"></script>
 
    <script src="static/websocket.js"></script>
 
    <script src="static/QueryString.js"></script>
 
    <script src="effect.js"></script>
 
  </body>
 
</html>
light9/effecteval/index.coffee
Show inline comments
 
new file 100644
 
model =
 
  songs: ko.observableArray([])
 

	
 
reconnectingWebSocket "ws://localhost:8070/songEffectsUpdates", (msg) ->
 
  console.log(msg.songs)
 
  model.songs(msg.songs)
 
  
 
ko.applyBindings(model)
 

	
 
  # there's a shorter unpack thing
 
  #writeBack = ko.computed ->
 
  #  console.log('sendback' ,{code: model.code()})
 
  
 
\ No newline at end of file
light9/effecteval/index.html
Show inline comments
 
<!doctype html>
 
<html>
 
  <head>
 
    <title>effecteval</title>
 
    <meta charset="utf-8" />
 
  </head>
 
  <body>
 
    All effect instances:
 
    <!-- subscribe to a query of all effects and their songs -->
 
    <ul>
 
      <li><a href="effect?uri=http://ex/effect/song1/openingLook">song1 -> opening look</a></li>
 
      <li><a href="effect?uri=http://ex/effect/song1/full">song1 -> full</a></li>
 
    <ul data-bind="foreach: songs">
 
      <li>
 
        <a data-bind="attr: {href: uri}">Song <span data-bind="text: label"></span></a>
 
      </li>
 
      <ul data-bind="foreach: effects">
 
        <li><a data-bind="attr: {href: 'effect?'+jQuery.param({uri: $data})}, text: $data"></a></li>
 
      </ul>
 
    </ul>
 
    <script src="static/jquery-2.1.1.min.js"></script>
 
    <script src="static/knockout-3.1.0.js"></script>
 
    <script src="static/websocket.js"></script>
 
    <script src="index.js"></script>
 
  </body>
 
</html>
0 comments (0 inline, 0 general)