changeset 1776:b680d6f50a93

start sequencer web report. WIP Ignore-this: e97eadee6190d2c90cfb81f3542f4f2f
author drewp@bigasterisk.com
date Sun, 03 Jun 2018 21:04:00 +0000
parents 64db0e6b2be9
children 299d49de85a8
files bin/effectsequencer light9/effect/sequencer.html light9/effect/sequencer.py
diffstat 3 files changed, 220 insertions(+), 7 deletions(-) [+]
line wrap: on
line diff
--- a/bin/effectsequencer	Sun Jun 03 18:46:36 2018 +0000
+++ b/bin/effectsequencer	Sun Jun 03 21:04:00 2018 +0000
@@ -12,7 +12,7 @@
 import optparse, sys, logging
 import cyclone.web
 from rdflib import URIRef
-from light9.effect.sequencer import Sequencer, sendToCollector
+from light9.effect.sequencer import Sequencer, sendToCollector, Updates
 from light9 import clientsession
 
 class App(object):
@@ -38,9 +38,13 @@
                                              settings))
 
         self.cycloneApp = cyclone.web.Application(handlers=[
+            (r'/()', cyclone.web.StaticFileHandler,
+             {"path" : "light9/effect/", "default_filename" : "sequencer.html"}),
+            (r'/updates', Updates),
             (r'/stats', StatsForCyclone),
         ],
                                                   debug=True,
+                                                  seq=self.seq,
                                                   graph=self.graph,
                                                   stats=self.stats)
         reactor.listenTCP(networking.effectSequencer.port, self.cycloneApp)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/light9/effect/sequencer.html	Sun Jun 03 21:04:00 2018 +0000
@@ -0,0 +1,167 @@
+<!doctype html>
+<html>
+  <head>
+    <title>effect sequencer</title>
+    <meta charset="utf-8" />
+    <script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+    <link rel="import" href="/lib/polymer/polymer.html">
+    <link rel="import" href="/lib/iron-ajax/iron-ajax.html">
+    <link rel="import" href="../rdfdb-synced-graph.html">
+    <link rel="import" href="../resource-display.html">
+    <script src="/node_modules/n3/n3-browser.js"></script> 
+    <script src="/lib/async/dist/async.js"></script>
+    <script src="/lib/underscore/underscore-min.js"></script>
+
+    <link rel="stylesheet"  href="/style.css">
+    <style>
+     td { white-space: nowrap; }
+    </style>
+  </head>
+  <body>
+
+    <dom-module id="light9-collector-device">
+      <template>
+        <style>
+         :host {
+             display: block;
+             break-inside: avoid-column;
+         }
+         h3 {
+             margin-top: 12px;
+             margin-bottom: 0;
+         }
+         td.nonzero {
+             background: #310202;
+             color: #e25757;
+         }
+         td.full {
+             background: #2b0000;
+             color: red;
+             font-weight: bold;
+         }
+        </style>
+        <h3><resource-display graph="{{graph}}" uri="{{uri}}"></resource-display></h3>
+        <table class="borders">
+          <tr>
+            <th>output attr</th>
+            <th>value</th>
+            <th>output chan</th>
+          </tr>
+          <template is="dom-repeat" items="{{attrs}}">
+            <tr>
+              <td>{{item.attr}}</td>
+              <td class$="{{item.valClass}}">{{item.val}} →</td>
+              <td>{{item.chan}}</td>
+            </tr>
+          </template>
+
+      </template>
+      <script>
+       HTMLImports.whenReady(function () {
+           Polymer({
+               is: "light9-collector-device",
+               properties: {
+                   graph: {type: Object, notify: true},
+                   uri: {type: Object, notify: true},
+                   attrs: {type: Array, notify: true},
+               },
+               observers: [
+                   "initUpdates(updates)",
+               ],
+               initUpdates: function(updates) {
+                   updates.addListener(function(msg) {
+                       if (msg.outputAttrsSet && msg.outputAttrsSet.dev == this.uri.value) {
+                           this.set('attrs', msg.outputAttrsSet.attrs);
+                           this.attrs.forEach(function(row) {
+                               row.valClass = row.val == 255 ? 'full' : (row.val ? 'nonzero' : '');
+                           });
+                       }
+                   }.bind(this));
+               },
+           });
+       });
+      </script>
+    </dom-module>
+
+
+    <dom-module id="light9-collector-ui">
+      <template>
+        <rdfdb-synced-graph graph="{{graph}}"></rdfdb-synced-graph>
+
+        <h1>Collector <a href="stats">[stats]</a></h1>
+
+        <h2>Devices</h2>
+        <div style="column-width: 18em">
+        <template is="dom-repeat" items="{{devices}}">
+          <light9-collector-device
+              graph="{{graph}}" updates="{{updates}}"
+              uri="{{item}}"></light9-collector-device>
+        </template>
+        </div>
+      </template>
+      <script>
+       class Updates {
+           constructor() {
+               this.listeners = [];
+               
+           }
+           addListener(cb) {
+               this.listeners.push(cb);
+           }
+           onMessage(msg) {
+               this.listeners.forEach(function(lis) {
+                   lis(msg);
+               });
+           }
+       }
+       HTMLImports.whenReady(function () {
+           Polymer({
+               is: "light9-collector-ui",
+               properties: {
+                   graph: {type: Object, notify: true},
+                   updates: {type: Object, notify: true},
+                   devices: {type: Array},
+               },
+               observers: [
+                   'onGraph(graph)',
+               ],
+               ready: function() {
+                   this.updates = new Updates();
+                   var sock = new WebSocket(
+                       window.location.href.replace(/^http/, 'ws') + 'updates');
+                   sock.onmessage = function(ev) {
+                       this.updates.onMessage(JSON.parse(ev.data));
+                   }.bind(this);
+               },
+               onGraph: function(graph) {
+                   this.graph.runHandler(this.findDevices.bind(this), 'findDevices');
+               },
+               findDevices: function() {
+                   var U = function(x) {
+                       return this.graph.Uri(x);
+                   };
+                   this.set('devices', []);
+
+                   let classes = this.graph.subjects(U('rdf:type'), U(':DeviceClass'));
+                   _.uniq(_.sortBy(classes, 'value'), true).forEach(function(dc) {
+                       _.sortBy(this.graph.subjects(U('rdf:type'), dc), 'value').forEach(function(dev) {
+                           this.push('devices', dev);
+                       }.bind(this));
+                   }.bind(this));
+               }
+           });
+       });
+      </script>
+    </dom-module>
+    
+    list notes of song, time with note, eff attrs, function stuff
+    update rates, stutters from reloads.
+    <script>
+
+     var source = new EventSource('updates');
+     source.onmessage = function(e) {
+       console.log(JSON.parse(e.data));
+     };
+    </script>
+  </body>
+</html>
--- a/light9/effect/sequencer.py	Sun Jun 03 18:46:36 2018 +0000
+++ b/light9/effect/sequencer.py	Sun Jun 03 21:04:00 2018 +0000
@@ -12,6 +12,7 @@
 import time
 from twisted.internet.inotify import INotify
 from twisted.python.filepath import FilePath
+from louie import dispatcher
 
 from light9 import networking
 from light9.namespaces import L9, RDF
@@ -131,15 +132,21 @@
         
     def outputSettings(self, t):
         """
-        list of (device, attr, value)
+        list of (device, attr, value), and a report for web
         """
+        report = {'note': str(self.uri)}
         effectSettings = self.baseEffectSettings.copy()
         effectSettings[L9['strength']] = self.evalCurve(t)
-        return self.effectEval.outputFromEffect(
+        report['effectSettings'] = dict(
+            (str(k), str(v))
+            for k,v in sorted(effectSettings.items()))
+        out = self.effectEval.outputFromEffect(
             effectSettings.items(),
             songTime=t,
             # note: not using origin here since it's going away
             noteTime=t - self.points[0][0])
+        print 'out', out.asList()
+        return out, report
 
 
 class CodeWatcher(object):
@@ -207,19 +214,54 @@
         self.recentUpdateTimes = self.recentUpdateTimes[-20:] + [now]
         stats.recentFps = len(self.recentUpdateTimes) / (self.recentUpdateTimes[-1] - self.recentUpdateTimes[0] + .0001)
         if now > self.lastStatLog + 10:
-            log.info("%.2f fps", stats.recentFps)
+            dispatcher.send('state', update={'recentFps': stats.recentFps})
             self.lastStatLog = now
         
         reactor.callLater(1 / self.fps, self.update)
 
         musicState = self.music.getLatest()
         song = URIRef(musicState['song']) if musicState.get('song') else None
+        dispatcher.send('state', update={'song': str(song)})
         if 't' not in musicState:
             return
         t = musicState['t']
 
         settings = []
+        songNotes = sorted(self.notes.get(song, []))
+        noteReports = []
+        for note in songNotes:
+            s, report = note.outputSettings(t)
+            noteReports.append(report)
+            settings.append(s)
+        dispatcher.send('state', update={'songNotes': noteReports})
+        self.sendToCollector(DeviceSettings.fromList(self.graph, settings))
+
+import cyclone.sse
+class Updates(cyclone.sse.SSEHandler):
+    def __init__(self, application, request, **kwargs):
+        cyclone.sse.SSEHandler.__init__(self, application, request,
+                                        **kwargs)
+        self.state = {}
+        dispatcher.connect(self.updateState, 'state')
+        self.numConnected = 0
+
+    def updateState(self, update):
+        self.state.update(update)
         
-        for note in self.notes.get(song, []):
-            settings.append(note.outputSettings(t))
-        self.sendToCollector(DeviceSettings.fromList(self.graph, settings))
+    def bind(self):
+        print 'new client', self.settings.seq
+        self.numConnected += 1
+        
+        if self.numConnected == 1:
+            self.loop()
+
+    def loop(self):
+        if self.numConnected == 0:
+            return
+        self.sendEvent(self.state)
+        reactor.callLater(2, self.loop)
+        
+    def unbind(self):
+        self.numConnected -= 1
+        print 'bye', self.numConnected
+