changeset 1858:7772cc48e016

reformat all python Ignore-this: 1135b78893f8b3d31badddda7f45678f
author drewp@bigasterisk.com
date Tue, 21 May 2019 23:56:12 +0000
parents 3f1b9b9b0505
children f066d6e874db
files bin/ascoltami2 bin/bcf_puppet_demo bin/bumppad bin/captureDevice bin/clientdemo bin/collector bin/collector_loadtest.py bin/curvecalc bin/dmx_color_test.py bin/dmxserver bin/effecteval bin/effectsequencer bin/homepageConfig bin/inputdemo bin/inputquneo bin/kcclient bin/keyboardcomposer bin/lightsim bin/listsongs bin/mpd_timing_test bin/musicPad bin/musictime bin/paintserver bin/picamserve bin/rdfdb bin/run_local.py bin/staticclient bin/subcomposer bin/subserver bin/tkdnd_minimal_drop.py bin/tracker bin/vidref bin/vidrefsetup bin/wavecurve bin/webcontrol light9/Effects.py light9/Fadable.py light9/FlyingFader.py light9/Patch.py light9/ascoltami/player.py light9/ascoltami/playlist.py light9/ascoltami/webapp.py light9/chase.py light9/clientsession.py light9/collector/collector.py light9/collector/collector_client.py light9/collector/collector_test.py light9/collector/device.py light9/collector/device_test.py light9/collector/output.py light9/collector/output_test.py light9/curvecalc/client.py light9/curvecalc/cursors.py light9/curvecalc/curve.py light9/curvecalc/curveedit.py light9/curvecalc/curveview.py light9/curvecalc/musicaccess.py light9/curvecalc/output.py light9/curvecalc/subterm.py light9/curvecalc/subtermview.py light9/curvecalc/zoomcontrol.py light9/dmxchanedit.py light9/dmxclient.py light9/editchoice.py light9/editchoicegtk.py light9/effect/edit.py light9/effect/effecteval.py light9/effect/scale.py light9/effect/sequencer.py light9/effect/settings.py light9/effect/settings_test.py light9/effect/simple_outputs.py light9/effecteval/effect.py light9/effecteval/effectloop.py light9/effecteval/test_effect.py light9/greplin_cyclone.py light9/gtkpyconsole.py light9/io/__init__.py light9/io/udmx.py light9/namespaces.py light9/networking.py light9/observable.py light9/paint/capture.py light9/paint/solve.py light9/paint/solve_test.py light9/prof.py light9/subcomposer/subcomposerweb.py light9/vidref/main.py light9/vidref/musictime.py light9/vidref/qt_test.py light9/vidref/remotepivideo.py light9/vidref/replay.py light9/vidref/videorecorder.py
diffstat 93 files changed, 3498 insertions(+), 2407 deletions(-) [+]
line wrap: on
line diff
--- a/bin/ascoltami2	Tue May 21 23:55:35 2019 +0000
+++ b/bin/ascoltami2	Tue May 21 23:56:12 2019 +0000
@@ -4,7 +4,7 @@
 import web, thread, sys, optparse, logging
 from rdflib import URIRef
 sys.path.append(".")
-sys.path.append('/usr/lib/python2.7/dist-packages') # For gi
+sys.path.append('/usr/lib/python2.7/dist-packages')  # For gi
 
 import gi
 gi.require_version('Gst', '1.0')
@@ -17,7 +17,9 @@
 
 from gi.repository import GObject, Gst, Gtk
 
+
 class App(object):
+
     def __init__(self, graph, show):
         self.graph = graph
         self.player = Player(onEOS=self.onEOS)
@@ -32,21 +34,27 @@
 
         try:
             nextSong = self.playlist.nextSong(thisSongUri)
-        except NoSuchSong: # we're at the end of the playlist
+        except NoSuchSong:  # we're at the end of the playlist
             return
 
         self.player.setSong(songLocation(graph, nextSong), play=False)
 
+
 if __name__ == "__main__":
     GObject.threads_init()
     Gst.init(None)
 
     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",
+    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",
+    parser.add_option("--twistedlog",
+                      action="store_true",
                       help="twisted logging")
     (options, args) = parser.parse_args()
 
@@ -54,7 +62,7 @@
 
     if not options.show:
         raise ValueError("missing --show http://...")
-            
+
     graph = showconfig.getGraph()
     app = App(graph, URIRef(options.show))
     if options.twistedlog:
--- a/bin/bcf_puppet_demo	Tue May 21 23:55:35 2019 +0000
+++ b/bin/bcf_puppet_demo	Tue May 21 23:56:12 2019 +0000
@@ -5,12 +5,13 @@
 from bcf2000 import BCF2000
 from twisted.internet import reactor
 
+
 class PuppetSliders(BCF2000):
+
     def valueIn(self, name, value):
         if name == 'slider1':
             self.valueOut('slider5', value)
 
 
-
 b = PuppetSliders()
 reactor.run()
--- a/bin/bumppad	Tue May 21 23:55:35 2019 +0000
+++ b/bin/bumppad	Tue May 21 23:56:12 2019 +0000
@@ -1,5 +1,5 @@
 #!bin/python
-from __future__ import division,nested_scopes
+from __future__ import division, nested_scopes
 import sys, time, math
 import Tkinter as tk
 
@@ -7,64 +7,82 @@
 import light9.dmxclient as dmxclient
 from light9.TLUtility import make_attributes_from_args
 
-from light9.Submaster import Submaster,sub_maxes
+from light9.Submaster import Submaster, sub_maxes
+
 
 class pad(tk.Frame):
-    levs = None # Submaster : level
-    def __init__(self,master,root,mag):
-        make_attributes_from_args('master','mag')
-        tk.Frame.__init__(self,master)
-        self.levs={}
-        for xy,key,subname in [
-            ((1,1),'KP_Up','centered'),
-            ((1,3),"KP_Down",'third-c'),
-            ((0,2),'KP_Left','scoop-l'),
-            ((2,2),'KP_Right','scoop-r'),
-            ((1,0),'KP_Divide','cyc'),
-            ((0,3),"KP_End",'hottest'),
-            ((2,3),'KP_Next','deepblues'),
-            ((0,4),'KP_Insert',"zip_red"),
-            ((2,4),'KP_Delete',"zip_orange"),
-            ((3,1),'KP_Add','strobedim'),
-            ((3,3),'KP_Enter','zip_blue'),
-            ((1,2),'KP_Begin','scoop-c'),                            
-                        ]:
-            
+    levs = None  # Submaster : level
+
+    def __init__(self, master, root, mag):
+        make_attributes_from_args('master', 'mag')
+        tk.Frame.__init__(self, master)
+        self.levs = {}
+        for xy, key, subname in [
+            ((1, 1), 'KP_Up', 'centered'),
+            ((1, 3), "KP_Down", 'third-c'),
+            ((0, 2), 'KP_Left', 'scoop-l'),
+            ((2, 2), 'KP_Right', 'scoop-r'),
+            ((1, 0), 'KP_Divide', 'cyc'),
+            ((0, 3), "KP_End", 'hottest'),
+            ((2, 3), 'KP_Next', 'deepblues'),
+            ((0, 4), 'KP_Insert', "zip_red"),
+            ((2, 4), 'KP_Delete', "zip_orange"),
+            ((3, 1), 'KP_Add', 'strobedim'),
+            ((3, 3), 'KP_Enter', 'zip_blue'),
+            ((1, 2), 'KP_Begin', 'scoop-c'),
+        ]:
+
             sub = Submaster(subname)
-            self.levs[sub]=0
-            
-            l = tk.Label(self,font="arial 12 bold",anchor='w',height=2,
-                         relief='groove',bd=5,
-                         text="%s\n%s" % (key.replace('KP_',''),sub.name))
-            l.grid(column=xy[0],row=xy[1],sticky='news')
-            
-            root.bind("<KeyPress-%s>"%key,
-                      lambda ev,sub=sub: self.bumpto(sub,1))
-            root.bind("<KeyRelease-%s>"%key,
-                      lambda ev,sub=sub: self.bumpto(sub,0))
-    def bumpto(self,sub,lev):
-        now=time.time()
-        self.levs[sub]=lev*self.mag.get()
+            self.levs[sub] = 0
+
+            l = tk.Label(self,
+                         font="arial 12 bold",
+                         anchor='w',
+                         height=2,
+                         relief='groove',
+                         bd=5,
+                         text="%s\n%s" % (key.replace('KP_', ''), sub.name))
+            l.grid(column=xy[0], row=xy[1], sticky='news')
+
+            root.bind(
+                "<KeyPress-%s>" % key, lambda ev, sub=sub: self.bumpto(sub, 1))
+            root.bind("<KeyRelease-%s>" % key,
+                      lambda ev, sub=sub: self.bumpto(sub, 0))
+
+    def bumpto(self, sub, lev):
+        now = time.time()
+        self.levs[sub] = lev * self.mag.get()
         self.master.after_idle(self.output)
+
     def output(self):
-        dmx = sub_maxes(*[s*l for s,l in self.levs.items()]).get_dmx_list()
-        dmxclient.outputlevels(dmx,clientid="bumppad")
-        
-root=tk.Tk()
+        dmx = sub_maxes(*[s * l for s, l in self.levs.items()]).get_dmx_list()
+        dmxclient.outputlevels(dmx, clientid="bumppad")
+
+
+root = tk.Tk()
 root.tk_setPalette("maroon4")
 root.wm_title("bumppad")
 mag = tk.DoubleVar()
 
-tk.Label(root,text="Keypad press/release activate sub; 1..5 set mag",
-             font="Helvetica -12 italic",anchor='w').pack(side='bottom',fill='x')
-                      
-pad(root,root,mag).pack(side='left',fill='both',exp=1)
+tk.Label(root,
+         text="Keypad press/release activate sub; 1..5 set mag",
+         font="Helvetica -12 italic",
+         anchor='w').pack(side='bottom', fill='x')
+
+pad(root, root, mag).pack(side='left', fill='both', exp=1)
 
-magscl = tk.Scale(root,orient='vertical',from_=1,to=0,res=.01,
-                   showval=1,variable=mag,label='mag',relief='raised',bd=1)
-for i in range(1,6):
-    root.bind("<Key-%s>"%i,lambda ev,i=i: mag.set(math.sqrt((i )/5)))
-magscl.pack(side='left',fill='y')
-
+magscl = tk.Scale(root,
+                  orient='vertical',
+                  from_=1,
+                  to=0,
+                  res=.01,
+                  showval=1,
+                  variable=mag,
+                  label='mag',
+                  relief='raised',
+                  bd=1)
+for i in range(1, 6):
+    root.bind("<Key-%s>" % i, lambda ev, i=i: mag.set(math.sqrt((i) / 5)))
+magscl.pack(side='left', fill='y')
 
 root.mainloop()
--- a/bin/captureDevice	Tue May 21 23:55:35 2019 +0000
+++ b/bin/captureDevice	Tue May 21 23:56:12 2019 +0000
@@ -26,15 +26,18 @@
 
 stats = scales.collection('/webServer', scales.PmfStat('setAttr'))
 
+
 class Camera(object):
+
     def __init__(self, imageUrl):
         self.imageUrl = imageUrl
-    
+
     def takePic(self, uri, writePath):
         log.info('takePic %s', uri)
-        return treq.get(self.imageUrl).addCallbacks(
-            lambda r: self._done(writePath, r), log.error)
-        
+        return treq.get(
+            self.imageUrl).addCallbacks(lambda r: self._done(writePath, r),
+                                        log.error)
+
     @inlineCallbacks
     def _done(self, writePath, response):
         jpg = yield response.content()
@@ -46,18 +49,21 @@
             out.write(jpg)
         log.info('wrote %s', writePath)
 
+
 def deferSleep(sec):
     d = Deferred()
     reactor.callLater(sec, d.callback, None)
     return d
-    
+
+
 class Capture(object):
     firstMoveTime = 3
     settleTime = .5
+
     def __init__(self, graph, dev):
         self.graph = graph
         self.dev = dev
-        
+
         def steps(a, b, n):
             return [round(a + (b - a) * i / n, 5) for i in range(n)]
 
@@ -72,8 +78,8 @@
         # aura
         rxSteps = steps(0.15, .95, 10)
         rySteps = steps(0, .9, 5)
-        zoomSteps = steps(.6, .9, 3)       
-        
+        zoomSteps = steps(.6, .9, 3)
+
         row = 0
         for ry in rySteps:
             xSteps = rxSteps[:]
@@ -82,31 +88,36 @@
             row += 1
             for rx in xSteps:
                 for zoom in zoomSteps:
-                    self.toGather.append(DeviceSettings(graph, [
-                        (dev, L9['rx'], rx),
-                        (dev, L9['ry'], ry),
-                        (dev, L9['color'], '#ffffff'),
-                        (dev, L9['zoom'], zoom),
-                        #(dev, L9['focus'], 0.13),
-                    ]))
+                    self.toGather.append(
+                        DeviceSettings(
+                            graph,
+                            [
+                                (dev, L9['rx'], rx),
+                                (dev, L9['ry'], ry),
+                                (dev, L9['color'], '#ffffff'),
+                                (dev, L9['zoom'], zoom),
+                                #(dev, L9['focus'], 0.13),
+                            ]))
 
         self.devTail = dev.rsplit('/')[-1]
-        self.session = URIRef('/'.join([showconfig.showUri(),
-                                   'capture', self.devTail, self.captureId]))
+        self.session = URIRef('/'.join(
+            [showconfig.showUri(), 'capture', self.devTail, self.captureId]))
         self.ctx = URIRef(self.session + '/index')
-                
-        self.graph.patch(Patch(addQuads=[
-            (self.session, RDF.type, L9['CaptureSession'], self.ctx),
-        ]))
-                
+
+        self.graph.patch(
+            Patch(addQuads=[
+                (self.session, RDF.type, L9['CaptureSession'], self.ctx),
+            ]))
+
         self.numPics = 0
         self.settingsCache = set()
         self.step().addErrback(log.error)
 
     def off(self):
-        return sendToCollector(client='captureDevice', session='main',
+        return sendToCollector(client='captureDevice',
+                               session='main',
                                settings=DeviceSettings(self.graph, []))
-        
+
     @inlineCallbacks
     def step(self):
         if not self.toGather:
@@ -115,61 +126,73 @@
             reactor.stop()
             return
         settings = self.toGather.pop()
-        
+
         log.info('[%s left] move to %r', len(self.toGather), settings)
-        yield sendToCollector(client='captureDevice', session='main',
+        yield sendToCollector(client='captureDevice',
+                              session='main',
                               settings=settings)
-        
-        yield deferSleep(self.firstMoveTime if self.numPics == 0 else
-                         self.settleTime)
-        
+
+        yield deferSleep(self.firstMoveTime if self.numPics ==
+                         0 else self.settleTime)
+
         picId = 'pic%s' % self.numPics
-        path = '/'.join([
-            'capture', self.devTail, self.captureId, picId]) + '.jpg'
+        path = '/'.join(['capture', self.devTail, self.captureId, picId
+                        ]) + '.jpg'
         uri = URIRef(self.session + '/' + picId)
-        
+
         yield camera.takePic(uri, os.path.join(showconfig.root(), path))
         self.numPics += 1
 
         writeCaptureDescription(self.graph, self.ctx, self.session, uri,
-                                self.dev,
-                                path, self.settingsCache, settings)
-        
+                                self.dev, path, self.settingsCache, settings)
+
         reactor.callLater(0, self.step)
 
-        
 
-camera = Camera('http://plus:8200/picamserve/pic?res=1080&resize=800&iso=800&redgain=1.6&bluegain=1.6&shutter=60000&x=0&w=1&y=0&h=.952')
+camera = Camera(
+    'http://plus:8200/picamserve/pic?res=1080&resize=800&iso=800&redgain=1.6&bluegain=1.6&shutter=60000&x=0&w=1&y=0&h=.952'
+)
+
 
 class Attrs(PrettyErrorHandler, cyclone.web.RequestHandler):
+
     def put(self):
         with stats.setAttr.time():
-            client, clientSession, settings, sendTime = parseJsonMessage(self.request.body)
+            client, clientSession, settings, sendTime = parseJsonMessage(
+                self.request.body)
             self.set_status(202)
 
+
 def launch(graph):
 
     cap = Capture(graph, dev=L9['device/aura5'])
     reactor.listenTCP(networking.captureDevice.port,
                       cyclone.web.Application(handlers=[
-                          (r'/()', cyclone.web.StaticFileHandler,
-                           {"path" : "light9/web", "default_filename" : "captureDevice.html"}),
+                          (r'/()', cyclone.web.StaticFileHandler, {
+                              "path": "light9/web",
+                              "default_filename": "captureDevice.html"
+                          }),
                           (r'/stats', StatsForCyclone),
                       ]),
                       interface='::')
     log.info('serving http on %s', networking.captureDevice.port)
-    
+
+
 def main():
     parser = optparse.OptionParser()
-    parser.add_option("-v", "--verbose", action="store_true",
+    parser.add_option("-v",
+                      "--verbose",
+                      action="store_true",
                       help="logging.DEBUG")
     (options, args) = parser.parse_args()
     log.setLevel(logging.DEBUG if options.verbose else logging.INFO)
 
     graph = SyncedGraph(networking.rdfdb.url, "captureDevice")
 
-    graph.initiallySynced.addCallback(lambda _: launch(graph)).addErrback(log.error)
+    graph.initiallySynced.addCallback(lambda _: launch(graph)).addErrback(
+        log.error)
     reactor.run()
 
+
 if __name__ == '__main__':
     main()
--- a/bin/clientdemo	Tue May 21 23:55:35 2019 +0000
+++ b/bin/clientdemo	Tue May 21 23:56:12 2019 +0000
@@ -16,12 +16,13 @@
     g = SyncedGraph(networking.rdfdb.url, "clientdemo")
 
     from light9.Submaster import PersistentSubmaster
-    sub = PersistentSubmaster(graph=g, uri=URIRef("http://light9.bigasterisk.com/sub/bcools"))
+    sub = PersistentSubmaster(
+        graph=g, uri=URIRef("http://light9.bigasterisk.com/sub/bcools"))
 
     #get sub to show its updating name, then push that all the way into KC gui so we can see just names refresh in there
 
+    L9 = Namespace("http://light9.bigasterisk.com/")
 
-    L9 = Namespace("http://light9.bigasterisk.com/")
     def updateDemoValue():
         v = list(g.objects(L9['demo'], L9['is']))
         print "demo value is %r" % v
@@ -29,8 +30,10 @@
     g.addHandler(updateDemoValue)
 
     def adj():
-        g.patch(Patch(addQuads=[(L9['demo'], L9['is'], Literal(os.getpid()),
-                                 L9['clientdemo'])],
-                      delQuads=[]))
+        g.patch(
+            Patch(addQuads=[(L9['demo'], L9['is'], Literal(os.getpid()),
+                             L9['clientdemo'])],
+                  delQuads=[]))
+
     reactor.callLater(2, adj)
     reactor.run()
--- a/bin/collector	Tue May 21 23:55:35 2019 +0000
+++ b/bin/collector	Tue May 21 23:56:12 2019 +0000
@@ -29,6 +29,7 @@
 from rdfdb.syncedgraph import SyncedGraph
 from light9.greplin_cyclone import StatsForCyclone
 
+
 def parseJsonMessage(msg):
     body = json.loads(msg)
     settings = []
@@ -40,14 +41,15 @@
         settings.append((URIRef(device), URIRef(attr), value))
     return body['client'], body['clientSession'], settings, body['sendTime']
 
+
 def startZmq(port, collector):
-    stats = scales.collection('/zmqServer',
-                              scales.PmfStat('setAttr'))
-    
+    stats = scales.collection('/zmqServer', scales.PmfStat('setAttr'))
+
     zf = ZmqFactory()
     addr = 'tcp://*:%s' % port
     log.info('creating zmq endpoint at %r', addr)
     e = ZmqEndpoint('bind', addr)
+
     class Pull(ZmqPullConnection):
         #highWaterMark = 3
         def onPull(self, message):
@@ -55,26 +57,28 @@
                 # todo: new compressed protocol where you send all URIs up
                 # front and then use small ints to refer to devices and
                 # attributes in subsequent requests.
-                client, clientSession, settings, sendTime = parseJsonMessage(message[0])
+                client, clientSession, settings, sendTime = parseJsonMessage(
+                    message[0])
                 collector.setAttrs(client, clientSession, settings, sendTime)
-        
+
     s = Pull(zf, e)
 
 
 class WebListeners(object):
+
     def __init__(self):
         self.clients = []
-        self.pendingMessageForDev = {} # dev: (attrs, outputmap)
+        self.pendingMessageForDev = {}  # dev: (attrs, outputmap)
         self.lastFlush = 0
-        
+
     def addClient(self, client):
-        self.clients.append([client, {}]) # seen = {dev: attrs}
+        self.clients.append([client, {}])  # seen = {dev: attrs}
         log.info('added client %s %s', len(self.clients), client)
 
     def delClient(self, client):
         self.clients = [[c, t] for c, t in self.clients if c != client]
         log.info('delClient %s, %s left', client, len(self.clients))
-        
+
     def outputAttrsSet(self, dev, attrs, outputMap):
         """called often- don't be slow"""
 
@@ -94,8 +98,8 @@
         while self.pendingMessageForDev:
             dev, (attrs, outputMap) = self.pendingMessageForDev.popitem()
 
-            msg = None # lazy, since makeMsg is slow
-            
+            msg = None  # lazy, since makeMsg is slow
+
             # this omits repeats, but can still send many
             # messages/sec. Not sure if piling up messages for the browser
             # could lead to slowdowns in the real dmx output.
@@ -112,16 +116,23 @@
         attrRows = []
         for attr, val in attrs.items():
             output, index = outputMap[(dev, attr)]
-            attrRows.append({'attr': attr.rsplit('/')[-1],
-                             'val': val,
-                             'chan': (output.shortId(), index + 1)})
+            attrRows.append({
+                'attr': attr.rsplit('/')[-1],
+                'val': val,
+                'chan': (output.shortId(), index + 1)
+            })
         attrRows.sort(key=lambda r: r['chan'])
         for row in attrRows:
             row['chan'] = '%s %s' % (row['chan'][0], row['chan'][1])
 
-        msg = json.dumps({'outputAttrsSet': {'dev': dev, 'attrs': attrRows}}, sort_keys=True)
+        msg = json.dumps({'outputAttrsSet': {
+            'dev': dev,
+            'attrs': attrRows
+        }},
+                         sort_keys=True)
         return msg
-    
+
+
 class Updates(cyclone.websocket.WebSocketHandler):
 
     def connectionMade(self, *args, **kwargs):
@@ -134,23 +145,27 @@
     def messageReceived(self, message):
         json.loads(message)
 
+
 stats = scales.collection('/webServer', scales.PmfStat('setAttr'))
 
+
 class Attrs(PrettyErrorHandler, cyclone.web.RequestHandler):
+
     def put(self):
         with stats.setAttr.time():
-            client, clientSession, settings, sendTime = parseJsonMessage(self.request.body)
-            self.settings.collector.setAttrs(client, clientSession, settings, sendTime)
+            client, clientSession, settings, sendTime = parseJsonMessage(
+                self.request.body)
+            self.settings.collector.setAttrs(client, clientSession, settings,
+                                             sendTime)
             self.set_status(202)
 
 
-            
 def launch(graph, doLoadTest=False):
     try:
         # todo: drive outputs with config files
         outputs = [
             # EnttecDmx(L9['output/dmxA/'], '/dev/dmx3', 80),
-             Udmx(L9['output/dmxA/'], bus=5, numChannels=80),
+            Udmx(L9['output/dmxA/'], bus=5, numChannels=80),
             #DummyOutput(L9['output/dmxA/'], 80),
             Udmx(L9['output/dmxB/'], bus=7, numChannels=500),
         ]
@@ -162,15 +177,19 @@
     c = Collector(graph, outputs, listeners)
 
     startZmq(networking.collectorZmq.port, c)
-    
+
     reactor.listenTCP(networking.collector.port,
                       cyclone.web.Application(handlers=[
-                          (r'/()', cyclone.web.StaticFileHandler,
-                           {"path" : "light9/collector/web", "default_filename" : "index.html"}),
+                          (r'/()', cyclone.web.StaticFileHandler, {
+                              "path": "light9/collector/web",
+                              "default_filename": "index.html"
+                          }),
                           (r'/updates', Updates),
                           (r'/attrs', Attrs),
                           (r'/stats', StatsForCyclone),
-                      ], collector=c, listeners=listeners),
+                      ],
+                                              collector=c,
+                                              listeners=listeners),
                       interface='::')
     log.info('serving http on %s, zmq on %s', networking.collector.port,
              networking.collectorZmq.port)
@@ -180,28 +199,38 @@
         # requests when there's free time
         def afterWarmup():
             log.info('running collector_loadtest')
-            d = utils.getProcessValue('bin/python', ['bin/collector_loadtest.py'])
+            d = utils.getProcessValue('bin/python',
+                                      ['bin/collector_loadtest.py'])
+
             def done(*a):
                 log.info('loadtest done')
                 reactor.stop()
+
             d.addCallback(done)
+
         reactor.callLater(2, afterWarmup)
-    
+
+
 def main():
     parser = optparse.OptionParser()
-    parser.add_option("-v", "--verbose", action="store_true",
+    parser.add_option("-v",
+                      "--verbose",
+                      action="store_true",
                       help="logging.DEBUG")
-    parser.add_option("--loadtest", action="store_true",
+    parser.add_option("--loadtest",
+                      action="store_true",
                       help="call myself with some synthetic load then exit")
     (options, args) = parser.parse_args()
     log.setLevel(logging.DEBUG if options.verbose else logging.INFO)
 
     logging.getLogger('colormath').setLevel(logging.INFO)
-    
+
     graph = SyncedGraph(networking.rdfdb.url, "collector")
 
-    graph.initiallySynced.addCallback(lambda _: launch(graph, options.loadtest)).addErrback(lambda e: reactor.crash())
+    graph.initiallySynced.addCallback(lambda _: launch(graph, options.loadtest)
+                                     ).addErrback(lambda e: reactor.crash())
     reactor.run()
 
+
 if __name__ == '__main__':
     main()
--- a/bin/collector_loadtest.py	Tue May 21 23:55:35 2019 +0000
+++ b/bin/collector_loadtest.py	Tue May 21 23:56:12 2019 +0000
@@ -7,6 +7,8 @@
 import time
 import logging
 log.setLevel(logging.DEBUG)
+
+
 def loadTest():
     print "scheduling loadtest"
     n = 2500
@@ -14,23 +16,27 @@
     session = "loadtest%s" % time.time()
     offset = 0
     for i in range(n):
+
         def send(i):
             if i % 100 == 0:
                 log.info('sendToCollector %s', i)
             d = sendToCollector("http://localhost:999999/", session,
-                    [[DEV["backlight1"], L9["color"], "#ffffff"], 
-                     [DEV["backlight2"], L9["color"], "#ffffff"], 
-                     [DEV["backlight3"], L9["color"], "#ffffff"], 
-                     [DEV["backlight4"], L9["color"], "#ffffff"], 
-                     [DEV["backlight5"], L9["color"], "#ffffff"], 
-                     [DEV["down2"], L9["color"], "#ffffff"], 
-                     [DEV["down3"], L9["color"], "#ffffff"], 
-                     [DEV["down4"], L9["color"], "#ffffff"], 
-                     [DEV["houseSide"], L9["level"], .8], 
-                     [DEV["backlight5"], L9["uv"], 0.011]])
+                                [[DEV["backlight1"], L9["color"], "#ffffff"],
+                                 [DEV["backlight2"], L9["color"], "#ffffff"],
+                                 [DEV["backlight3"], L9["color"], "#ffffff"],
+                                 [DEV["backlight4"], L9["color"], "#ffffff"],
+                                 [DEV["backlight5"], L9["color"], "#ffffff"],
+                                 [DEV["down2"], L9["color"], "#ffffff"],
+                                 [DEV["down3"], L9["color"], "#ffffff"],
+                                 [DEV["down4"], L9["color"], "#ffffff"],
+                                 [DEV["houseSide"], L9["level"], .8],
+                                 [DEV["backlight5"], L9["uv"], 0.011]])
+
             def ontime(dt, i=i):
                 times[i] = dt
+
             d.addCallback(ontime)
+
         reactor.callLater(offset, send, i)
         offset += .002
 
@@ -39,8 +45,10 @@
         with open('/tmp/times', 'w') as f:
             f.write(''.join('%s\n' % t for t in times))
         reactor.stop()
-    reactor.callLater(offset+.5, done)
+
+    reactor.callLater(offset + .5, done)
     reactor.run()
 
+
 if __name__ == '__main__':
     loadTest()
--- a/bin/curvecalc	Tue May 21 23:55:35 2019 +0000
+++ b/bin/curvecalc	Tue May 21 23:56:12 2019 +0000
@@ -1,5 +1,4 @@
 #!bin/python
-
 """
 now launches like this:
 % bin/curvecalc http://light9.bigasterisk.com/show/dance2007/song1
@@ -12,7 +11,7 @@
 from __future__ import division
 
 import sys
-sys.path.append('/usr/lib/python2.7/dist-packages') # For gtk
+sys.path.append('/usr/lib/python2.7/dist-packages')  # For gtk
 from twisted.internet import gtk3reactor
 gtk3reactor.install()
 from twisted.internet import reactor
@@ -24,13 +23,13 @@
 from gi.repository import Gdk
 
 from urlparse import parse_qsl
-import louie as dispatcher 
+import louie as dispatcher
 from rdflib import URIRef, Literal, RDF, RDFS
 import logging
 
 from run_local import log
 from light9 import showconfig, networking
-from light9.curvecalc import curveview 
+from light9.curvecalc import curveview
 from light9.curvecalc.curve import Curveset
 from light9.curvecalc.curveedit import serveCurveEdit
 from light9.curvecalc.musicaccess import Music
@@ -46,34 +45,40 @@
 from rdfdb.syncedgraph import SyncedGraph
 from light9.wavelength import wavelength
 
+
 class SubtermExists(ValueError):
     pass
 
+
 class Main(object):
+
     def __init__(self, graph, opts, session, curveset, music):
         self.graph, self.opts, self.session = graph, opts, session
         self.curveset, self.music = curveset, music
         self.lastSeenInputTime = 0
-        self.currentSubterms = [] # Subterm objects that are synced to the graph
+        self.currentSubterms = [
+        ]  # Subterm objects that are synced to the graph
 
         self.setTheme()
         wtree = self.wtree = Gtk.Builder()
         wtree.add_from_file("light9/curvecalc/curvecalc.glade")
         mainwin = wtree.get_object("MainWindow")
-        
+
         mainwin.connect("destroy", self.onQuit)
         wtree.connect_signals(self)
 
         mainwin.show_all()
 
         mainwin.connect("delete-event", lambda *args: reactor.crash())
+
         def updateTitle():
-            mainwin.set_title("curvecalc - %s" %
-                              graph.label(
-                                  graph.value(session, L9['currentSong'])))
+            mainwin.set_title(
+                "curvecalc - %s" %
+                graph.label(graph.value(session, L9['currentSong'])))
+
         graph.addHandler(updateTitle)
 
-        songChoice = Observable(None) # to be connected with the session song
+        songChoice = Observable(None)  # to be connected with the session song
 
         self.registerGraphToSongChoice(wtree, session, graph, songChoice)
         self.registerSongChoiceToGraph(session, graph, songChoice)
@@ -82,28 +87,31 @@
         ec = EditChoice(graph, songChoice, label="Editing song:")
         wtree.get_object("currentSongEditChoice").add(ec)
         ec.show()
-        
+
         wtree.get_object("subterms").connect("add", self.onSubtermChildAdded)
-        
-        self.refreshCurveView()       
-        
+
+        self.refreshCurveView()
+
         self.makeStatusLines(wtree.get_object("status"))
         self.setupNewSubZone()
         self.acceptDragsOnCurveViews()
-                
+
         # may not work
         wtree.get_object("paned1").set_position(600)
 
     def registerGraphToSongChoice(self, wtree, session, graph, songChoice):
+
         def setSong():
             current = graph.value(session, L9['currentSong'])
             if not wtree.get_object("followPlayerSongChoice").get_active():
                 songChoice(current)
                 dispatcher.send("song_has_changed")
+
         graph.addHandler(setSong)
 
     def registerSongChoiceToGraph(self, session, graph, songChoice):
         self.muteSongChoiceUntil = 0
+
         def songChoiceToGraph(newSong):
             if newSong is Local:
                 raise NotImplementedError('what do i patch')
@@ -116,12 +124,12 @@
                 log.debug('muted')
                 return
             self.muteSongChoiceUntil = now + 1
-            
-            graph.patchObject(context=session, subject=session,
-                              predicate=L9['currentSong'], newObject=newSong)
-            
-            
-            
+
+            graph.patchObject(context=session,
+                              subject=session,
+                              predicate=L9['currentSong'],
+                              newObject=newSong)
+
         songChoice.subscribe(songChoiceToGraph)
 
     def registerCurrentPlayerSongToUi(self, wtree, graph, songChoice):
@@ -129,47 +137,51 @@
         and
         current_player_song 'song' param -> songChoice, if you're in autofollow
         """
+
         def current_player_song(song):
             # (this is run on every frame)
             ps = wtree.get_object("playerSong")
             if URIRef(ps.get_uri()) != song:
                 log.debug("update playerSong to %s", ps.get_uri())
+
                 def setLabel():
                     ps.set_label(graph.label(song))
+
                 graph.addHandler(setLabel)
                 ps.set_uri(song)
             if song != songChoice():
                 if wtree.get_object("followPlayerSongChoice").get_active():
                     log.debug('followPlayerSongChoice is on')
                     songChoice(song)
-                
+
         dispatcher.connect(current_player_song, "current_player_song")
         self.current_player_song = current_player_song
-        
+
     def setupNewSubZone(self):
         self.wtree.get_object("newSubZone").drag_dest_set(
             flags=Gtk.DestDefaults.ALL,
             targets=[Gtk.TargetEntry('text/uri-list', 0, 0)],
             actions=Gdk.DragAction.COPY)
-        
+
     def acceptDragsOnCurveViews(self):
         w = self.wtree.get_object("curves")
         w.drag_dest_set(flags=Gtk.DestDefaults.ALL,
                         targets=[Gtk.TargetEntry('text/uri-list', 0, 0)],
                         actions=Gdk.DragAction.COPY)
-        def recv(widget, context, x, y, selection,
-                       targetType, time):
+
+        def recv(widget, context, x, y, selection, targetType, time):
             subUri = URIRef(selection.data.strip())
             print "into curves", subUri
-            with self.graph.currentState(
-                    tripleFilter=(subUri, RDFS.label, None)) as current:
+            with self.graph.currentState(tripleFilter=(subUri, RDFS.label,
+                                                       None)) as current:
                 subName = current.label(subUri)
 
             if '?' in subUri:
                 subName = self.handleSubtermDrop(subUri)
             else:
                 try:
-                    self.makeSubterm(subName, withCurve=True,
+                    self.makeSubterm(subName,
+                                     withCurve=True,
                                      sub=subUri,
                                      expr="%s(t)" % subName)
                 except SubtermExists:
@@ -177,23 +189,26 @@
                     # correct-- user mihgt need to fix things
                     pass
             curveView = self.curvesetView.row(subName).curveView
-            t = self.lastSeenInputTime # curveView.current_time() # new curve hasn't heard the time yet. this has gotten too messy- everyone just needs to be able to reach the time source
+            t = self.lastSeenInputTime  # curveView.current_time() # new curve hasn't heard the time yet. this has gotten too messy- everyone just needs to be able to reach the time source
             print "time", t
-            curveView.add_points([(t - .5, 0),
-                                  (t, 1)])
+            curveView.add_points([(t - .5, 0), (t, 1)])
+
         w.connect("drag-data-received", recv)
-        
+
     def onDragDataInNewSubZone(self, widget, context, x, y, selection,
-                       targetType, time):
+                               targetType, time):
         data = URIRef(selection.data.strip())
         if '?' in data:
             self.handleSubtermDrop(data)
             return
-        with self.graph.currentState(tripleFilter=(data, None, None)) as current:
+        with self.graph.currentState(tripleFilter=(data, None,
+                                                   None)) as current:
             subName = current.label(data)
-        self.makeSubterm(newname=subName, withCurve=True, sub=data,
+        self.makeSubterm(newname=subName,
+                         withCurve=True,
+                         sub=data,
                          expr="%s(t)" % subName)
-        
+
     def handleSubtermDrop(self, data):
         params = parse_qsl(data.split('?')[1])
         flattened = dict(params)
@@ -216,21 +231,21 @@
 
     def onRedrawCurves(self, *args):
         dispatcher.send("all curves rebuild")
-        
+
     def onSubtermsMap(self, *args):
         # if this was called too soon, like in __init__, the gtktable
         # would get its children but it wouldn't lay anything out that
         # I can see, and I'm not sure why. Waiting for map event is
         # just a wild guess.
         self.graph.addHandler(self.set_subterms_from_graph)
-        
+
     def onNewSubterm(self, *args):
         self.makeSubterm(Literal(""), withCurve=False)
         return
 
         # pretty sure i don't want this back, but not completely sure
         # what the UX should be to get the new curve.
-        
+
         dialog = self.wtree.get_object("newSubterm")
         # the plan is to autocomplete this on existing subterm names
         # (but let you make one up, too)
@@ -245,9 +260,9 @@
 
     def currentSong(self):
 
-        with self.graph.currentState(
-                tripleFilter=(self.session, L9['currentSong'], None)
-        ) as current:
+        with self.graph.currentState(tripleFilter=(self.session,
+                                                   L9['currentSong'],
+                                                   None)) as current:
             return current.value(self.session, L9['currentSong'])
 
     def songSubtermsContext(self):
@@ -277,13 +292,13 @@
             (uri, RDF.type, L9.Subterm, ctx),
             (uri, RDFS.label, Literal(newname), ctx),
             (self.currentSong(), L9['subterm'], uri, ctx),
-            ]
+        ]
         if sub is not None:
             quads.append((uri, L9['sub'], sub, ctx))
         if expr is not None:
             quads.append((uri, L9['expression'], Literal(expr), ctx))
         self.graph.patch(Patch(addQuads=quads))
-            
+
         return uri
 
     def all_subterm_labels(self):
@@ -301,7 +316,7 @@
                 if sub is not None:
                     labels.append(current.label(sub))
         return labels
-        
+
     def set_subterms_from_graph(self):
         """rebuild all the gtktable 'subterms' widgets and the
         self.currentSubterms list"""
@@ -311,7 +326,7 @@
         for st in set(self.graph.objects(song, L9['subterm'])):
             log.debug("song %s has subterm %s", song, st)
             term = Subterm(self.graph, st, self.songSubtermsContext(),
-                               self.curveset)
+                           self.curveset)
             newList.append(term)
         self.currentSubterms[:] = newList
 
@@ -335,13 +350,12 @@
           * { font-size: 92%; }
           .button:link { font-size: 7px }
         ''')
-        
+
         screen = Gdk.Display.get_default_screen(Gdk.Display.get_default())
         for p in providers:
             Gtk.StyleContext.add_provider_for_screen(
-                screen, p,
-                Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
-        
+                screen, p, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
+
     def onSubtermChildAdded(self, subtermsTable, *args):
         # this would probably work, but isn't getting called
         log.info("onSubtermChildAdded")
@@ -369,7 +383,7 @@
         ns.update(globals())
         ns.update(self.__dict__)
         togglePyConsole(self, item, ns)
-        
+
     def onSeeCurrentTime(self, item):
         dispatcher.send("see time")
 
@@ -395,15 +409,14 @@
     def makeStatusLines(self, master):
         """various labels that listen for dispatcher signals"""
         for row, (signame, textfilter) in enumerate([
-            ('input time', lambda t: "%.2fs"%t),
-            ('output levels',
-             lambda levels: textwrap.fill("; ".join(["%s:%.2f"%(n,v)
-                                                     for n,v in
-                                                     levels.items()[:2]
-                                                     if v>0]),70)),
-            ('update period', lambda t: "%.1fms"%(t*1000)),
+            ('input time', lambda t: "%.2fs" % t),
+            ('output levels', lambda levels: textwrap.fill(
+                "; ".join([
+                    "%s:%.2f" % (n, v) for n, v in levels.items()[:2] if v > 0
+                ]), 70)),
+            ('update period', lambda t: "%.1fms" % (t * 1000)),
             ('update status', lambda x: str(x)),
-            ]):
+        ]):
             key = Gtk.Label("%s:" % signame)
             value = Gtk.Label("")
             master.resize(row + 1, 2)
@@ -412,28 +425,31 @@
             key.set_alignment(1, 0)
             value.set_alignment(0, 0)
 
-            dispatcher.connect(lambda val, value=value, tf=textfilter:
-                               value.set_text(tf(val)),
-                               signame, weak=False)
+            dispatcher.connect(lambda val, value=value, tf=textfilter: value.
+                               set_text(tf(val)),
+                               signame,
+                               weak=False)
         dispatcher.connect(lambda val: setattr(self, 'lastSeenInputTime', val),
-                           'input time', weak=False)
+                           'input time',
+                           weak=False)
         master.show_all()
 
     def refreshCurveView(self):
         wtree = self.wtree
-        mtimes = [os.path.getmtime(f) for f in [
-            'light9/curvecalc/curveview.py',
-            'light9/curvecalc/zoomcontrol.py',
-            ]]
+        mtimes = [
+            os.path.getmtime(f) for f in [
+                'light9/curvecalc/curveview.py',
+                'light9/curvecalc/zoomcontrol.py',
+            ]
+        ]
 
         if (not hasattr(self, 'curvesetView') or
-            self.curvesetView._mtimes != mtimes):
+                self.curvesetView._mtimes != mtimes):
             print "reload curveview.py"
             curvesVBox = wtree.get_object("curves")
             zoomControlBox = wtree.get_object("zoomControlBox")
             [curvesVBox.remove(c) for c in curvesVBox.get_children()]
-            [zoomControlBox.remove(c) for c in
-             zoomControlBox.get_children()]
+            [zoomControlBox.remove(c) for c in zoomControlBox.get_children()]
             try:
                 linecache.clearcache()
                 reload(curveview)
@@ -443,8 +459,8 @@
                     self.curvesetView.live = False
 
                 # mem problem somewhere; need to hold a ref to this
-                self.curvesetView = curveview.Curvesetview(self.graph, 
-                    curvesVBox, zoomControlBox, self.curveset)
+                self.curvesetView = curveview.Curvesetview(
+                    self.graph, curvesVBox, zoomControlBox, self.curveset)
                 self.curvesetView._mtimes = mtimes
 
                 # this is scheduled after some tk shuffling, to
@@ -463,6 +479,7 @@
     """
     looks up the time in seconds for the session's current song
     """
+
     def __init__(self, graph, session):
         self.graph, self.session = graph, session
         graph.addHandler(self.update)
@@ -480,6 +497,7 @@
     def get(self):
         return self.maxtime
 
+
 def launch(args, graph, session, opts, startTime, music):
 
     try:
@@ -492,7 +510,7 @@
         pass
 
     curveset = Curveset(graph=graph, session=session)
-        
+
     log.debug("startup: output %s", time.time() - startTime)
 
     mt = MaxTime(graph, session)
@@ -501,9 +519,8 @@
     start = Main(graph, opts, session, curveset, music)
     out = Output(graph, session, music, curveset, start.currentSubterms)
 
+    dispatcher.send("show all")
 
-    dispatcher.send("show all")
-        
     if opts.startup_only:
         log.debug("quitting now because of --startup-only")
         return
@@ -515,22 +532,24 @@
             requestHandler.set_status(404)
             requestHandler.write("not hovering over any time")
             return
-        with graph.currentState(
-                tripleFilter=(session, L9['currentSong'], None)) as g:
+        with graph.currentState(tripleFilter=(session, L9['currentSong'],
+                                              None)) as g:
             song = g.value(session, L9['currentSong'])
-            json.dump({"song": song, "hoverTime" : times[0]}, requestHandler)
-        
+            json.dump({"song": song, "hoverTime": times[0]}, requestHandler)
+
     serveCurveEdit(networking.curveCalc.port, hoverTimeResponse, start.curveset)
 
+
 def main():
     startTime = time.time()
     parser = optparse.OptionParser()
     parser.set_usage("%prog [opts] [songURI]")
-    parser.add_option("--debug", action="store_true",
-                      help="log at DEBUG")
-    parser.add_option("--reload", action="store_true",
+    parser.add_option("--debug", action="store_true", help="log at DEBUG")
+    parser.add_option("--reload",
+                      action="store_true",
                       help="live reload of themes and code")
-    parser.add_option("--startup-only", action='store_true',
+    parser.add_option("--startup-only",
+                      action='store_true',
                       help="quit after loading everything (for timing tests)")
     parser.add_option("--profile", help='"hotshot" or "stat"')
     clientsession.add_option(parser)
@@ -540,16 +559,15 @@
 
     log.debug("startup: music %s", time.time() - startTime)
 
-
     session = clientsession.getUri('curvecalc', opts)
 
     music = Music()
     graph = SyncedGraph(networking.rdfdb.url, "curvecalc")
 
-    graph.initiallySynced.addCallback(
-        lambda _: launch(args, graph, session, opts, startTime, music))
+    graph.initiallySynced.addCallback(lambda _: launch(args, graph, session,
+                                                       opts, startTime, music))
     from light9 import prof
     prof.run(reactor.run, profile=opts.profile)
 
+
 main()
-
--- a/bin/dmx_color_test.py	Tue May 21 23:55:35 2019 +0000
+++ b/bin/dmx_color_test.py	Tue May 21 23:56:12 2019 +0000
@@ -7,6 +7,7 @@
 log.setLevel(logging.INFO)
 firstDmxChannel = 10
 
+
 def step():
     hue = (time.time() * .2) % 1.0
     r, g, b = colorsys.hsv_to_rgb(hue, 1, 1)
--- a/bin/dmxserver	Tue May 21 23:55:35 2019 +0000
+++ b/bin/dmxserver	Tue May 21 23:56:12 2019 +0000
@@ -27,10 +27,10 @@
 from __future__ import division
 from twisted.internet import reactor
 from twisted.web import xmlrpc, server
-import sys,time,os
+import sys, time, os
 from optparse import OptionParser
 import run_local
-import txosc.dispatch, txosc.async
+import txosc.dispatch, txosc. async
 from light9.io import ParportDMX, UsbDMX
 
 from light9.updatefreq import Updatefreq
@@ -39,21 +39,26 @@
 from txzmq import ZmqEndpoint, ZmqFactory, ZmqPullConnection, ZmqRequestTimeoutError
 import json
 
+
 def startZmq(port, outputlevels):
     zf = ZmqFactory()
     e = ZmqEndpoint('bind', 'tcp://*:%s' % port)
     s = ZmqPullConnection(zf, e)
+
     def onPull(message):
         msg = json.loads(message[0])
         outputlevels(msg['clientid'], msg['levellist'])
+
     s.onPull = onPull
 
+
 class ReceiverApplication(object):
     """
     receive UDP OSC messages. address is /dmx/1 for dmx channel 1,
     arguments are 0-1 floats for that channel and any number of
     following channels.
     """
+
     def __init__(self, port, lightServer):
         self.port = port
         self.lightServer = lightServer
@@ -61,7 +66,7 @@
         self.receiver.addCallback("/dmx/*", self.pixel_handler)
         self._server_port = reactor.listenUDP(
             self.port,
-            txosc.async.DatagramServerProtocol(self.receiver),
+            txosc. async .DatagramServerProtocol(self.receiver),
             interface='0.0.0.0')
         print "Listening OSC on udp port %s" % (self.port)
 
@@ -70,127 +75,127 @@
         startChannel = int(message.address.split('/')[2])
         levels = [a.value for a in message.arguments]
         allLevels = [0] * (startChannel - 1) + levels
-        self.lightServer.xmlrpc_outputlevels("osc@%s" % startChannel,
-                                             allLevels)
+        self.lightServer.xmlrpc_outputlevels("osc@%s" % startChannel, allLevels)
+
 
 class XMLRPCServe(xmlrpc.XMLRPC):
-    def __init__(self,options):
+
+    def __init__(self, options):
 
         xmlrpc.XMLRPC.__init__(self)
-        
-        self.clientlevels={} # clientID : list of levels
-        self.lastseen={} # clientID : time last seen
-        self.clientfreq={} # clientID : updatefreq
-        
-        self.combinedlevels=[] # list of levels, after max'ing the clients
-        self.clientschanged=1 # have clients sent anything since the last send?
-        self.options=options
-        self.lastupdate=0 # time of last dmx send
-        self.laststatsprint=0  # time
+
+        self.clientlevels = {}  # clientID : list of levels
+        self.lastseen = {}  # clientID : time last seen
+        self.clientfreq = {}  # clientID : updatefreq
+
+        self.combinedlevels = []  # list of levels, after max'ing the clients
+        self.clientschanged = 1  # have clients sent anything since the last send?
+        self.options = options
+        self.lastupdate = 0  # time of last dmx send
+        self.laststatsprint = 0  # time
 
         # desired seconds between sendlevels() calls
-        self.calldelay=1/options.updates_per_sec 
+        self.calldelay = 1 / options.updates_per_sec
 
         print "starting parport connection"
         self.parportdmx = UsbDMX(dimmers=90, port=options.dmx_device)
-        if os.environ.get('DMXDUMMY',0):
+        if os.environ.get('DMXDUMMY', 0):
             self.parportdmx.godummy()
         else:
             self.parportdmx.golive()
-            
 
-        self.updatefreq=Updatefreq() # freq of actual dmx sends
-        self.num_unshown_updates=None
-        self.lastshownlevels=None
+        self.updatefreq = Updatefreq()  # freq of actual dmx sends
+        self.num_unshown_updates = None
+        self.lastshownlevels = None
         # start the loop
         self.sendlevels()
 
         # the other loop
         self.purgeclients()
-        
+
     def purgeclients(self):
-        
         """forget about any clients who haven't sent levels in a while.
         this runs in a loop"""
 
-        purge_age=10 # seconds
-        
-        reactor.callLater(1,self.purgeclients)
+        purge_age = 10  # seconds
 
-        now=time.time()
+        reactor.callLater(1, self.purgeclients)
+
+        now = time.time()
         cids = self.lastseen.keys()
         for cid in cids:
-            lastseen=self.lastseen[cid]
+            lastseen = self.lastseen[cid]
             if lastseen < now - purge_age:
-                print ("forgetting client %s (no activity for %s sec)" %
-                       (cid,purge_age))
+                print("forgetting client %s (no activity for %s sec)" %
+                      (cid, purge_age))
                 try:
                     del self.clientlevels[cid]
                 except KeyError:
                     pass
                 del self.clientfreq[cid]
                 del self.lastseen[cid]
-        
+
     def sendlevels(self):
-        
         """sends to dmx if levels have changed, or if we havent sent
         in a while"""
 
-        reactor.callLater(self.calldelay,self.sendlevels)
+        reactor.callLater(self.calldelay, self.sendlevels)
 
         if self.clientschanged:
             # recalc levels
 
             self.calclevels()
-         
-            if (self.num_unshown_updates is None or # first time
-                self.options.fast_updates or # show always
-                (self.combinedlevels!=self.lastshownlevels and # changed
-                 self.num_unshown_updates>5)): # not too frequent
-                self.num_unshown_updates=0
+
+            if (self.num_unshown_updates is None or  # first time
+                    self.options.fast_updates or  # show always
+                (
+                    self.combinedlevels != self.lastshownlevels and  # changed
+                    self.num_unshown_updates > 5)):  # not too frequent
+                self.num_unshown_updates = 0
                 self.printlevels()
-                self.lastshownlevels=self.combinedlevels[:]
+                self.lastshownlevels = self.combinedlevels[:]
             else:
-                self.num_unshown_updates+=1
+                self.num_unshown_updates += 1
 
-        if time.time()>self.laststatsprint+2:
-            self.laststatsprint=time.time()
+        if time.time() > self.laststatsprint + 2:
+            self.laststatsprint = time.time()
             self.printstats()
 
         # used to be a fixed 1 in here, for the max delay between
         # calls, instead of calldelay
-        if self.clientschanged or time.time() > self.lastupdate + self.calldelay:
-            self.lastupdate=time.time()
+        if self.clientschanged or time.time(
+        ) > self.lastupdate + self.calldelay:
+            self.lastupdate = time.time()
             self.sendlevels_dmx()
 
-        self.clientschanged=0 # clear the flag
-        
+        self.clientschanged = 0  # clear the flag
+
     def calclevels(self):
         """combine all the known client levels into self.combinedlevels"""
-        self.combinedlevels=[]
-        for chan in range(0,self.parportdmx.dimmers):
-            x=0
+        self.combinedlevels = []
+        for chan in range(0, self.parportdmx.dimmers):
+            x = 0
             for clientlist in self.clientlevels.values():
-                if len(clientlist)>chan:
+                if len(clientlist) > chan:
                     # clamp client levels to 0..1
-                    cl=max(0,min(1,clientlist[chan]))
-                    x=max(x,cl)
+                    cl = max(0, min(1, clientlist[chan]))
+                    x = max(x, cl)
             self.combinedlevels.append(x)
 
     def printlevels(self):
         """write all the levels to stdout"""
-        print "Levels:","".join(["% 2d "%(x*100) for
-                                 x in self.combinedlevels])
-    
+        print "Levels:", "".join(
+            ["% 2d " % (x * 100) for x in self.combinedlevels])
+
     def printstats(self):
         """print the clock, freq, etc, with a \r at the end"""
 
-        sys.stdout.write("dmxserver up at %s, [polls %s] "%
-                         (time.strftime("%H:%M:%S"),
-                          str(self.updatefreq),
-                          ))
-        for cid,freq in self.clientfreq.items():
-            sys.stdout.write("[%s %s] " % (cid,str(freq)))
+        sys.stdout.write("dmxserver up at %s, [polls %s] " % (
+            time.strftime("%H:%M:%S"),
+            str(self.updatefreq),
+        ))
+        for cid, freq in self.clientfreq.items():
+            sys.stdout.write("[%s %s] " % (cid, str(freq)))
         sys.stdout.write("\r")
         sys.stdout.flush()
 
@@ -198,18 +203,18 @@
         """output self.combinedlevels to dmx, and keep the updates/sec stats"""
         # they'll get divided by 100
         if self.parportdmx:
-            self.parportdmx.sendlevels([l*100 for l in self.combinedlevels])
+            self.parportdmx.sendlevels([l * 100 for l in self.combinedlevels])
         self.updatefreq.update()
-    
-    def xmlrpc_echo(self,x):
+
+    def xmlrpc_echo(self, x):
         return x
-    
-    def xmlrpc_outputlevels(self,cid,levellist):
+
+    def xmlrpc_outputlevels(self, cid, levellist):
         """send a unique id for your client (name+pid maybe), then
         the variable-length dmx levellist (scaled 0..1)"""
-        if levellist!=self.clientlevels.get(cid,None):
-            self.clientlevels[cid]=levellist
-            self.clientschanged=1
+        if levellist != self.clientlevels.get(cid, None):
+            self.clientlevels[cid] = levellist
+            self.clientschanged = 1
         self.trackClientFreq(cid)
         return "ok"
 
@@ -227,43 +232,50 @@
             i -= 1
         if i < 0:
             return []
-        trunc = trunc[:i+1]
+        trunc = trunc[:i + 1]
         return trunc
-    
+
     def trackClientFreq(self, cid):
         if cid not in self.lastseen:
             print "hello new client %s" % cid
-            self.clientfreq[cid]=Updatefreq()
-        self.lastseen[cid]=time.time()
+            self.clientfreq[cid] = Updatefreq()
+        self.lastseen[cid] = time.time()
         self.clientfreq[cid].update()
-        
+
 
-parser=OptionParser()
-parser.add_option("-f","--fast-updates",action='store_true',
+parser = OptionParser()
+parser.add_option("-f",
+                  "--fast-updates",
+                  action='store_true',
                   help=('display all dmx output to stdout instead '
                         'of the usual reduced output'))
-parser.add_option("-r","--updates-per-sec",type='float',default=20,
+parser.add_option("-r",
+                  "--updates-per-sec",
+                  type='float',
+                  default=20,
                   help=('dmx output frequency'))
-parser.add_option("-d","--dmx-device", default='/dev/dmx0',
+parser.add_option("-d",
+                  "--dmx-device",
+                  default='/dev/dmx0',
                   help='dmx device name')
-parser.add_option("-n", "--dummy", action="store_true",
+parser.add_option("-n",
+                  "--dummy",
+                  action="store_true",
                   help="dummy mode, same as DMXDUMMY=1 env variable")
-(options,songfiles)=parser.parse_args()
+(options, songfiles) = parser.parse_args()
 
 print options
 
 if options.dummy:
     os.environ['DMXDUMMY'] = "1"
 
-
 port = networking.dmxServer.port
 print "starting xmlrpc server on port %s" % port
 xmlrpcServe = XMLRPCServe(options)
-reactor.listenTCP(port,server.Site(xmlrpcServe))
+reactor.listenTCP(port, server.Site(xmlrpcServe))
 
 startZmq(networking.dmxServerZmq.port, xmlrpcServe.xmlrpc_outputlevels)
-                  
+
 oscApp = ReceiverApplication(9051, xmlrpcServe)
 
 reactor.run()
-
--- a/bin/effecteval	Tue May 21 23:55:35 2019 +0000
+++ b/bin/effecteval	Tue May 21 23:56:12 2019 +0000
@@ -8,8 +8,8 @@
 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
+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
@@ -22,19 +22,24 @@
 
 from lib.cycloneerr import PrettyErrorHandler
 
+
 class EffectEdit(PrettyErrorHandler, cyclone.web.RequestHandler):
+
     def get(self):
-        self.set_header('Content-Type', 'text/html')        
+        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),
+        self.settings.graph.patch(
+            Patch(delQuads=[
+                (song, L9['effect'], uri, ctx),
             ]))
-        
+
+
 @inlineCallbacks
 def currentSong():
     s = (yield getMusicStatus())['song']
@@ -42,13 +47,17 @@
         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-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')
-    
+        self.set_header('Access-Control-Allow-Headers',
+                        'Content-Type, Authorization, X-Requested-With')
+
     def options(self):
         self.wideOpenCors()
         self.write('')
@@ -60,27 +69,34 @@
 
         try:
             song = URIRef(self.get_argument('uri'))
-        except Exception: # which?
+        except Exception:  # which?
             song = yield currentSong()
 
         event = self.get_argument('event', default='default')
-        
+
         note = self.get_argument('note', default=None)
         if note is not None:
             note = URIRef(note)
 
         log.info("adding to %s", song)
-        note, p = yield songNotePatch(self.settings.graph, dropped, song, event, ctx=song, note=note)
-        
+        note, p = yield songNotePatch(self.settings.graph,
+                                      dropped,
+                                      song,
+                                      event,
+                                      ctx=song,
+                                      note=note)
+
         self.settings.graph.patch(p)
         self.settings.graph.suggestPrefixes(song, {'song': URIRef(song + '/')})
         self.write(json.dumps({'note': note}))
-        
+
+
 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'])
@@ -88,14 +104,18 @@
         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']))]
+            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'))
@@ -113,7 +133,7 @@
         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")
 
@@ -121,12 +141,12 @@
         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])
+    patch = graph.getObjectPatch(context=c,
+                                 subject=s,
+                                 predicate=p,
+                                 newObject=newObjs[0])
 
     moreAdds = []
     for line in newObjs[1:]:
@@ -135,8 +155,9 @@
                       addQuads=patch.addQuads + moreAdds)
     graph.patch(fullPatch)
 
-        
+
 class Code(PrettyErrorHandler, cyclone.web.RequestHandler):
+
     def put(self):
         effect = URIRef(self.get_argument('uri'))
         codeLines = []
@@ -150,16 +171,18 @@
         if not codeLines:
             log.info("no codelines received on PUT /code")
             return
-        with self.settings.graph.currentState(
-                tripleFilter=(None, L9['effect'], effect)) as g:
+        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
@@ -176,6 +199,7 @@
 # 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)
@@ -183,34 +207,44 @@
         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.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'),
-                                       )
+        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'/()', 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'/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),
@@ -224,25 +258,33 @@
                                                   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]))
+        self.write(
+            subprocess.check_output(
+                ['/usr/bin/coffee', '--compile', '--print', self.src]))
 
-        
+
 if __name__ == "__main__":
     parser = optparse.OptionParser()
-    parser.add_option('--show',
+    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",
+        default=showconfig.showUri())
+    parser.add_option("-v",
+                      "--verbose",
+                      action="store_true",
                       help="logging.DEBUG")
-    parser.add_option("--twistedlog", action="store_true",
+    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()
@@ -250,7 +292,7 @@
 
     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
--- a/bin/effectsequencer	Tue May 21 23:55:35 2019 +0000
+++ b/bin/effectsequencer	Tue May 21 23:56:12 2019 +0000
@@ -17,7 +17,9 @@
 
 from light9 import clientsession
 
+
 class App(object):
+
     def __init__(self, show, session):
         self.show = show
         self.session = session
@@ -25,23 +27,25 @@
         self.graph = SyncedGraph(networking.rdfdb.url, "effectSequencer")
         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'),
+        )
 
-        self.stats = scales.collection('/',
-                                       scales.PmfStat('sendLevels'),
-                                       scales.PmfStat('getMusic'),
-                                       scales.PmfStat('evals'),
-                                       scales.PmfStat('sendOutput'),
-                                       scales.IntStat('errors'),
-                                       )
     def launch(self, *args):
         self.seq = Sequencer(
-            self.graph,
-            lambda settings: sendToCollector('effectSequencer', self.session,
-                                             settings))
+            self.graph, lambda settings: sendToCollector(
+                'effectSequencer', self.session, settings))
 
         self.cycloneApp = cyclone.web.Application(handlers=[
-            (r'/()', cyclone.web.StaticFileHandler,
-             {"path" : "light9/effect/", "default_filename" : "sequencer.html"}),
+            (r'/()', cyclone.web.StaticFileHandler, {
+                "path": "light9/effect/",
+                "default_filename": "sequencer.html"
+            }),
             (r'/updates', Updates),
             (r'/stats', StatsForCyclone),
         ],
@@ -55,12 +59,16 @@
 
 if __name__ == "__main__":
     parser = optparse.OptionParser()
-    parser.add_option('--show',
+    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",
+        default=showconfig.showUri())
+    parser.add_option("-v",
+                      "--verbose",
+                      action="store_true",
                       help="logging.DEBUG")
-    parser.add_option("--twistedlog", action="store_true",
+    parser.add_option("--twistedlog",
+                      action="store_true",
                       help="twisted logging")
     clientsession.add_option(parser)
     (options, args) = parser.parse_args()
@@ -68,7 +76,7 @@
 
     if not options.show:
         raise ValueError("missing --show http://...")
-        
+
     session = clientsession.getUri('effectSequencer', options)
 
     app = App(URIRef(options.show), session)
--- a/bin/homepageConfig	Tue May 21 23:55:35 2019 +0000
+++ b/bin/homepageConfig	Tue May 21 23:56:12 2019 +0000
@@ -17,6 +17,7 @@
     raise ValueError('no %r :webServer' % netHome)
 print "listen %s;" % splitport(urlparse(webServer).netloc)[1]
 
+
 def location(path, server):
     print """
     location /%(path)s/ {
@@ -32,6 +33,7 @@
       rewrite /[^/]+/(.*) /$1 break;
     }""" % vars()
 
+
 for role, server in sorted(graph.predicate_objects(netHome)):
     if not server.startswith('http') or role == L9['webServer']:
         continue
@@ -41,11 +43,11 @@
     server = server.rstrip('/')
     location(path, server)
 
-
-
 showPath = showconfig.showUri().split('/', 3)[-1]
 print """
     location /%(path)s {
       root %(root)s;
-    }""" % {'path': showPath,
-            'root': showconfig.root()[:-len(showPath)]}
+    }""" % {
+    'path': showPath,
+    'root': showconfig.root()[:-len(showPath)]
+}
--- a/bin/inputdemo	Tue May 21 23:55:35 2019 +0000
+++ b/bin/inputdemo	Tue May 21 23:56:12 2019 +0000
@@ -1,6 +1,6 @@
 #!bin/python
 import sys
-sys.path.append('/usr/lib/python2.7/dist-packages') # For gtk
+sys.path.append('/usr/lib/python2.7/dist-packages')  # For gtk
 from twisted.internet import gtk3reactor
 gtk3reactor.install()
 from twisted.internet import reactor
@@ -14,12 +14,13 @@
 import cyclone.httpclient
 from light9.curvecalc.client import sendLiveInputPoint
 
+
 class App(object):
+
     def __init__(self):
         parser = optparse.OptionParser()
         parser.set_usage("%prog [opts] [curve uri]")
-        parser.add_option("--debug", action="store_true",
-                          help="log at DEBUG")
+        parser.add_option("--debug", action="store_true", help="log at DEBUG")
         clientsession.add_option(parser)
         opts, args = parser.parse_args()
 
@@ -30,16 +31,20 @@
 
         self.graph.initiallySynced.addCallback(lambda _: self.launch())
 
-        self.curve = args[0] if args else URIRef('http://light9.bigasterisk.com/show/dance2014/song1/curve/c-1401259747.675542')
+        self.curve = args[0] if args else URIRef(
+            'http://light9.bigasterisk.com/show/dance2014/song1/curve/c-1401259747.675542'
+        )
         print "sending points on curve %s" % self.curve
-        
+
         reactor.run()
 
     def launch(self):
         win = Gtk.Window()
 
         slider = Gtk.Scale.new_with_range(orientation=Gtk.Orientation.VERTICAL,
-                                          min=0, max=1, step=.001)
+                                          min=0,
+                                          max=1,
+                                          step=.001)
         slider.props.inverted = True
         slider.connect('value-changed', self.onChanged)
 
@@ -52,8 +57,10 @@
     def onChanged(self, scale):
         t1 = time.time()
         d = sendLiveInputPoint(self.curve, scale.get_value())
+
         @d.addCallback
         def done(result):
             print "posted in %.1f ms" % (1000 * (time.time() - t1))
 
+
 App()
--- a/bin/inputquneo	Tue May 21 23:55:35 2019 +0000
+++ b/bin/inputquneo	Tue May 21 23:56:12 2019 +0000
@@ -14,18 +14,20 @@
 from light9 import networking
 
 import sys
-sys.path.append('/usr/lib/python2.7/dist-packages') # For pygame
+sys.path.append('/usr/lib/python2.7/dist-packages')  # For pygame
 import pygame.midi
 
 curves = {
     23: URIRef('http://light9.bigasterisk.com/show/dance2014/song1/curve/c-2'),
     24: URIRef('http://light9.bigasterisk.com/show/dance2014/song1/curve/c-3'),
     25: URIRef('http://light9.bigasterisk.com/show/dance2014/song1/curve/c-4'),
-    6:URIRef('http://light9.bigasterisk.com/show/dance2014/song1/curve/c-5'),
+    6: URIRef('http://light9.bigasterisk.com/show/dance2014/song1/curve/c-5'),
     18: URIRef('http://light9.bigasterisk.com/show/dance2014/song1/curve/c-6'),
 }
 
+
 class WatchMidi(object):
+
     def __init__(self, graph):
         self.graph = graph
         pygame.midi.init()
@@ -36,7 +38,7 @@
 
         self.noteIsOn = {}
 
-        self.effectMap = {} # note: effect class uri
+        self.effectMap = {}  # note: effect class uri
         self.graph.addHandler(self.setupNotes)
 
     def setupNotes(self):
@@ -45,14 +47,15 @@
             if qn:
                 self.effectMap[int(qn)] = e
         log.info("setup with %s effects", len(self.effectMap))
-        
+
     def findQuneo(self):
         for dev in range(pygame.midi.get_count()):
-            interf, name, isInput, isOutput, opened = pygame.midi.get_device_info(dev)
+            interf, name, isInput, isOutput, opened = pygame.midi.get_device_info(
+                dev)
             if 'QUNEO' in name and isInput:
                 return dev
         raise ValueError("didn't find quneo input device")
-        
+
     def step(self):
         if not self.inp.poll():
             return
@@ -62,7 +65,6 @@
             if status in [NOTEON, NOTEOFF]:
                 print status, d1, d2
 
-
             if status == NOTEON:
                 if not self.noteIsOn.get(d1):
                     self.noteIsOn[d1] = True
@@ -71,7 +73,10 @@
                         cyclone.httpclient.fetch(
                             url=networking.effectEval.path('songEffects'),
                             method='POST',
-                            headers={'Content-Type': ['application/x-www-form-urlencoded']},
+                            headers={
+                                'Content-Type':
+                                ['application/x-www-form-urlencoded']
+                            },
                             postdata=urllib.urlencode([('drop', e)]),
                         )
                     except KeyError:
@@ -80,10 +85,9 @@
             if status == NOTEOFF:
                 self.noteIsOn[d1] = False
 
-
             if 0:
                 # curve editing mode, not done yet
-                for group in [(23,24,25), (6, 18)]:
+                for group in [(23, 24, 25), (6, 18)]:
                     if d1 in group:
                         if not self.noteIsOn.get(group):
                             print "start zero"
@@ -91,18 +95,20 @@
                             for d in group:
                                 sendLiveInputPoint(curves[d], 0)
                             self.noteIsOn[group] = True
-                        else: # miss first update
+                        else:  # miss first update
                             sendLiveInputPoint(curves[d1], d2 / 127)
 
-                    if status == 128: #noteoff
+                    if status == 128:  #noteoff
                         for d in group:
                             sendLiveInputPoint(curves[d], 0)
                         self.noteIsOn[group] = False
 
+
 def main():
     log.setLevel(logging.DEBUG)
     graph = SyncedGraph(networking.rdfdb.url, "inputQuneo")
     wm = WatchMidi(graph)
     reactor.run()
 
+
 main()
--- a/bin/kcclient	Tue May 21 23:55:35 2019 +0000
+++ b/bin/kcclient	Tue May 21 23:56:12 2019 +0000
@@ -1,5 +1,4 @@
 #!/usr/bin/env python
-
 """send KeyboardComposer a fade request, for use from the shell"""
 
 import sys
@@ -10,10 +9,8 @@
 subname = sys.argv[1]
 level = sys.argv[2]
 fadesecs = '0'
-if len(sys.argv)>3:
+if len(sys.argv) > 3:
     fadesecs = sys.argv[3]
 
 levelServer = Resource(networking.keyboardComposer.url)
 levelServer.post('fadesub', subname=subname, level=level, secs=fadesecs)
-
-
--- a/bin/keyboardcomposer	Tue May 21 23:55:35 2019 +0000
+++ b/bin/keyboardcomposer	Tue May 21 23:56:12 2019 +0000
@@ -27,43 +27,58 @@
 
 from bcf2000 import BCF2000
 
-nudge_keys = {
-    'up'   : list('qwertyui'),
-    'down' : list('asdfghjk')
-}
+nudge_keys = {'up': list('qwertyui'), 'down': list('asdfghjk')}
+
 
 class DummySliders:
+
     def valueOut(self, name, value):
         pass
+
     def close(self):
         pass
+
     def reopen(self):
         pass
 
+
 class SubScale(tk.Scale, Fadable):
+
     def __init__(self, master, *args, **kw):
         self.scale_var = kw.get('variable') or tk.DoubleVar()
-        kw.update({'variable' : self.scale_var,
-                   'from' : 1, 'to' : 0, 'showvalue' : 0,
-                   'sliderlength' : 15, 'res' : 0.01,
-                   'width' : 40, 'troughcolor' : 'black', 'bg' : 'grey40',
-                   'highlightthickness' : 1, 'bd' : 1,
-                   'highlightcolor' : 'red', 'highlightbackground' : 'black',
-                   'activebackground' : 'red'})
+        kw.update({
+            'variable': self.scale_var,
+            'from': 1,
+            'to': 0,
+            'showvalue': 0,
+            'sliderlength': 15,
+            'res': 0.01,
+            'width': 40,
+            'troughcolor': 'black',
+            'bg': 'grey40',
+            'highlightthickness': 1,
+            'bd': 1,
+            'highlightcolor': 'red',
+            'highlightbackground': 'black',
+            'activebackground': 'red'
+        })
         tk.Scale.__init__(self, master, *args, **kw)
         Fadable.__init__(self, var=self.scale_var, wheel_step=0.05)
         self.draw_indicator_colors()
+
     def draw_indicator_colors(self):
         if self.scale_var.get() == 0:
             self['troughcolor'] = 'black'
         else:
             self['troughcolor'] = 'blue'
 
+
 class SubmasterBox(tk.Frame):
     """
     this object owns the level of the submaster (the rdf graph is the
     real authority)
     """
+
     def __init__(self, master, graph, sub, session, col, row):
         self.graph = graph
         self.sub = sub
@@ -71,22 +86,31 @@
         self.col, self.row = col, row
         bg = self.graph.value(sub, L9.color, default='#000000')
         rgb = webcolors.hex_to_rgb(bg)
-        hsv = colorsys.rgb_to_hsv(*[x/255 for x in rgb])
-        darkBg = webcolors.rgb_to_hex(tuple([int(x * 255) for x in colorsys.hsv_to_rgb(
-            hsv[0], hsv[1], .2)]))
+        hsv = colorsys.rgb_to_hsv(*[x / 255 for x in rgb])
+        darkBg = webcolors.rgb_to_hex(
+            tuple([
+                int(x * 255) for x in colorsys.hsv_to_rgb(hsv[0], hsv[1], .2)
+            ]))
         tk.Frame.__init__(self, master, bd=1, relief='raised', bg=bg)
         self.name = self.graph.label(sub)
         self.slider_var = tk.DoubleVar()
         self.pauseTrace = False
         self.scale = SubScale(self, variable=self.slider_var, width=20)
 
-        self.namelabel = tk.Label(self, font="Arial 9", bg=darkBg,
-            fg='white', pady=0)
+        self.namelabel = tk.Label(self,
+                                  font="Arial 9",
+                                  bg=darkBg,
+                                  fg='white',
+                                  pady=0)
         self.graph.addHandler(self.updateName)
 
         self.namelabel.pack(side=tk.TOP)
-        levellabel = tk.Label(self, textvariable=self.slider_var, font="Arial 6",
-            bg='black', fg='white', pady=0)
+        levellabel = tk.Label(self,
+                              textvariable=self.slider_var,
+                              font="Arial 6",
+                              bg='black',
+                              fg='white',
+                              pady=0)
         levellabel.pack(side=tk.TOP)
         self.scale.pack(side=tk.BOTTOM, expand=1, fill=tk.BOTH)
 
@@ -111,7 +135,9 @@
         self.updateGraphWithLevel(self.sub, self.slider_var.get())
 
         # needs fixing: plan is to use dispatcher or a method call to tell a hardware-mapping object who changed, and then it can make io if that's a current hw slider
-        dispatcher.send("send_to_hw", sub=self.sub, hwCol=self.col + 1,
+        dispatcher.send("send_to_hw",
+                        sub=self.sub,
+                        hwCol=self.col + 1,
                         boxRow=self.row)
 
     def updateGraphWithLevel(self, uri, level):
@@ -125,8 +151,10 @@
                                 subject=self.session,
                                 predicate=L9['subSetting'],
                                 nodeClass=L9['SubSetting'],
-                                keyPred=L9['sub'], newKey=uri,
-                                valuePred=L9['level'], newValue=Literal(level))
+                                keyPred=L9['sub'],
+                                newKey=uri,
+                                valuePred=L9['level'],
+                                newValue=Literal(level))
 
     def updateLevelFromGraph(self):
         """read rdf level, write it to subbox.slider_var"""
@@ -135,30 +163,34 @@
 
         for setting in graph.objects(self.session, L9['subSetting']):
             if graph.value(setting, L9['sub']) == self.sub:
-                self.pauseTrace = True # don't bounce this update back to server
+                self.pauseTrace = True  # don't bounce this update back to server
                 try:
                     self.slider_var.set(graph.value(setting, L9['level']))
                 finally:
                     self.pauseTrace = False
 
     def updateName(self):
+
         def shortUri(u):
             return '.../' + u.split('/')[-1]
-        self.namelabel.config(text=self.graph.label(self.sub) or shortUri(self.sub))
+
+        self.namelabel.config(
+            text=self.graph.label(self.sub) or shortUri(self.sub))
+
 
 class KeyboardComposer(tk.Frame, SubClient):
-    def __init__(self, root, graph, session,
-                 hw_sliders=True):
+
+    def __init__(self, root, graph, session, hw_sliders=True):
         tk.Frame.__init__(self, root, bg='black')
         SubClient.__init__(self)
         self.graph = graph
         self.session = session
 
-        self.subbox = {} # sub uri : SubmasterBox
-        self.slider_table = {} # coords : SubmasterBox
-        self.rows = [] # this holds Tk Frames for each row
+        self.subbox = {}  # sub uri : SubmasterBox
+        self.slider_table = {}  # coords : SubmasterBox
+        self.rows = []  # this holds Tk Frames for each row
 
-        self.current_row = 0 # should come from session graph
+        self.current_row = 0  # should come from session graph
 
         self.use_hw_sliders = hw_sliders
         self.connect_to_hw(hw_sliders)
@@ -170,7 +202,7 @@
 
         self.codeWatcher = CodeWatcher(
             onChange=lambda: self.graph.addHandler(self.redraw_sliders))
-        
+
         self.send_levels_loop(delay=.05)
         self.graph.addHandler(self.rowFromGraph)
 
@@ -180,19 +212,28 @@
 
         self.sliders_status_var = tk.IntVar()
         self.sliders_status_var.set(self.use_hw_sliders)
-        self.sliders_checkbutton = tk.Checkbutton(self.buttonframe,
-            text="Sliders", variable=self.sliders_status_var,
+        self.sliders_checkbutton = tk.Checkbutton(
+            self.buttonframe,
+            text="Sliders",
+            variable=self.sliders_status_var,
             command=lambda: self.toggle_slider_connectedness(),
-            bg='black', fg='white')
+            bg='black',
+            fg='white')
         self.sliders_checkbutton.pack(side=tk.LEFT)
 
-        self.alltozerobutton = tk.Button(self.buttonframe, text="All to Zero",
-            command=self.alltozero, bg='black', fg='white')
+        self.alltozerobutton = tk.Button(self.buttonframe,
+                                         text="All to Zero",
+                                         command=self.alltozero,
+                                         bg='black',
+                                         fg='white')
         self.alltozerobutton.pack(side='left')
 
-        self.save_stage_button = tk.Button(self.buttonframe, text="Save",
+        self.save_stage_button = tk.Button(
+            self.buttonframe,
+            text="Save",
             command=lambda: self.save_current_stage(self.sub_name.get()),
-            bg='black', fg='white')
+            bg='black',
+            fg='white')
         self.save_stage_button.pack(side=tk.LEFT)
         self.sub_name = tk.Entry(self.buttonframe, bg='black', fg='white')
         self.sub_name.pack(side=tk.LEFT)
@@ -222,11 +263,9 @@
 
         withgroups = []
         for effect in self.graph.subjects(RDF.type, L9['Effect']):
-            withgroups.append((
-                self.graph.value(effect, L9['group']),
-                self.graph.value(effect, L9['order']),
-                self.graph.label(effect),
-                effect))
+            withgroups.append((self.graph.value(effect, L9['group']),
+                               self.graph.value(effect, L9['order']),
+                               self.graph.label(effect), effect))
         withgroups.sort()
 
         log.info("withgroups %s", withgroups)
@@ -240,13 +279,15 @@
                 rowcount += 1
                 col = 0
 
-            subbox = SubmasterBox(row, self.graph, effect, self.session, col, rowcount)
+            subbox = SubmasterBox(row, self.graph, effect, self.session, col,
+                                  rowcount)
             subbox.place(relx=col / 8, rely=0, relwidth=1 / 8, relheight=1)
             self.subbox[effect] = self.slider_table[(rowcount, col)] = subbox
 
             self.setup_key_nudgers(subbox.scale)
 
-            self.effectEval[effect] = light9.effect.effecteval.EffectEval(self.graph, effect, simpleOutputs)
+            self.effectEval[effect] = light9.effect.effecteval.EffectEval(
+                self.graph, effect, simpleOutputs)
 
             col = (col + 1) % 8
             last_group = group
@@ -277,15 +318,19 @@
         keyhintrow = tk.Frame(self)
 
         col = 0
-        for upkey, downkey in zip(nudge_keys['up'],
-                                  nudge_keys['down']):
+        for upkey, downkey in zip(nudge_keys['up'], nudge_keys['down']):
             # what a hack!
             downkey = downkey.replace('semicolon', ';')
             upkey, downkey = (upkey.upper(), downkey.upper())
 
             # another what a hack!
-            keylabel = tk.Label(keyhintrow, text='%s\n%s' % (upkey, downkey),
-                width=1, font=('Arial', 10), bg='red', fg='white', anchor='c')
+            keylabel = tk.Label(keyhintrow,
+                                text='%s\n%s' % (upkey, downkey),
+                                width=1,
+                                font=('Arial', 10),
+                                bg='red',
+                                fg='white',
+                                anchor='c')
             keylabel.pack(side=tk.LEFT, expand=1, fill=tk.X)
             col += 1
 
@@ -322,7 +367,9 @@
         self.change_row(self.current_row + diff)
 
     def rowFromGraph(self):
-        self.change_row(int(self.graph.value(self.session, L9['currentRow'], default=0)), fromGraph=True)
+        self.change_row(int(
+            self.graph.value(self.session, L9['currentRow'], default=0)),
+                        fromGraph=True)
 
     def change_row(self, row, fromGraph=False):
         old_row = self.current_row
@@ -357,8 +404,7 @@
                 self.sliders.valueOut("button-upper%d" % col, False)
                 self.sliders.valueOut("slider%d" % col, 0)
                 continue
-            self.send_to_hw(sub=subbox.sub, hwCol=col,
-                            boxRow=self.current_row)
+            self.send_to_hw(sub=subbox.sub, hwCol=col, boxRow=self.current_row)
 
     def got_nudger(self, number, direction, full=0):
         try:
@@ -382,7 +428,7 @@
         try:
             subbox = self.slider_table[(self.current_row, col)]
         except KeyError:
-            return # no slider assigned at that column
+            return  # no slider assigned at that column
 
         if hasattr(self, 'pendingHwSet'):
             import twisted.internet.error
@@ -400,7 +446,7 @@
 
         if boxRow != self.current_row:
             return
-        
+
         try:
             level = self.get_levels()[sub]
         except KeyError:
@@ -421,13 +467,16 @@
         """group is a URI or None"""
         row = tk.Frame(self, bd=2, bg='black')
         row.subGroup = group
+
         def onDrop(ev):
             self.change_group(sub=URIRef(ev.data), row=row)
             return "link"
-        
-        dropTargetRegister(row, onDrop=onDrop, typeList=['*'],
+
+        dropTargetRegister(row,
+                           onDrop=onDrop,
+                           typeList=['*'],
                            hoverStyle=dict(background="#555500"))
-        
+
         row.pack(expand=1, fill=tk.BOTH)
         self.setup_key_nudgers(row)
         self.rows.append(row)
@@ -437,9 +486,10 @@
         """update this sub's group, and maybe other sub groups as needed, so
         this sub displays in this row"""
         group = row.subGroup
-        self.graph.patchObject(
-            context=self.session,
-            subject=sub, predicate=L9['group'], newObject=group)
+        self.graph.patchObject(context=self.session,
+                               subject=sub,
+                               predicate=L9['group'],
+                               newObject=group)
 
     def highlight_row(self, row):
         row = self.rows[row]
@@ -450,8 +500,9 @@
         row['bg'] = 'black'
 
     def get_levels(self):
-        return dict([(uri, box.slider_var.get())
-            for uri, box in self.subbox.items()])
+        return dict([
+            (uri, box.slider_var.get()) for uri, box in self.subbox.items()
+        ])
 
     def get_output_settings(self, _graph=None):
         _graph = _graph or self.graph
@@ -481,11 +532,11 @@
             (effect, RDF.type, L9['Effect'], ctx),
             (effect, RDFS.label, Literal(subname), ctx),
             (effect, L9['publishAttr'], L9['strength'], ctx),
-            ])
+        ])
 
         self.graph.suggestPrefixes(ctx, {'eff': effect + '/'})
         self.graph.patch(Patch(addQuads=stmts, delQuads=[]))
-              
+
         self.sub_name.delete(0, tk.END)
 
     def alltozero(self):
@@ -493,6 +544,7 @@
             if subbox.scale.scale_var.get() != 0:
                 subbox.scale.fade(value=0.0, length=0)
 
+
 # move to web lib
 def postArgGetter(request):
     """return a function that takes arg names and returns string
@@ -500,19 +552,23 @@
     support for repeated args."""
     # this is something nevow normally does for me
     request.content.seek(0)
-    fields = cgi.FieldStorage(request.content, request.received_headers,
+    fields = cgi.FieldStorage(request.content,
+                              request.received_headers,
                               environ={'REQUEST_METHOD': 'POST'})
+
     def getArg(n):
         try:
             return request.args[n][0]
         except KeyError:
             return fields[n].value
+
     return getArg
 
 
 class LevelServerHttp(resource.Resource):
     isLeaf = True
-    def __init__(self,name_to_subbox):
+
+    def __init__(self, name_to_subbox):
         self.name_to_subbox = name_to_subbox
 
     def render_POST(self, request):
@@ -521,15 +577,18 @@
         if request.path == '/fadesub':
             # fadesub?subname=scoop&level=0&secs=.2
             self.name_to_subbox[arg('subname')].scale.fade(
-                float(arg('level')),
-                float(arg('secs')))
+                float(arg('level')), float(arg('secs')))
             return "set %s to %s" % (arg('subname'), arg('level'))
         else:
             raise NotImplementedError(repr(request))
 
+
 class Sliders(BCF2000):
+
     def __init__(self, kc):
-        devices = ['/dev/snd/midiC3D0', '/dev/snd/midiC2D0', '/dev/snd/midiC1D0']
+        devices = [
+            '/dev/snd/midiC3D0', '/dev/snd/midiC2D0', '/dev/snd/midiC1D0'
+        ]
         for dev in devices:
             try:
                 BCF2000.__init__(self, dev=dev)
@@ -541,6 +600,7 @@
 
         self.kc = kc
         log.info('found sliders on %s', dev)
+
     def valueIn(self, name, value):
         kc = self.kc
         if name.startswith("slider"):
@@ -572,21 +632,25 @@
             kc.change_row(kc.current_row + diff)
             self.valueOut(name, 0)
 
+
 def launch(opts, root, graph, session):
     tl = toplevelat("Keyboard Composer - %s" % opts.session,
-                    existingtoplevel=root, graph=graph, session=session)
+                    existingtoplevel=root,
+                    graph=graph,
+                    session=session)
 
-    kc = KeyboardComposer(tl, graph, session,
-                          hw_sliders=not opts.no_sliders)
+    kc = KeyboardComposer(tl, graph, session, hw_sliders=not opts.no_sliders)
     kc.pack(fill=tk.BOTH, expand=1)
 
     for helpline in ["Bindings: B3 mute"]:
-        tk.Label(root,text=helpline, font="Helvetica -12 italic",
-                 anchor='w').pack(side='top',fill='x')
-            
+        tk.Label(root, text=helpline, font="Helvetica -12 italic",
+                 anchor='w').pack(side='top', fill='x')
+
+
 if __name__ == "__main__":
     parser = OptionParser()
-    parser.add_option('--no-sliders', action='store_true',
+    parser.add_option('--no-sliders',
+                      action='store_true',
                       help="don't attach to hardware sliders")
     clientsession.add_option(parser)
     parser.add_option('-v', action='store_true', help="log info level")
@@ -598,16 +662,16 @@
 
     # i think this also needs delayed start (like subcomposer has), to have a valid graph
     # before setting any stuff from the ui
-    
+
     root = tk.Tk()
     initTkdnd(root.tk, 'tkdnd/trunk/')
 
     session = clientsession.getUri('keyboardcomposer', opts)
 
-    graph.initiallySynced.addCallback(
-        lambda _: launch(opts, root, graph, session))
+    graph.initiallySynced.addCallback(lambda _: launch(opts, root, graph,
+                                                       session))
 
     root.protocol('WM_DELETE_WINDOW', reactor.stop)
 
-    tksupport.install(root,ms=20)
+    tksupport.install(root, ms=20)
     prof.run(reactor.run, profile=None)
--- a/bin/lightsim	Tue May 21 23:55:35 2019 +0000
+++ b/bin/lightsim	Tue May 21 23:56:12 2019 +0000
@@ -21,26 +21,33 @@
 from lightsim.openglsim import Surface
 
 log = logging.getLogger()
-logging.basicConfig(format="%(asctime)s %(levelname)-5s %(name)s %(filename)s:%(lineno)d: %(message)s")
+logging.basicConfig(
+    format=
+    "%(asctime)s %(levelname)-5s %(name)s %(filename)s:%(lineno)d: %(message)s")
 log.setLevel(logging.DEBUG)
 
+
 def filenamesForChan(graph, chan):
     for lyr in graph.objects(chan, L9['previewLayer']):
         for imgPath in graph.objects(lyr, L9['path']):
             yield imgPath
 
+
 _lastLevels = None
+
+
 def poll(graph, serv, pollFreq, oglSurface):
     pollFreq.update()
     dispatcher.send("status", key="pollFreq", value=str(pollFreq))
     d = serv.callRemote("currentlevels", dmxclient._id)
+
     def received(dmxLevels):
         global _lastLevels
         if dmxLevels == _lastLevels:
             return
         _lastLevels = dmxLevels
 
-        level = {} # filename : level
+        level = {}  # filename : level
         for i, lev in enumerate(dmxLevels):
             if lev == 0:
                 continue
@@ -54,18 +61,21 @@
                 level[str(imgPath)] = lev
 
         oglSurface.newLevels(levels=level)
+
     d.addCallback(received)
     return d
 
+
 class StatusKeys(QWidget):
     """listens for dispatcher signal 'status' and displays the key/value args"""
+
     def __init__(self, parent):
         QWidget.__init__(self)
         self.layout = QVBoxLayout()
         self.setLayout(self.layout)
-        self.row = {} # key name : (Frame, value Label)
+        self.row = {}  # key name : (Frame, value Label)
         dispatcher.connect(self.status, "status")
-        
+
     def status(self, key, value):
         if key not in self.row:
             row = QWidget()
@@ -81,7 +91,9 @@
             lab = self.row[key]
             lab.setText(value)
 
+
 class Window(QMainWindow):
+
     def __init__(self, filenames):
         QMainWindow.__init__(self, None)
         self.setWindowTitle(dmxclient._id)
@@ -90,13 +102,14 @@
         self.setCentralWidget(w)
         mainLayout = QVBoxLayout()
         w.setLayout(mainLayout)
-        
-        self.glWidget = Surface(self, filenames, imgRescaleTo=128*2)
+
+        self.glWidget = Surface(self, filenames, imgRescaleTo=128 * 2)
 
         mainLayout.addWidget(self.glWidget)
 
         status = StatusKeys(mainLayout)
-        mainLayout.addWidget(status)      
+        mainLayout.addWidget(status)
+
 
 def requiredImages(graph):
     """filenames that we'll need to show, based on a config structure
@@ -110,6 +123,7 @@
             filenames.append(str(p))
     return filenames
 
+
 if __name__ == '__main__':
     app = reactor.qApp
 
@@ -123,4 +137,3 @@
     LoopingCall(poll, graph, serv, pollFreq, window.glWidget).start(.05)
 
     reactor.run()
-
--- a/bin/listsongs	Tue May 21 23:55:35 2019 +0000
+++ b/bin/listsongs	Tue May 21 23:56:12 2019 +0000
@@ -1,5 +1,4 @@
 #!bin/python
-
 """for completion, print the available song uris on stdout
 
 in .zshrc:
@@ -17,6 +16,7 @@
 
 graph = SyncedGraph(networking.rdfdb.url, "listsongs")
 
+
 @graph.initiallySynced.addCallback
 def printSongs(result):
     with graph.currentState() as current:
@@ -24,4 +24,5 @@
             print song
     reactor.stop()
 
+
 reactor.run()
--- a/bin/mpd_timing_test	Tue May 21 23:55:35 2019 +0000
+++ b/bin/mpd_timing_test	Tue May 21 23:56:12 2019 +0000
@@ -1,5 +1,4 @@
 #!/usr/bin/python
-
 """
 records times coming out of ascoltami
 
@@ -17,5 +16,5 @@
 s = xmlrpclib.ServerProxy("http://localhost:8040")
 start = time.time()
 while 1:
-    print time.time()-start,s.gettime()
+    print time.time() - start, s.gettime()
     time.sleep(.01)
--- a/bin/musicPad	Tue May 21 23:55:35 2019 +0000
+++ b/bin/musicPad	Tue May 21 23:56:12 2019 +0000
@@ -11,7 +11,7 @@
 log = logging.getLogger()
 
 introPad = 4
-postPad = 9 # 5 + autostop + 4
+postPad = 9  # 5 + autostop + 4
 
 playlist = Playlist.fromShow(showconfig.getGraph(), showconfig.showUri())
 for p in playlist.allSongPaths():
@@ -22,18 +22,16 @@
     try:
         os.makedirs(outputDir)
     except OSError:
-        pass # exists
+        pass  # exists
     outputPath = os.path.join(outputDir, os.path.basename(p))
     outputWave = wave.open(outputPath, 'w')
     outputWave.setparams(inputWave.getparams())
 
     bytesPerSecond = (inputWave.getnchannels() * inputWave.getsampwidth() *
                       inputWave.getframerate())
-    
+
     outputWave.writeframesraw("\x00" * (bytesPerSecond * introPad))
     outputWave.writeframesraw(inputWave.readframes(inputWave.getnframes()))
     outputWave.writeframesraw("\x00" * (bytesPerSecond * postPad))
     outputWave.close()
     log.info("wrote %s", outputPath)
-
-    
--- a/bin/musictime	Tue May 21 23:55:35 2019 +0000
+++ b/bin/musictime	Tue May 21 23:56:12 2019 +0000
@@ -6,9 +6,12 @@
 import time
 import restkit, jsonlib
 
+
 class MusicTime:
+
     def __init__(self, url):
         self.player = restkit.Resource(url)
+
     def get_music_time(self):
         playtime = None
         while not playtime:
@@ -20,31 +23,43 @@
                 time.sleep(2)
         return playtime
 
+
 class MusicTimeTk(tk.Frame, MusicTime):
+
     def __init__(self, master, url):
         tk.Frame.__init__(self)
         MusicTime.__init__(self, url)
         self.timevar = tk.DoubleVar()
-        self.timelabel = tk.Label(self, textvariable=self.timevar, bd=2,
-            relief='raised', width=10, padx=2, pady=2, anchor='w')
+        self.timelabel = tk.Label(self,
+                                  textvariable=self.timevar,
+                                  bd=2,
+                                  relief='raised',
+                                  width=10,
+                                  padx=2,
+                                  pady=2,
+                                  anchor='w')
         self.timelabel.pack(expand=1, fill='both')
+
         def print_time(evt, *args):
             self.timevar.set(self.get_music_time())
             print self.timevar.get(), evt.keysym
+
         self.timelabel.bind('<KeyPress>', print_time)
         self.timelabel.bind('<1>', print_time)
         self.timelabel.focus()
         self.update_time()
+
     def update_time(self):
         self.timevar.set(self.get_music_time())
         self.after(100, self.update_time)
 
+
 if __name__ == "__main__":
     from optparse import OptionParser
     parser = OptionParser()
     parser.add_option("-u", "--url", default=light9.networking.musicPlayer.url)
     options, args = parser.parse_args()
-    
+
     root = tk.Tk()
     root.title("Time")
     MusicTimeTk(root, options.url).pack(expand=1, fill='both')
--- a/bin/paintserver	Tue May 21 23:55:35 2019 +0000
+++ b/bin/paintserver	Tue May 21 23:56:12 2019 +0000
@@ -17,31 +17,38 @@
 from light9.namespaces import RDF, L9, DEV
 
 
-
+class Solve(PrettyErrorHandler, cyclone.web.RequestHandler):
 
-class Solve(PrettyErrorHandler, cyclone.web.RequestHandler):
     def post(self):
         painting = json.loads(self.request.body)
         with self.settings.stats.solve.time():
             img = self.settings.solver.draw(painting)
-            sample, sampleDist = self.settings.solver.bestMatch(img, device=DEV['aura2'])
+            sample, sampleDist = self.settings.solver.bestMatch(
+                img, device=DEV['aura2'])
             with self.settings.graph.currentState() as g:
                 bestPath = g.value(sample, L9['imagePath']).replace(L9[''], '')
             #out = solver.solve(painting)
             #layers = solver.simulationLayers(out)
-            
-        self.write(json.dumps({
-            'bestMatch': {'uri': sample, 'path': bestPath, 'dist': sampleDist},
-        #    'layers': layers,
-        #    'out': out,
-        }))
+
+        self.write(
+            json.dumps({
+                'bestMatch': {
+                    'uri': sample,
+                    'path': bestPath,
+                    'dist': sampleDist
+                },
+                #    'layers': layers,
+                #    'out': out,
+            }))
 
     def reloadSolver(self):
         reload(light9.paint.solve)
         self.settings.solver = light9.paint.solve.Solver(self.settings.graph)
         self.settings.solver.loadSamples()
 
+
 class BestMatches(PrettyErrorHandler, cyclone.web.RequestHandler):
+
     def post(self):
         body = json.loads(self.request.body)
         painting = body['painting']
@@ -49,35 +56,40 @@
         with self.settings.stats.solve.time():
             img = self.settings.solver.draw(painting)
             outSettings = self.settings.solver.bestMatches(img, devs)
-            self.write(json.dumps({
-                'settings': outSettings.asList()
-                }))
-        
+            self.write(json.dumps({'settings': outSettings.asList()}))
+
+
 class App(object):
+
     def __init__(self, show, session):
         self.show = show
         self.session = session
 
         self.graph = SyncedGraph(networking.rdfdb.url, "paintServer")
-        self.graph.initiallySynced.addCallback(self.launch).addErrback(log.error)
-        
-        self.stats = scales.collection('/', scales.PmfStat('solve'),
-                                       )
-       
+        self.graph.initiallySynced.addCallback(self.launch).addErrback(
+            log.error)
+
+        self.stats = scales.collection(
+            '/',
+            scales.PmfStat('solve'),
+        )
+
     def launch(self, *args):
 
-        self.solver = light9.paint.solve.Solver(self.graph, sessions=[
-            L9['show/dance2017/capture/aura1/cap1876596'],
-            L9['show/dance2017/capture/aura2/cap1876792'],
-            L9['show/dance2017/capture/aura3/cap1877057'],
-            L9['show/dance2017/capture/aura4/cap1877241'],
-            L9['show/dance2017/capture/aura5/cap1877406'],
-            L9['show/dance2017/capture/q1/cap1874255'],
-            L9['show/dance2017/capture/q2/cap1873665'],
-            L9['show/dance2017/capture/q3/cap1876223'],
-        ])
+        self.solver = light9.paint.solve.Solver(
+            self.graph,
+            sessions=[
+                L9['show/dance2017/capture/aura1/cap1876596'],
+                L9['show/dance2017/capture/aura2/cap1876792'],
+                L9['show/dance2017/capture/aura3/cap1877057'],
+                L9['show/dance2017/capture/aura4/cap1877241'],
+                L9['show/dance2017/capture/aura5/cap1877406'],
+                L9['show/dance2017/capture/q1/cap1874255'],
+                L9['show/dance2017/capture/q2/cap1873665'],
+                L9['show/dance2017/capture/q3/cap1876223'],
+            ])
         self.solver.loadSamples()
-        
+
         self.cycloneApp = cyclone.web.Application(handlers=[
             (r'/stats', StatsForCyclone),
             (r'/solve', Solve),
@@ -93,12 +105,16 @@
 
 if __name__ == "__main__":
     parser = optparse.OptionParser()
-    parser.add_option('--show',
+    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",
+        default=showconfig.showUri())
+    parser.add_option("-v",
+                      "--verbose",
+                      action="store_true",
                       help="logging.DEBUG")
-    parser.add_option("--twistedlog", action="store_true",
+    parser.add_option("--twistedlog",
+                      action="store_true",
                       help="twisted logging")
     clientsession.add_option(parser)
     (options, args) = parser.parse_args()
@@ -106,7 +122,7 @@
 
     if not options.show:
         raise ValueError("missing --show http://...")
-        
+
     session = clientsession.getUri('paint', options)
 
     app = App(URIRef(options.show), session)
--- a/bin/picamserve	Tue May 21 23:55:35 2019 +0000
+++ b/bin/picamserve	Tue May 21 23:56:12 2019 +0000
@@ -1,7 +1,8 @@
 #!env_pi/bin/python
 from __future__ import division
 from run_local import log
-import sys;sys.path.append('/usr/lib/python2.7/dist-packages/')
+import sys
+sys.path.append('/usr/lib/python2.7/dist-packages/')
 import io, logging, traceback, time
 import cyclone.web
 from twisted.internet import reactor, threads
@@ -12,16 +13,24 @@
     import picamera
     cameraCls = picamera.PiCamera
 except ImportError:
+
     class cameraCls(object):
-        def __enter__(self): return self
-        def __exit__(self, *a): pass
+
+        def __enter__(self):
+            return self
+
+        def __exit__(self, *a):
+            pass
+
         def capture(self, out, *a, **kw):
             out.write(open('yuv.demo').read())
+
         def capture_continuous(self, *a, **kw):
             for i in range(1000):
                 time.sleep(1)
                 yield str(i)
 
+
 def setCameraParams(c, arg):
     res = int(arg('res', 480))
     c.resolution = {
@@ -33,14 +42,15 @@
     c.exposure_mode = arg('exposure_mode', 'fixedfps')
     c.awb_mode = arg('awb_mode', 'off')
     c.brightness = int(arg('brightness', 50))
-    c.exposure_compensation= int(arg('exposure_compensation', 0))
+    c.exposure_compensation = int(arg('exposure_compensation', 0))
     c.awb_gains = (float(arg('redgain', 1)), float(arg('bluegain', 1)))
     c.ISO = int(arg('iso', 250))
     c.rotation = int(arg('rotation', '0'))
 
+
 def setupCrop(c, arg):
-    c.crop = (float(arg('x', 0)), float(arg('y', 0)),
-              float(arg('w', 1)), float(arg('h', 1)))
+    c.crop = (float(arg('x', 0)), float(arg('y', 0)), float(arg('w', 1)),
+              float(arg('h', 1)))
     rw = rh = int(arg('resize', 100))
     # width 1920, showing w=.3 of image, resize=100 -> scale is 100/.3*1920
     # scl is [ output px / camera px ]
@@ -53,7 +63,8 @@
         # height is the constraint
         rw = int(scl2 * c.crop[2] * c.resolution[0])
     return rw, rh
-    
+
+
 @prof.logTime
 def getFrame(c, arg):
     setCameraParams(c, arg)
@@ -61,8 +72,10 @@
     out = io.BytesIO('w')
     prof.logTime(c.capture)(out, 'jpeg', use_video_port=True, resize=resize)
     return out.getvalue()
-    
+
+
 class Pic(cyclone.web.RequestHandler):
+
     def get(self):
         try:
             self.set_header('Content-Type', 'image/jpeg')
@@ -70,20 +83,24 @@
         except Exception:
             traceback.print_exc()
 
+
 def captureContinuousAsync(c, resize, onFrame):
     """
     Calls c.capture_continuous is called in another thread. onFrame is
     called in this reactor thread with each (frameTime, frame)
     result. Runs until onFrame raises StopIteration.
     """
+
     def runner(c, resize):
         stream = io.BytesIO()
         t = time.time()
-        for nextFrame in c.capture_continuous(stream, 'jpeg', use_video_port=True,
+        for nextFrame in c.capture_continuous(stream,
+                                              'jpeg',
+                                              use_video_port=True,
                                               resize=resize):
             t2 = time.time()
             log.debug(" - framecap got %s bytes in %.1f ms",
-                     len(stream.getvalue()), 1000 * (t2 - t))
+                      len(stream.getvalue()), 1000 * (t2 - t))
             try:
                 # This is slow, like 13ms. Hopefully
                 # capture_continuous is working on gathering the next
@@ -91,8 +108,8 @@
                 # Instead, we could be stashing frames onto a queue or
                 # something that the main thread can pull when
                 # possible (and toss if it gets behind).
-                threads.blockingCallFromThread(reactor,
-                                               onFrame, t, stream.getvalue())
+                threads.blockingCallFromThread(reactor, onFrame, t,
+                                               stream.getvalue())
             except StopIteration:
                 break
             t3 = time.time()
@@ -100,30 +117,35 @@
             stream.truncate()
             stream.seek(0)
             t = time.time()
+
     return threads.deferToThread(runner, c, resize)
 
+
 class FpsReport(object):
+
     def __init__(self):
         self.frameTimes = []
         self.lastFpsLog = 0
-        
+
     def frame(self):
         now = time.time()
-        
+
         self.frameTimes.append(now)
-        
+
         if len(self.frameTimes) > 15:
             del self.frameTimes[:5]
-            
+
         if now > self.lastFpsLog + 2 and len(self.frameTimes) > 5:
-            deltas = [(b - a) for a, b in zip(self.frameTimes[:-1],
-                                              self.frameTimes[1:])]
+            deltas = [(b - a)
+                      for a, b in zip(self.frameTimes[:-1], self.frameTimes[1:])
+                     ]
             avg = sum(deltas) / len(deltas)
             log.info("fps: %.1f", 1 / avg)
             self.lastFpsLog = now
-        
-    
+
+
 class Pics(cyclone.web.RequestHandler):
+
     @inlineCallbacks
     def get(self):
         try:
@@ -131,21 +153,22 @@
             c = self.settings.camera
             setCameraParams(c, self.get_argument)
             resize = setupCrop(c, self.get_argument)
-                          
+
             self.running = True
             log.info("connection open from %s", self.request.remote_ip)
             fpsReport = FpsReport()
-            
+
             def onFrame(frameTime, frame):
                 if not self.running:
                     raise StopIteration
-                    
+
                 now = time.time()
                 self.write("%s %s\n" % (len(frame), frameTime))
                 self.write(frame)
                 self.flush()
 
                 fpsReport.frame()
+
             # another camera request coming in at the same time breaks
             # the server. it would be nice if this request could
             # let-go-and-reopen when it knows about another request
@@ -153,21 +176,30 @@
             yield captureContinuousAsync(c, resize, onFrame)
         except Exception:
             traceback.print_exc()
-            
+
     def on_connection_close(self, *a, **kw):
         log.info("connection closed")
         self.running = False
-            
+
+
 log.setLevel(logging.INFO)
 
 with cameraCls() as camera:
     port = 8208
-    reactor.listenTCP(port, cyclone.web.Application(handlers=[
-        (r'/pic', Pic),
-        (r'/pics', Pics),
-        (r'/static/(.*)', cyclone.web.StaticFileHandler, {'path': 'light9/web/'}),
-        (r'/(|gui.js)', cyclone.web.StaticFileHandler, {'path': 'light9/vidref/',
-                                                 'default_filename': 'index.html'}),
-        ], debug=True, camera=camera))
+    reactor.listenTCP(
+        port,
+        cyclone.web.Application(handlers=[
+            (r'/pic', Pic),
+            (r'/pics', Pics),
+            (r'/static/(.*)', cyclone.web.StaticFileHandler, {
+                'path': 'light9/web/'
+            }),
+            (r'/(|gui.js)', cyclone.web.StaticFileHandler, {
+                'path': 'light9/vidref/',
+                'default_filename': 'index.html'
+            }),
+        ],
+                                debug=True,
+                                camera=camera))
     log.info("serving on %s" % port)
     reactor.run()
--- a/bin/rdfdb	Tue May 21 23:55:35 2019 +0000
+++ b/bin/rdfdb	Tue May 21 23:56:12 2019 +0000
@@ -5,8 +5,9 @@
 import rdfdb.service
 
 rdfdb.service.main(
-    dirUriMap={os.environ['LIGHT9_SHOW'].rstrip('/') + '/':
-               showconfig.showUri() + '/'},
+    dirUriMap={
+        os.environ['LIGHT9_SHOW'].rstrip('/') + '/': showconfig.showUri() + '/'
+    },
     prefixes={
         'show': showconfig.showUri() + '/',
         '': 'http://light9.bigasterisk.com/',
@@ -17,5 +18,4 @@
         'dev': 'http://light9.bigasterisk.com/device/',
     },
     port=networking.rdfdb.port,
-    )
-
+)
--- a/bin/run_local.py	Tue May 21 23:55:35 2019 +0000
+++ b/bin/run_local.py	Tue May 21 23:56:12 2019 +0000
@@ -4,30 +4,37 @@
 
 import sys, os, socket
 
+
 def fixSysPath():
-    root = os.path.abspath(os.path.join(os.path.dirname(sys.argv[0]), '..')) + '/'
+    root = os.path.abspath(os.path.join(os.path.dirname(sys.argv[0]),
+                                        '..')) + '/'
 
     # this is site-packages/zope.interface-4.5.0-py2.7-nspkg.pth,
     # slightly edited.
     import types
-    has_mfs = sys.version_info > (3, 5);
+    has_mfs = sys.version_info > (3, 5)
     p = root + 'env/local/lib/python2.7/site-packages/zope'
-    importlib = has_mfs and __import__('importlib.util');
-    has_mfs and __import__('importlib.machinery');
+    importlib = has_mfs and __import__('importlib.util')
+    has_mfs and __import__('importlib.machinery')
     m = has_mfs and sys.modules.setdefault(
-        'zope', importlib.util.module_from_spec(
-            importlib.machinery.PathFinder.find_spec(
-                'zope', [os.path.dirname(p)])));
-    m = m or sys.modules.setdefault('zope', types.ModuleType('zope'));
-    mp = (m or []) and m.__dict__.setdefault('__path__',[]);
+        'zope',
+        importlib.util.module_from_spec(
+            importlib.machinery.PathFinder.find_spec('zope',
+                                                     [os.path.dirname(p)])))
+    m = m or sys.modules.setdefault('zope', types.ModuleType('zope'))
+    mp = (m or []) and m.__dict__.setdefault('__path__', [])
     (p not in mp) and mp.append(p)
-    
+
     p = root + 'env/local/lib/python2.7/site-packages/greplin'
-    importlib = has_mfs and __import__('importlib.util');
-    has_mfs and __import__('importlib.machinery');
-    m = has_mfs and sys.modules.setdefault('greplin', importlib.util.module_from_spec(importlib.machinery.PathFinder.find_spec('greplin', [os.path.dirname(p)])));
-    m = m or sys.modules.setdefault('greplin', types.ModuleType('greplin'));
-    mp = (m or []) and m.__dict__.setdefault('__path__',[]);
+    importlib = has_mfs and __import__('importlib.util')
+    has_mfs and __import__('importlib.machinery')
+    m = has_mfs and sys.modules.setdefault(
+        'greplin',
+        importlib.util.module_from_spec(
+            importlib.machinery.PathFinder.find_spec('greplin',
+                                                     [os.path.dirname(p)])))
+    m = m or sys.modules.setdefault('greplin', types.ModuleType('greplin'))
+    mp = (m or []) and m.__dict__.setdefault('__path__', [])
     (p not in mp) and mp.append(p)
 
     sys.path = [
@@ -46,6 +53,7 @@
         root + 'env/lib/python2.7/site-packages/gtk-2.0',
     ]
 
+
 fixSysPath()
 
 from twisted.python.failure import Failure
@@ -55,12 +63,14 @@
 except ImportError:
     pass
 else:
+
     def rce(self, exc, val, tb):
         sys.stderr.write("Exception in Tkinter callback\n")
         if True:
             sys.excepthook(exc, val, tb)
         else:
             Failure(val, exc, tb).printDetailedTraceback()
+
     Tkinter.Tk.report_callback_exception = rce
 
 import coloredlogs, logging, time
@@ -71,10 +81,12 @@
     pass
 
 progName = sys.argv[0].split('/')[-1]
-log = logging.getLogger() # this has to get the root logger
-log.name = progName # but we can rename it for clarity
+log = logging.getLogger()  # this has to get the root logger
+log.name = progName  # but we can rename it for clarity
+
 
 class FractionTimeFilter(logging.Filter):
+
     def filter(self, record):
         record.fractionTime = (
             time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(record.created)) +
@@ -82,6 +94,7 @@
         # Don't filter the record.
         return 1
 
+
 coloredlogs.install(
     level='DEBUG',
     fmt='%(fractionTime)s %(name)s[%(process)d] %(levelname)s %(message)s')
@@ -90,10 +103,13 @@
 
 def setTerminalTitle(s):
     if os.environ.get('TERM', '') in ['xterm', 'rxvt', 'rxvt-unicode-256color']:
-        print "\033]0;%s\007" % s # not escaped/protected correctly
+        print "\033]0;%s\007" % s  # not escaped/protected correctly
+
 
 if 'listsongs' not in sys.argv[0] and 'homepageConfig' not in sys.argv[0]:
-    setTerminalTitle('[%s] %s' % (socket.gethostname(), ' '.join(sys.argv).replace('bin/', '')))
+    setTerminalTitle(
+        '[%s] %s' %
+        (socket.gethostname(), ' '.join(sys.argv).replace('bin/', '')))
 
 # see http://www.youtube.com/watch?v=3cIOT9kM--g for commands that make
 # profiles and set background images
--- a/bin/staticclient	Tue May 21 23:55:35 2019 +0000
+++ b/bin/staticclient	Tue May 21 23:56:12 2019 +0000
@@ -16,7 +16,8 @@
 
 if __name__ == "__main__":
     parser = OptionParser(usage="%prog")
-    parser.add_option('--chan', help='channel number, starts at 1', type=int) #todo: or name or uri
+    parser.add_option('--chan', help='channel number, starts at 1',
+                      type=int)  #todo: or name or uri
     parser.add_option('--level', help='0..1', type=float)
     parser.add_option('-v', action='store_true', help="log debug level")
 
@@ -26,9 +27,11 @@
 
     levels = [0] * (opts.chan - 1) + [opts.level]
     log.info('staticclient will write this forever: %r', levels)
+
     def write():
         log.debug('writing %r', levels)
         dmxclient.outputlevels(levels, twisted=1)
+
     log.info('looping...')
     task.LoopingCall(write).start(1)
     reactor.run()
--- a/bin/subcomposer	Tue May 21 23:55:35 2019 +0000
+++ b/bin/subcomposer	Tue May 21 23:56:12 2019 +0000
@@ -69,6 +69,7 @@
       Submaster.editLevel -> graph (handled in Submaster)
 
     """
+
     def __init__(self, master, graph, session):
         tk.Frame.__init__(self, master, bg='black')
         self.graph = graph
@@ -83,11 +84,12 @@
         self._currentChoice = Observable(Local)
 
         # this is a PersistentSubmaster (even for local)
-        self.currentSub = Observable(Submaster.PersistentSubmaster(
-                    graph, self.switchToLocal()))
+        self.currentSub = Observable(
+            Submaster.PersistentSubmaster(graph, self.switchToLocal()))
 
         def pc(val):
             log.info("change viewed sub to %s", val)
+
         self._currentChoice.subscribe(pc)
 
         ec = self.editChoice = EditChoice(self, self.graph, self._currentChoice)
@@ -105,39 +107,41 @@
         e.pack()
         b = tk.Button(box, text="Make global")
         b.pack()
+
         def clicked(*args):
             self.makeGlobal(newName=e.get())
             box.destroy()
-            
+
         b.bind("<Button-1>", clicked)
         e.focus()
-        
+
     def makeGlobal(self, newName):
         """promote our local submaster into a non-local, named one"""
         uri = self.currentSub().uri
         newUri = showconfig.showUri() + ("/sub/%s" %
                                          urllib.quote(newName, safe=''))
-        with self.graph.currentState(
-                tripleFilter=(uri, None, None)) as current:
+        with self.graph.currentState(tripleFilter=(uri, None, None)) as current:
             if (uri, RDF.type, L9['LocalSubmaster']) not in current:
                 raise ValueError("%s is not a local submaster" % uri)
             if (newUri, None, None) in current:
                 raise ValueError("new uri %s is in use" % newUri)
-                
+
         # the local submaster was storing in ctx=self.session, but now
         # we want it to be in ctx=uri
 
         self.relocateSub(newUri, newName)
 
         # these are in separate patches for clarity as i'm debugging this
-        self.graph.patch(Patch(addQuads=[
-            (newUri, RDFS.label, Literal(newName), newUri),
-        ], delQuads=[
-            (newUri, RDF.type, L9['LocalSubmaster'], newUri),
-                           ]))
-        self.graph.patchObject(self.session,
-                               self.session, L9['currentSub'], newUri)
-        
+        self.graph.patch(
+            Patch(addQuads=[
+                (newUri, RDFS.label, Literal(newName), newUri),
+            ],
+                  delQuads=[
+                      (newUri, RDF.type, L9['LocalSubmaster'], newUri),
+                  ]))
+        self.graph.patchObject(self.session, self.session, L9['currentSub'],
+                               newUri)
+
     def relocateSub(self, newUri, newName):
         # maybe this goes in Submaster
         uri = self.currentSub().uri
@@ -146,15 +150,16 @@
             if u == uri:
                 return newUri
             return u
+
         delQuads = self.currentSub().allQuads()
-        addQuads = [(repl(s), p, repl(o), newUri) for s,p,o,c in delQuads]
+        addQuads = [(repl(s), p, repl(o), newUri) for s, p, o, c in delQuads]
         # patch can't span contexts yet
         self.graph.patch(Patch(addQuads=addQuads, delQuads=[]))
         self.graph.patch(Patch(addQuads=[], delQuads=delQuads))
-        
-        
+
     def setupSubChoiceLinks(self):
         graph = self.graph
+
         def ann():
             print "currently: session=%s currentSub=%r _currentChoice=%r" % (
                 self.session, self.currentSub(), self._currentChoice())
@@ -165,7 +170,7 @@
             # are failing. this calms things down.
             log.warn('skip graphChanged')
             return
-        
+
             s = graph.value(self.session, L9['currentSub'])
             log.debug('HANDLER getting session currentSub from graph: %s', s)
             if s is None:
@@ -176,12 +181,12 @@
         def subChanged(newSub):
             log.debug('HANDLER currentSub changed to %s', newSub)
             if newSub is None:
-                graph.patchObject(self.session,
-                                  self.session, L9['currentSub'], None)
+                graph.patchObject(self.session, self.session, L9['currentSub'],
+                                  None)
                 return
             self.sendupdate()
-            graph.patchObject(self.session,
-                              self.session, L9['currentSub'], newSub.uri)
+            graph.patchObject(self.session, self.session, L9['currentSub'],
+                              newSub.uri)
 
             localStmt = (newSub.uri, RDF.type, L9['LocalSubmaster'])
             with graph.currentState(tripleFilter=localStmt) as current:
@@ -195,7 +200,7 @@
                     self._currentChoice(newSub.uri)
 
         dispatcher.connect(self.levelsChanged, "sub levels changed")
-            
+
         @self._currentChoice.subscribe
         def choiceChanged(newChoice):
             log.debug('HANDLER choiceChanged to %s', newChoice)
@@ -203,13 +208,14 @@
                 newChoice = self.switchToLocal()
             if newChoice is not None:
                 newSub = Submaster.PersistentSubmaster(graph, newChoice)
-                log.debug('write new choice to currentSub, from %r to %r', self.currentSub(), newSub)
+                log.debug('write new choice to currentSub, from %r to %r',
+                          self.currentSub(), newSub)
                 self.currentSub(newSub)
 
     def levelsChanged(self, sub):
         if sub == self.currentSub():
             self.sendupdate()
-        
+
     def switchToLocal(self):
         """
         change our display to a local submaster
@@ -220,27 +226,30 @@
         self.localSerial += 1
         new = URIRef("http://light9.bigasterisk.com/sub/local/%s" % localId)
         log.debug('making up a local sub %s', new)
-        self.graph.patch(Patch(addQuads=[
-            (new, RDF.type, L9['Submaster'], self.session),
-            (new, RDF.type, L9['LocalSubmaster'], self.session),
-        ]))
-        
+        self.graph.patch(
+            Patch(addQuads=[
+                (new, RDF.type, L9['Submaster'], self.session),
+                (new, RDF.type, L9['LocalSubmaster'], self.session),
+            ]))
+
         return new
-        
+
     def setupLevelboxUi(self):
         self.levelbox = Levelbox(self, self.graph, self.currentSub)
         self.levelbox.pack(side='top')
 
-        tk.Button(self, text="All to zero",
-             command=lambda *args: self.currentSub().clear()).pack(side='top')
+        tk.Button(
+            self,
+            text="All to zero",
+            command=lambda *args: self.currentSub().clear()).pack(side='top')
 
     def savenewsub(self, subname):
-        leveldict={}
-        for i,lev in zip(range(len(self.levels)),self.levels):
-            if lev!=0:
-                leveldict[get_channel_name(i+1)]=lev
+        leveldict = {}
+        for i, lev in zip(range(len(self.levels)), self.levels):
+            if lev != 0:
+                leveldict[get_channel_name(i + 1)] = lev
 
-        s=Submaster.Submaster(subname, levels=leveldict)
+        s = Submaster.Submaster(subname, levels=leveldict)
         s.save()
 
     def sendupdate(self):
@@ -252,14 +261,15 @@
     if not opts.no_geometry:
         toplevelat("subcomposer - %s" % opts.session, root, graph, session)
 
-        
     sc = Subcomposer(root, graph, session)
     sc.pack()
-    
+
     subcomposerweb.init(graph, session, sc.currentSub)
 
-    tk.Label(root,text="Bindings: B1 adjust level; B2 set full; B3 instant bump",
-             font="Helvetica -12 italic",anchor='w').pack(side='top',fill='x')
+    tk.Label(root,
+             text="Bindings: B1 adjust level; B2 set full; B3 instant bump",
+             font="Helvetica -12 italic",
+             anchor='w').pack(side='top', fill='x')
 
     if len(args) == 1:
         # it might be a little too weird that cmdline arg to this
@@ -269,8 +279,7 @@
         # same window pos), so maybe it doesn't matter. But still,
         # this tool should probably default to making new sessions
         # usually instead of loading the same one
-        graph.patchObject(session,
-                          session, L9['currentSub'], URIRef(args[0]))
+        graph.patchObject(session, session, L9['currentSub'], URIRef(args[0]))
 
     task.LoopingCall(sc.sendupdate).start(10)
 
@@ -279,7 +288,8 @@
 
 if __name__ == "__main__":
     parser = OptionParser(usage="%prog [suburi]")
-    parser.add_option('--no-geometry', action='store_true',
+    parser.add_option('--no-geometry',
+                      action='store_true',
                       help="don't save/restore window geometry")
     parser.add_option('-v', action='store_true', help="log debug level")
 
@@ -287,8 +297,8 @@
     opts, args = parser.parse_args()
 
     log.setLevel(logging.DEBUG if opts.v else logging.INFO)
-    
-    root=tk.Tk()
+
+    root = tk.Tk()
     root.config(bg='black')
     root.tk_setPalette("#004633")
 
@@ -297,8 +307,9 @@
     graph = SyncedGraph(networking.rdfdb.url, "subcomposer")
     session = clientsession.getUri('subcomposer', opts)
 
-    graph.initiallySynced.addCallback(lambda _: launch(opts, args, root, graph, session))
+    graph.initiallySynced.addCallback(lambda _: launch(opts, args, root, graph,
+                                                       session))
 
     root.protocol('WM_DELETE_WINDOW', reactor.stop)
-    tksupport.install(root,ms=10)
+    tksupport.install(root, ms=10)
     prof.run(reactor.run, profile=False)
--- a/bin/subserver	Tue May 21 23:55:35 2019 +0000
+++ b/bin/subserver	Tue May 21 23:56:12 2019 +0000
@@ -16,17 +16,19 @@
 from light9 import networking, showconfig
 
 from lib.cycloneerr import PrettyErrorHandler
-        
+
+
 class Static(PrettyErrorHandler, cyclone.web.StaticFileHandler):
+
     def get(self, path, *args, **kw):
         if path in ['', 'effects']:
             return self.respondStaticJade("light9/subserver/%s.jade" %
                                           (path or 'index'))
-            
+
         if path.endswith(".js"):
             return self.responseStaticCoffee(
                 'light9/subserver/%s' %
-                path.replace(".js", ".coffee")) # potential security hole
+                path.replace(".js", ".coffee"))  # potential security hole
 
         cyclone.web.StaticFileHandler.get(self, path, *args, **kw)
 
@@ -35,14 +37,18 @@
         self.write(html)
 
     def responseStaticCoffee(self, src):
-        self.write(subprocess.check_output([
-            '/usr/bin/coffee', '--compile', '--print', src]))
+        self.write(
+            subprocess.check_output(
+                ['/usr/bin/coffee', '--compile', '--print', src]))
+
 
 class Snapshot(PrettyErrorHandler, cyclone.web.RequestHandler):
+
     @defer.inlineCallbacks
     def post(self):
         about = URIRef(self.get_argument("about"))
-        response = yield cyclone.httpclient.fetch(networking.vidref.path("snapshot"), method="POST", timeout=1)
+        response = yield cyclone.httpclient.fetch(
+            networking.vidref.path("snapshot"), method="POST", timeout=1)
 
         snapUri = URIRef(json.loads(response.body)['snapshot'])
         # vidref could write about when it was taken, etc. would it be
@@ -50,15 +56,17 @@
         # the graph, and then it doesn't even have to return anything?
 
         ctx = showconfig.showUri() + "/snapshots"
-        
-        self.settings.graph.patch(Patch(addQuads=[
-            (about, L9['image'], snapUri, ctx),
-            (snapUri, DCTERMS['created'],
-             Literal(datetime.datetime.now(tzlocal())), ctx),
+
+        self.settings.graph.patch(
+            Patch(addQuads=[
+                (about, L9['image'], snapUri, ctx),
+                (snapUri, DCTERMS['created'],
+                 Literal(datetime.datetime.now(tzlocal())), ctx),
             ]))
-        
+
         self.write(json.dumps({'snapshot': snapUri}))
 
+
 def newestImage(subject):
     newest = (None, None)
     for img in graph.objects(subject, L9['image']):
@@ -66,10 +74,13 @@
         if created > newest[0]:
             newest = (created, img)
     return newest[1]
-        
+
+
 if __name__ == "__main__":
     parser = optparse.OptionParser()
-    parser.add_option("-v", "--verbose", action="store_true",
+    parser.add_option("-v",
+                      "--verbose",
+                      action="store_true",
                       help="logging.DEBUG")
     (options, args) = parser.parse_args()
 
@@ -77,13 +88,17 @@
 
     graph = SyncedGraph(networking.rdfdb.url, "subServer")
 
-    
     port = networking.subServer.port
-    reactor.listenTCP(port, cyclone.web.Application(handlers=[
-        (r'/snapshot', Snapshot),
-        (r'/(.*)', Static,
-         {"path" : "light9/subserver",
-          "default_filename" : "index.jade"}),
-        ], debug=True, graph=graph))
+    reactor.listenTCP(
+        port,
+        cyclone.web.Application(handlers=[
+            (r'/snapshot', Snapshot),
+            (r'/(.*)', Static, {
+                "path": "light9/subserver",
+                "default_filename": "index.jade"
+            }),
+        ],
+                                debug=True,
+                                graph=graph))
     log.info("serving on %s" % port)
     reactor.run()
--- a/bin/tkdnd_minimal_drop.py	Tue May 21 23:55:35 2019 +0000
+++ b/bin/tkdnd_minimal_drop.py	Tue May 21 23:56:12 2019 +0000
@@ -13,8 +13,7 @@
 frame1 = tk.Frame()
 frame1.pack()
 
-labelInner = tk.Label(frame1, borderwidth=2,
-                      relief='groove', padx=10, pady=10)
+labelInner = tk.Label(frame1, borderwidth=2, relief='groove', padx=10, pady=10)
 labelInner.pack(side='left')
 labelInner.config(text="drop target inner %s" % labelInner._w)
 tk.Label(frame1, text="not a target").pack(side='left')
@@ -22,21 +21,36 @@
 
 def onDrop(ev):
     print "onDrop", ev
+
+
 def enter(ev):
     print 'enter', ev
+
+
 def leave(ev):
     print 'leave', ev
-dropTargetRegister(label, onDrop=onDrop, onDropEnter=enter, onDropLeave=leave,
+
+
+dropTargetRegister(label,
+                   onDrop=onDrop,
+                   onDropEnter=enter,
+                   onDropLeave=leave,
                    hoverStyle=dict(background="yellow", relief='groove'))
 
-dropTargetRegister(labelInner, onDrop=onDrop, onDropEnter=enter, onDropLeave=leave,
+dropTargetRegister(labelInner,
+                   onDrop=onDrop,
+                   onDropEnter=enter,
+                   onDropLeave=leave,
                    hoverStyle=dict(background="yellow", relief='groove'))
 
+
 def prn():
-    print "cont", root.winfo_containing(201,151)
+    print "cont", root.winfo_containing(201, 151)
+
+
 b = tk.Button(root, text="coord", command=prn)
 b.pack()
 
 #tk.mainloop()
-tksupport.install(root,ms=10)
+tksupport.install(root, ms=10)
 reactor.run()
--- a/bin/tracker	Tue May 21 23:55:35 2019 +0000
+++ b/bin/tracker	Tue May 21 23:56:12 2019 +0000
@@ -1,272 +1,311 @@
 #!/usr/bin/python
-from __future__ import division,nested_scopes
+from __future__ import division, nested_scopes
 
 import sys
 sys.path.append("../../editor/pour")
 sys.path.append("../light8")
 
 from Submaster import Submaster
-from skim.zooming import Zooming,Pair
-from math import sqrt,sin,cos
+from skim.zooming import Zooming, Pair
+from math import sqrt, sin, cos
 from pygame.rect import Rect
-from xmlnodebase import xmlnodeclass,collectiveelement,xmldocfile
+from xmlnodebase import xmlnodeclass, collectiveelement, xmldocfile
 from dispatch import dispatcher
 
 import dmxclient
 
 import Tkinter as tk
 
-defaultfont="arial 8"
+defaultfont = "arial 8"
 
-def pairdist(pair1,pair2):
+
+def pairdist(pair1, pair2):
     return pair1.dist(pair2)
 
-def canvashighlighter(canvas,obj,attribute,normalval,highlightval):
+
+def canvashighlighter(canvas, obj, attribute, normalval, highlightval):
     """creates bindings on a canvas obj that make attribute go
     from normal to highlight when the mouse is over the obj"""
-    canvas.tag_bind(obj,"<Enter>",
-                    lambda ev: canvas.itemconfig(obj,**{attribute:highlightval}))
-    canvas.tag_bind(obj,"<Leave>",
-                    lambda ev: canvas.itemconfig(obj,**{attribute:normalval}))
+    canvas.tag_bind(
+        obj, "<Enter>", lambda ev: canvas.itemconfig(
+            obj, **{attribute: highlightval}))
+    canvas.tag_bind(
+        obj,
+        "<Leave>", lambda ev: canvas.itemconfig(obj, **{attribute: normalval}))
+
 
 class Field(xmlnodeclass):
-    
     """one light has a field of influence. for any point on the
     canvas, you can ask this field how strong it is. """
 
-    def name(self,newval=None):
+    def name(self, newval=None):
         """light/sub name"""
-        return self._getorsetattr("name",newval)
-    def center(self,x=None,y=None):
+        return self._getorsetattr("name", newval)
+
+    def center(self, x=None, y=None):
         """x,y float coords for the center of this light in the field. returns
         a Pair, although it accepts x,y"""
-        return Pair(self._getorsettypedattr("x",float,x),
-                    self._getorsettypedattr("y",float,y))
+        return Pair(self._getorsettypedattr("x", float, x),
+                    self._getorsettypedattr("y", float, y))
 
-    def falloff(self,dist=None):
-        
+    def falloff(self, dist=None):
         """linear falloff from 1 at center, to 0 at dist pixels away
         from center"""
-        return self._getorsettypedattr("falloff",float,dist)
+        return self._getorsettypedattr("falloff", float, dist)
 
-    def getdistforintensity(self,intens):
+    def getdistforintensity(self, intens):
         """returns the distance you'd have to be for the given intensity (0..1)"""
-        return (1-intens)*self.falloff()
+        return (1 - intens) * self.falloff()
 
-    def calc(self,x,y):
+    def calc(self, x, y):
         """returns field strength at point x,y"""
-        dist=pairdist(Pair(x,y),self.center())
-        return max(0,(self.falloff()-dist)/self.falloff())
+        dist = pairdist(Pair(x, y), self.center())
+        return max(0, (self.falloff() - dist) / self.falloff())
+
 
 class Fieldset(collectiveelement):
     """group of fields. persistent."""
-    def childtype(self): return Field
+
+    def childtype(self):
+        return Field
 
     def version(self):
         """read-only version attribute on fieldset tag"""
-        return self._getorsetattr("version",None)
+        return self._getorsetattr("version", None)
 
-    def report(self,x,y):
+    def report(self, x, y):
         """reports active fields and their intensities"""
-        active=0
+        active = 0
         for f in self.getall():
-            name=f.name()
-            intens=f.calc(x,y)
-            if intens>0:
-                print name,intens,
-                active+=1
-        if active>0:
+            name = f.name()
+            intens = f.calc(x, y)
+            if intens > 0:
+                print name, intens,
+                active += 1
+        if active > 0:
             print
-        self.dmxsend(x,y)
-    def dmxsend(self,x,y):
+        self.dmxsend(x, y)
+
+    def dmxsend(self, x, y):
         """output lights to dmx"""
-        levels=dict([(f.name(),f.calc(x,y)) for f in self.getall()])
-        dmxlist=Submaster(None,levels).get_dmx_list()
+        levels = dict([(f.name(), f.calc(x, y)) for f in self.getall()])
+        dmxlist = Submaster(None, levels).get_dmx_list()
         dmxclient.outputlevels(dmxlist)
-        
-        
+
     def getbounds(self):
         """returns xmin,xmax,ymin,ymax for the non-zero areas of this field"""
-        r=None
+        r = None
         for f in self.getall():
-            rad=f.getdistforintensity(0)
-            fx,fy=f.center()
-            fieldrect=Rect(fx-rad,fy-rad,rad*2,rad*2)
+            rad = f.getdistforintensity(0)
+            fx, fy = f.center()
+            fieldrect = Rect(fx - rad, fy - rad, rad * 2, rad * 2)
             if r is None:
-                r=fieldrect
+                r = fieldrect
             else:
-                r=r.union(fieldrect)
-        return r.left,r.right,r.top,r.bottom
+                r = r.union(fieldrect)
+        return r.left, r.right, r.top, r.bottom
+
 
 class Fieldsetfile(xmldocfile):
-    def __init__(self,filename):
-        self._openornew(filename,topleveltype=Fieldset)
+
+    def __init__(self, filename):
+        self._openornew(filename, topleveltype=Fieldset)
+
     def fieldset(self):
         return self._gettoplevel()
 
+
 ########################################################################
 ########################################################################
 
+
 class FieldDisplay:
     """the view for a Field."""
-    def __init__(self,canvas,field):
-        self.canvas=canvas
-        self.field=field
-        self.tags=[str(id(self))] # canvas tag to id our objects
-        
+
+    def __init__(self, canvas, field):
+        self.canvas = canvas
+        self.field = field
+        self.tags = [str(id(self))]  # canvas tag to id our objects
+
     def setcoords(self):
         """adjust canvas obj coords to match the field"""
         # this uses the canvas object ids saved by makeobjs
-        f=self.field
-        c=self.canvas
-        w2c=self.canvas.world2canvas
+        f = self.field
+        c = self.canvas
+        w2c = self.canvas.world2canvas
 
         # rings
-        for intens,ring in self.rings.items():
-            rad=f.getdistforintensity(intens)
-            p1=w2c(*(f.center()-Pair(rad,rad)))
-            p2=w2c(*(f.center()+Pair(rad,rad)))
-            c.coords(ring,p1[0],p1[1],p2[0],p2[1])
+        for intens, ring in self.rings.items():
+            rad = f.getdistforintensity(intens)
+            p1 = w2c(*(f.center() - Pair(rad, rad)))
+            p2 = w2c(*(f.center() + Pair(rad, rad)))
+            c.coords(ring, p1[0], p1[1], p2[0], p2[1])
 
         # text
-        p1=w2c(*f.center())
-        c.coords(self.txt,*p1)
+        p1 = w2c(*f.center())
+        c.coords(self.txt, *p1)
 
     def makeobjs(self):
         """(re)create the canvas objs (null coords) and make their bindings"""
-        c=self.canvas
-        f=self.field
+        c = self.canvas
+        f = self.field
         c.delete(self.tags)
 
-        w2c=self.canvas.world2canvas
+        w2c = self.canvas.world2canvas
 
         # make rings
-        self.rings={} # rad,canvasobj
-        for intens,color in (#(1,'white'),
-                             (.8,'gray90'),(.6,'gray80'),(.4,'gray60'),(.2,'gray50'),
-                             (0,'#000080')):
-            self.rings[intens]=c.create_oval(0,0,0,0,
-                                          outline=color,width=2,tags=self.tags,
-                                          outlinestipple='gray50')
+        self.rings = {}  # rad,canvasobj
+        for intens, color in (  #(1,'white'),
+            (.8, 'gray90'), (.6, 'gray80'), (.4, 'gray60'), (.2, 'gray50'),
+            (0, '#000080')):
+            self.rings[intens] = c.create_oval(0,
+                                               0,
+                                               0,
+                                               0,
+                                               outline=color,
+                                               width=2,
+                                               tags=self.tags,
+                                               outlinestipple='gray50')
 
         # make text
-        self.txt=c.create_text(0,0,text=f.name(),font=defaultfont+" bold",
-                               fill='white',anchor='c',
-                               tags=self.tags)
+        self.txt = c.create_text(0,
+                                 0,
+                                 text=f.name(),
+                                 font=defaultfont + " bold",
+                                 fill='white',
+                                 anchor='c',
+                                 tags=self.tags)
 
         # highlight text bindings
-        canvashighlighter(c,self.txt,'fill',normalval='white',highlightval='red')
+        canvashighlighter(c,
+                          self.txt,
+                          'fill',
+                          normalval='white',
+                          highlightval='red')
 
         # position drag bindings
         def press(ev):
-            self._lastmouse=ev.x,ev.y
+            self._lastmouse = ev.x, ev.y
+
         def motion(ev):
-            dcan=Pair(*[a-b for a,b in zip((ev.x,ev.y),self._lastmouse)])
-            dworld=c.canvas2world_vector(*dcan)
-            self.field.center(*(self.field.center()+dworld))
-            self._lastmouse=ev.x,ev.y
-            self.setcoords() # redraw
+            dcan = Pair(*[a - b for a, b in zip((ev.x, ev.y), self._lastmouse)])
+            dworld = c.canvas2world_vector(*dcan)
+            self.field.center(*(self.field.center() + dworld))
+            self._lastmouse = ev.x, ev.y
+            self.setcoords()  # redraw
+
         def release(ev):
-            if hasattr(self,'_lastmouse'):
+            if hasattr(self, '_lastmouse'):
                 del self._lastmouse
-            dispatcher.send("field coord changed") # updates bounds
-            
-        c.tag_bind(self.txt,"<ButtonPress-1>",press)
-        c.tag_bind(self.txt,"<B1-Motion>",motion)
-        c.tag_bind(self.txt,"<B1-ButtonRelease>",release)
+            dispatcher.send("field coord changed")  # updates bounds
+
+        c.tag_bind(self.txt, "<ButtonPress-1>", press)
+        c.tag_bind(self.txt, "<B1-Motion>", motion)
+        c.tag_bind(self.txt, "<B1-ButtonRelease>", release)
 
         # radius drag bindings
-        outerring=self.rings[0]
-        canvashighlighter(c,outerring,
-                          'outline',normalval='#000080',highlightval='#4040ff')
+        outerring = self.rings[0]
+        canvashighlighter(c,
+                          outerring,
+                          'outline',
+                          normalval='#000080',
+                          highlightval='#4040ff')
+
         def motion(ev):
-            worldmouse=self.canvas.canvas2world(ev.x,ev.y)
-            currentdist=pairdist(worldmouse,self.field.center())
+            worldmouse = self.canvas.canvas2world(ev.x, ev.y)
+            currentdist = pairdist(worldmouse, self.field.center())
             self.field.falloff(currentdist)
             self.setcoords()
-        c.tag_bind(outerring,"<B1-Motion>",motion)
-        c.tag_bind(outerring,"<B1-ButtonRelease>",release) # from above
+
+        c.tag_bind(outerring, "<B1-Motion>", motion)
+        c.tag_bind(outerring, "<B1-ButtonRelease>", release)  # from above
 
         self.setcoords()
-    
+
+
 class Tracker(tk.Frame):
-
     """whole tracker widget, which is mostly a view for a
     Fieldset. tracker makes its own fieldset"""
 
     # world coords of the visible canvas (preserved even in window resizes)
-    xmin=0
-    xmax=100
-    ymin=0
-    ymax=100
+    xmin = 0
+    xmax = 100
+    ymin = 0
+    ymax = 100
+
+    fieldsetfile = None
+    displays = None  # Field : FieldDisplay. we keep these in sync with the fieldset
 
-    fieldsetfile=None
-    displays=None # Field : FieldDisplay. we keep these in sync with the fieldset
-    
-    def __init__(self,master):
-        tk.Frame.__init__(self,master)
+    def __init__(self, master):
+        tk.Frame.__init__(self, master)
 
-        self.displays={}
-        
-        c=self.canvas=Zooming(self,bg='black',closeenough=5)
-        c.pack(fill='both',exp=1)
+        self.displays = {}
+
+        c = self.canvas = Zooming(self, bg='black', closeenough=5)
+        c.pack(fill='both', exp=1)
 
         # preserve edge coords over window resize
-        c.bind("<Configure>",self.configcoords)
-        
-        c.bind("<Motion>",
-               lambda ev: self._fieldset().report(*c.canvas2world(ev.x,ev.y)))
+        c.bind("<Configure>", self.configcoords)
+
+        c.bind("<Motion>", lambda ev: self._fieldset().report(*c.canvas2world(
+            ev.x, ev.y)))
+
         def save(ev):
             print "saving"
             self.fieldsetfile.save()
-        master.bind("<Key-s>",save)
-        dispatcher.connect(self.autobounds,"field coord changed")
+
+        master.bind("<Key-s>", save)
+        dispatcher.connect(self.autobounds, "field coord changed")
 
     def _fieldset(self):
         return self.fieldsetfile.fieldset()
 
-    def load(self,filename):
-        self.fieldsetfile=Fieldsetfile(filename)
+    def load(self, filename):
+        self.fieldsetfile = Fieldsetfile(filename)
         self.displays.clear()
         for f in self.fieldsetfile.fieldset().getall():
-            self.displays[f]=FieldDisplay(self.canvas,f)
+            self.displays[f] = FieldDisplay(self.canvas, f)
             self.displays[f].makeobjs()
         self.autobounds()
 
-    def configcoords(self,*args):
+    def configcoords(self, *args):
         # force our canvas coords to stay at the edges of the window
-        c=self.canvas
-        cornerx,cornery=c.canvas2world(0,0)
-        c.move(cornerx-self.xmin,
-               cornery-self.ymin)
-        c.setscale(0,0,
-                   c.winfo_width()/(self.xmax-self.xmin),
-                   c.winfo_height()/(self.ymax-self.ymin))
+        c = self.canvas
+        cornerx, cornery = c.canvas2world(0, 0)
+        c.move(cornerx - self.xmin, cornery - self.ymin)
+        c.setscale(0, 0,
+                   c.winfo_width() / (self.xmax - self.xmin),
+                   c.winfo_height() / (self.ymax - self.ymin))
 
     def autobounds(self):
         """figure out our bounds from the fieldset, and adjust the display zooms.
         writes the corner coords onto the canvas."""
-        self.xmin,self.xmax,self.ymin,self.ymax=self._fieldset().getbounds()
+        self.xmin, self.xmax, self.ymin, self.ymax = self._fieldset().getbounds(
+        )
 
         self.configcoords()
-        
-        c=self.canvas
+
+        c = self.canvas
         c.delete('cornercoords')
-        for x,anc2 in ((self.xmin,'w'),(self.xmax,'e')):
-            for y,anc1 in ((self.ymin,'n'),(self.ymax,'s')):
-                pos=c.world2canvas(x,y)
-                c.create_text(pos[0],pos[1],text="%s,%s"%(x,y),
-                              fill='white',anchor=anc1+anc2,
+        for x, anc2 in ((self.xmin, 'w'), (self.xmax, 'e')):
+            for y, anc1 in ((self.ymin, 'n'), (self.ymax, 's')):
+                pos = c.world2canvas(x, y)
+                c.create_text(pos[0],
+                              pos[1],
+                              text="%s,%s" % (x, y),
+                              fill='white',
+                              anchor=anc1 + anc2,
                               tags='cornercoords')
         [d.setcoords() for d in self.displays.values()]
 
+
 ########################################################################
 ########################################################################
-                
-root=tk.Tk()
+
+root = tk.Tk()
 root.wm_geometry('700x350')
-tra=Tracker(root)
-tra.pack(fill='both',exp=1)
+tra = Tracker(root)
+tra.pack(fill='both', exp=1)
 
 tra.load("fieldsets/demo")
 
--- a/bin/vidref	Tue May 21 23:55:35 2019 +0000
+++ b/bin/vidref	Tue May 21 23:56:12 2019 +0000
@@ -1,7 +1,7 @@
 #!bin/python
 from run_local import log
 import sys
-sys.path.append('/usr/lib/python2.7/dist-packages') # For gtk
+sys.path.append('/usr/lib/python2.7/dist-packages')  # For gtk
 from twisted.internet import gtk2reactor
 gtk2reactor.install()
 from twisted.internet import reactor, defer
@@ -15,19 +15,19 @@
 from light9.vidref.replay import snapshotDir
 from rdfdb.syncedgraph import SyncedGraph
 
- # find replay dirs correctly. show multiple
- # replays. trash. reorder/pin. dump takes that are too short; they're
- # just from seeking
+# find replay dirs correctly. show multiple
+# replays. trash. reorder/pin. dump takes that are too short; they're
+# just from seeking
 
 parser = optparse.OptionParser()
-parser.add_option("-v", "--verbose", action="store_true",
-                  help="logging.DEBUG")
+parser.add_option("-v", "--verbose", action="store_true", help="logging.DEBUG")
 (options, args) = parser.parse_args()
 
-
 log.setLevel(logging.DEBUG if options.verbose else logging.INFO)
 
+
 class Snapshot(cyclone.web.RequestHandler):
+
     @defer.inlineCallbacks
     def post(self):
         # save next pic
@@ -38,7 +38,7 @@
             assert outputFilename.startswith(snapshotDir())
             out = networking.vidref.path(
                 "snapshot/%s" % outputFilename[len(snapshotDir()):].lstrip('/'))
-            
+
             self.write(json.dumps({'snapshot': out}))
             self.set_header("Location", out)
             self.set_status(303)
@@ -47,11 +47,13 @@
             traceback.print_exc()
             raise
 
+
 class SnapshotPic(cyclone.web.StaticFileHandler):
     pass
 
 
 class Time(cyclone.web.RequestHandler):
+
     def put(self):
         body = json.loads(self.request.body)
         t = body['t']
@@ -65,14 +67,21 @@
 gui = Gui(graph)
 
 port = networking.vidref.port
-reactor.listenTCP(port, cyclone.web.Application(handlers=[
-    (r'/()', cyclone.web.StaticFileHandler,
-     {'path': 'light9/vidref', 'default_filename': 'vidref.html'}),
-    (r'/snapshot', Snapshot),
-    (r'/snapshot/(.*)', SnapshotPic, {"path": snapshotDir()}),
-    (r'/time', Time),
-    ], debug=True, gui=gui))
+reactor.listenTCP(
+    port,
+    cyclone.web.Application(handlers=[
+        (r'/()', cyclone.web.StaticFileHandler, {
+            'path': 'light9/vidref',
+            'default_filename': 'vidref.html'
+        }),
+        (r'/snapshot', Snapshot),
+        (r'/snapshot/(.*)', SnapshotPic, {
+            "path": snapshotDir()
+        }),
+        (r'/time', Time),
+    ],
+                            debug=True,
+                            gui=gui))
 log.info("serving on %s" % port)
 
 reactor.run()
-
--- a/bin/vidrefsetup	Tue May 21 23:55:35 2019 +0000
+++ b/bin/vidrefsetup	Tue May 21 23:56:12 2019 +0000
@@ -16,17 +16,23 @@
 
 from lib.cycloneerr import PrettyErrorHandler
 
+
 class RedirToCamera(PrettyErrorHandler, cyclone.web.RequestHandler):
+
     def get(self):
-        return self.redirect(networking.picamserve.path(
-            'pic?' + self.request.query))
-        
+        return self.redirect(
+            networking.picamserve.path('pic?' + self.request.query))
+
+
 class UrlToCamera(PrettyErrorHandler, cyclone.web.RequestHandler):
+
     def get(self):
         self.set_header('Content-Type', 'text/plain')
         self.write(networking.picamserve.path('pic'))
-                   
+
+
 class VidrefCamRequest(PrettyErrorHandler, cyclone.web.RequestHandler):
+
     def get(self):
         graph = self.settings.graph
         show = showconfig.showUri()
@@ -35,7 +41,7 @@
             if ret is None:
                 self.send_error(404)
             self.redirect(ret)
-            
+
     def put(self):
         graph = self.settings.graph
         show = showconfig.showUri()
@@ -45,9 +51,12 @@
                           newObject=URIRef(self.get_argument('uri')))
         self.send_error(202)
 
+
 def main():
     parser = optparse.OptionParser()
-    parser.add_option("-v", "--verbose", action="store_true",
+    parser.add_option("-v",
+                      "--verbose",
+                      action="store_true",
                       help="logging.DEBUG")
     (options, args) = parser.parse_args()
 
@@ -55,15 +64,23 @@
     graph = SyncedGraph(networking.rdfdb.url, "vidrefsetup")
 
     # deliberately conflict with vidref since they can't talk at once to cam
-    port = networking.vidref.port 
+    port = networking.vidref.port
 
-    reactor.listenTCP(port, cyclone.web.Application(handlers=[
-        (r'/pic', RedirToCamera),
-        (r'/picUrl', UrlToCamera),
-        (r'/vidrefCamRequest', VidrefCamRequest),
-        (r'/()', cyclone.web.StaticFileHandler, {'path': 'light9/vidref/', 'default_filename': 'vidref.html'}),
-        ], debug=True, graph=graph))
+    reactor.listenTCP(
+        port,
+        cyclone.web.Application(handlers=[
+            (r'/pic', RedirToCamera),
+            (r'/picUrl', UrlToCamera),
+            (r'/vidrefCamRequest', VidrefCamRequest),
+            (r'/()', cyclone.web.StaticFileHandler, {
+                'path': 'light9/vidref/',
+                'default_filename': 'vidref.html'
+            }),
+        ],
+                                debug=True,
+                                graph=graph))
     log.info("serving on %s" % port)
     reactor.run()
 
+
 main()
--- a/bin/wavecurve	Tue May 21 23:55:35 2019 +0000
+++ b/bin/wavecurve	Tue May 21 23:56:12 2019 +0000
@@ -3,24 +3,30 @@
 import run_local
 from light9.wavepoints import simp
 
+
 def createCurve(inpath, outpath, t):
     print "reading %s, writing %s" % (inpath, outpath)
     points = simp(inpath.replace('.ogg', '.wav'), seconds_per_average=t)
 
     f = file(outpath, 'w')
     for time_val in points:
-        print >>f, "%s %s" % time_val
+        print >> f, "%s %s" % time_val
+
 
 parser = optparse.OptionParser(usage="""%prog inputSong.wav outputCurve
 
 You probably just want -a
 
 """)
-parser.add_option("-t", type="float", default=.01,
+parser.add_option("-t",
+                  type="float",
+                  default=.01,
                   help="seconds per sample (default .01, .07 is smooth)")
-parser.add_option("-a", "--all", action="store_true",
+parser.add_option("-a",
+                  "--all",
+                  action="store_true",
                   help="make standard curves for all songs")
-options,args = parser.parse_args()
+options, args = parser.parse_args()
 
 if options.all:
     from light9 import showconfig
@@ -32,8 +38,7 @@
     playlist = Playlist.fromShow(showconfig.getGraph(), showconfig.showUri())
     for song in playlist.allSongs():
         inpath = showconfig.songOnDisk(song)
-        for curveName, t in [('music', .01),
-                             ('smooth_music', .07)]:
+        for curveName, t in [('music', .01), ('smooth_music', .07)]:
             outpath = showconfig.curvesDir() + "/%s-%s" % (
                 showconfig.songFilenameFromURI(song), curveName)
             createCurve(inpath, outpath, t)
--- a/bin/webcontrol	Tue May 21 23:55:35 2019 +0000
+++ b/bin/webcontrol	Tue May 21 23:56:12 2019 +0000
@@ -21,14 +21,14 @@
 from light9.namespaces import L9
 from urllib import urlencode
 
+
 # move to web lib
 def post(url, **args):
-    return getPage(url,
-                   method='POST',
-                   postdata=urlencode(args))
+    return getPage(url, method='POST', postdata=urlencode(args))
 
 
 class Commands(object):
+
     @staticmethod
     def playSong(graph, songUri):
         s = xmlrpclib.ServerProxy(networking.musicPlayer.url)
@@ -45,17 +45,22 @@
     @staticmethod
     def worklightsOn(graph):
         return post(networking.keyboardComposer.path('fadesub'),
-                    subname='scoop', level=.5, secs=.5)
+                    subname='scoop',
+                    level=.5,
+                    secs=.5)
 
     @staticmethod
     def worklightsOff(graph):
         return post(networking.keyboardComposer.path('fadesub'),
-                    subname='scoop', level=0, secs=.5)
+                    subname='scoop',
+                    level=0,
+                    secs=.5)
 
     @staticmethod
     def dimmerSet(graph, dimmer, value):
         raise NotImplementedError("subcomposer doesnt have an http port yet")
 
+
 class Main(rend.Page):
     docFactory = loaders.xmlfile(sibpath(__file__, "../light9/webcontrol.html"))
 
@@ -75,9 +80,9 @@
         out = []
         for song in songs:
             out.append(
-                T.form(method="post", action="playSong")[
-                    T.input(type='hidden', name='songUri', value=song),
-                    T.button(type='submit')[graph.label(song)]])
+                T.form(method="post", action="playSong")
+                [T.input(type='hidden', name='songUri', value=song),
+                 T.button(type='submit')[graph.label(song)]])
         return out
 
     @inlineCallbacks
@@ -85,18 +90,19 @@
         try:
             func = getattr(Commands, segments[0])
             req = inevow.IRequest(ctx)
-            simpleArgDict = dict((k, v[0]) for k,v in req.args.items())
+            simpleArgDict = dict((k, v[0]) for k, v in req.args.items())
             try:
                 ret = yield robust_apply(func, func, self.graph,
                                          **simpleArgDict)
-            except KeyboardInterrupt: raise
+            except KeyboardInterrupt:
+                raise
             except Exception, e:
                 print "Error on command %s" % segments[0]
                 traceback.print_exc()
-                returnValue((url.here.up().
-                             add('status', str(e)).
-                             add('error', 1), segments[1:]))
-                
+                returnValue((url.here.up().add('status',
+                                               str(e)).add('error',
+                                                           1), segments[1:]))
+
             returnValue((url.here.up().add('status', ret), segments[1:]))
             #actually return the orig page, with a status message from the func
         except AttributeError:
@@ -105,7 +111,8 @@
 
     def child_icon(self, ctx):
         return static.File("/usr/share/pyshared/elisa/plugins/poblesec/tango")
-            
+
+
 graph = showconfig.getGraph()
 show = showconfig.showUri()
 
--- a/light9/Effects.py	Tue May 21 23:55:35 2019 +0000
+++ b/light9/Effects.py	Tue May 21 23:56:12 2019 +0000
@@ -11,21 +11,24 @@
 log = logging.getLogger()
 
 registered = []
+
+
 def register(f):
     registered.append(f)
     return f
 
+
 @register
 class Strip(object):
     """list of r,g,b tuples for sending to an LED strip"""
-    which = 'L' # LR means both. W is the wide one
+    which = 'L'  # LR means both. W is the wide one
     pixels = []
 
     def __repr__(self):
         return '<Strip which=%r px0=%r>' % (self.which, self.pixels[0])
-    
+
     @classmethod
-    def solid(cls, which='L', color=(1,1,1), hsv=None):
+    def solid(cls, which='L', color=(1, 1, 1), hsv=None):
         """hsv overrides color"""
         if hsv is not None:
             color = colorsys.hsv_to_rgb(hsv[0] % 1.0, hsv[1], hsv[2])
@@ -37,24 +40,34 @@
     def __mul__(self, f):
         if not isinstance(f, (int, float)):
             raise TypeError
-            
+
         s = Strip()
         s.which = self.which
-        s.pixels = [(r*f, g*f, b*f) for r,g,b in self.pixels]
+        s.pixels = [(r * f, g * f, b * f) for r, g, b in self.pixels]
         return s
 
     __rmul__ = __mul__
 
+
 @register
 class Blacklight(float):
     """a level for the blacklight PWM output"""
+
     def __mul__(self, f):
         return Blacklight(float(self) * f)
+
     __rmul__ = __mul__
-    
+
+
 @register
-def chase(t, ontime=0.5, offset=0.2, onval=1.0, 
-          offval=0.0, names=None, combiner=max, random=False):
+def chase(t,
+          ontime=0.5,
+          offset=0.2,
+          onval=1.0,
+          offval=0.0,
+          names=None,
+          combiner=max,
+          random=False):
     """names is list of URIs. returns a submaster that chases through
     the inputs"""
     if random:
@@ -69,24 +82,27 @@
             dmx = Patch.dmx_from_uri(uri)
         except KeyError:
             log.info(("chase includes %r, which doesn't resolve to a dmx chan" %
-                   uri))
+                      uri))
             continue
         lev[dmx] = value
 
-    return Submaster.Submaster(name="chase" ,levels=lev)
+    return Submaster.Submaster(name="chase", levels=lev)
+
 
 @register
 def hsv(h, s, v, light='all', centerScale=.5):
-    r,g,b = colorsys.hsv_to_rgb(h % 1.0, s, v)
+    r, g, b = colorsys.hsv_to_rgb(h % 1.0, s, v)
     lev = {}
     if light in ['left', 'all']:
-        lev[73], lev[74], lev[75] = r,g,b
+        lev[73], lev[74], lev[75] = r, g, b
     if light in ['right', 'all']:
-        lev[80], lev[81], lev[82] = r,g,b
+        lev[80], lev[81], lev[82] = r, g, b
     if light in ['center', 'all']:
-        lev[88], lev[89], lev[90] = r*centerScale,g*centerScale,b*centerScale
+        lev[88], lev[89], lev[
+            90] = r * centerScale, g * centerScale, b * centerScale
     return Submaster.Submaster(name='hsv', levels=lev)
-    
+
+
 @register
 def stack(t, names=None, fade=0):
     """names is list of URIs. returns a submaster that stacks the the inputs
@@ -102,19 +118,22 @@
             try:
                 dmx = Patch.dmx_from_uri(uri)
             except KeyError:
-                log.info(("stack includes %r, which doesn't resolve to a dmx chan"%
-                       uri))
+                log.info(
+                    ("stack includes %r, which doesn't resolve to a dmx chan" %
+                     uri))
                 continue
             lev[dmx] = 1
         else:
             break
-    
+
     return Submaster.Submaster(name="stack", levels=lev)
 
+
 @register
 def smoove(x):
-    return -2 * (x ** 3) + 3 * (x ** 2)
-    
+    return -2 * (x**3) + 3 * (x**2)
+
+
 def configExprGlobals():
     graph = showconfig.getGraph()
     ret = {}
@@ -130,8 +149,10 @@
 
     ret['nsin'] = lambda x: (math.sin(x * (2 * math.pi)) + 1) / 2
     ret['ncos'] = lambda x: (math.cos(x * (2 * math.pi)) + 1) / 2
+
     def nsquare(t, on=.5):
         return (t % 1.0) < on
+
     ret['nsquare'] = nsquare
 
     _smooth_random_items = [random_mod.random() for x in range(100)]
@@ -156,7 +177,4 @@
     ret['noise2'] = smooth_random2
     ret['notch2'] = notch_random2
 
-
-
-    
     return ret
--- a/light9/Fadable.py	Tue May 21 23:55:35 2019 +0000
+++ b/light9/Fadable.py	Tue May 21 23:56:12 2019 +0000
@@ -2,6 +2,7 @@
 from Tix import *
 import time
 
+
 class Fadable:
     """Fading mixin: must mix in with a Tk widget (or something that has
     'after' at least) This is currently used by VolumeBox and MixerTk.
@@ -21,24 +22,28 @@
     raise or lower the volume.  Shift-mouse wheeling will cause a more
     precise volume adjustment.  Control-mouse wheeling will cause a
     longer fade."""
-    def __init__(self, var, wheel_step=5, use_fades=1, key_bindings=1,
+
+    def __init__(self,
+                 var,
+                 wheel_step=5,
+                 use_fades=1,
+                 key_bindings=1,
                  mouse_bindings=1):
-        self.use_fades = use_fades # whether increase and decrease should fade
-        self.wheel_step = wheel_step # amount that increase and descrease should
-                                     # change volume (by default)
-        
+        self.use_fades = use_fades  # whether increase and decrease should fade
+        self.wheel_step = wheel_step  # amount that increase and descrease should
+        # change volume (by default)
+
         self.fade_start_level = 0
         self.fade_end_level = 0
         self.fade_start_time = 0
         self.fade_length = 1
         self.fade_step_time = 10
         self.fade_var = var
-        self.fading = 0 # whether a fade is in progress
+        self.fading = 0  # whether a fade is in progress
 
         if key_bindings:
             for k in range(1, 10):
-                self.bind("<Key-%d>" % k,
-                    lambda evt, k=k: self.fade(k / 10.0))
+                self.bind("<Key-%d>" % k, lambda evt, k=k: self.fade(k / 10.0))
             self.bind("<Key-0>", lambda evt: self.fade(1.0))
             self.bind("<grave>", lambda evt: self.fade(0))
 
@@ -61,7 +66,7 @@
             self.bind('<Control-4>', lambda evt: self.increase(length=1))
             self.bind('<Control-5>', lambda evt: self.decrease(length=1))
 
-        self.last_level = None # used for muting
+        self.last_level = None  # used for muting
 
     def set_var_rounded(self, value):
         """use this instead of just self.fade_var.set(value) so we can
@@ -72,26 +77,27 @@
         # variable's display instead of using Label(textvariable=var)
         # and format it there.
         self.fade_var.set(round(value, 7))
-        
+
     def fade(self, value, length=0.5, step_time=10):
         """Fade to value in length seconds with steps every step_time
         milliseconds"""
-        if length == 0: # 0 seconds fades happen right away and prevents
-                        # and prevents us from entering the fade loop,
-                        # which would cause a divide by zero
+        if length == 0:  # 0 seconds fades happen right away and prevents
+            # and prevents us from entering the fade loop,
+            # which would cause a divide by zero
             self.set_var_rounded(value)
-            self.fading = 0 # we stop all fades
-        else: # the general case
+            self.fading = 0  # we stop all fades
+        else:  # the general case
             self.fade_start_time = time.time()
             self.fade_length = length
 
             self.fade_start_level = self.fade_var.get()
             self.fade_end_level = value
-            
+
             self.fade_step_time = step_time
             if not self.fading:
                 self.fading = 1
                 self.do_fade()
+
     def do_fade(self):
         """Actually performs the fade for Fadable.fade.  Shouldn't be called
         directly."""
@@ -106,6 +112,7 @@
             self.after(self.fade_step_time, self.do_fade)
         else:
             self.fading = 0
+
     def increase(self, multiplier=1, length=0.3):
         """Increases the volume by multiplier * wheel_step.  If use_fades is
         true, it do this as a fade over length time."""
@@ -116,6 +123,7 @@
             newlevel = self.fade_var.get() + amount
         newlevel = min(100, newlevel)
         self.set_volume(newlevel, length)
+
     def decrease(self, multiplier=1, length=0.3):
         """Descreases the volume by multiplier * wheel_step.  If use_fades
         is true, it do this as a fade over length time."""
@@ -126,6 +134,7 @@
             newlevel = self.fade_var.get() - amount
         newlevel = max(0, newlevel)
         self.set_volume(newlevel, length)
+
     def set_volume(self, newlevel, length=0.3):
         """Sets the volume to newlevel, performing a fade of length if
         use_fades is true."""
@@ -133,13 +142,14 @@
             self.fade(newlevel, length=length)
         else:
             self.set_var_rounded(newlevel)
+
     def toggle_mute(self):
         """Toggles whether the volume is being muted."""
         if self.last_level is None:
             self.last_level = self.fade_var.get()
-            if self.last_level == 0: # we don't want last_level to be zero,
-                                     # since it will make us toggle between 0
-                                     # and 0
+            if self.last_level == 0:  # we don't want last_level to be zero,
+                # since it will make us toggle between 0
+                # and 0
                 newlevel = 1
             else:
                 newlevel = 0
@@ -148,4 +158,3 @@
             self.last_level = None
 
         self.set_var_rounded(newlevel)
-
--- a/light9/FlyingFader.py	Tue May 21 23:55:35 2019 +0000
+++ b/light9/FlyingFader.py	Tue May 21 23:56:12 2019 +0000
@@ -1,86 +1,113 @@
 from Tix import *
-from time import time,sleep
+from time import time, sleep
 from __future__ import division
 
+
 class Mass:
+
     def __init__(self):
-        self.x=0 # position
-        self.xgoal=0 # position goal
-        
-        self.v=0 # velocity
-        self.maxspeed = .8 # maximum speed, in position/second
-        self.maxaccel = 3 # maximum acceleration, in position/second^2
-        self.eps = .03 # epsilon - numbers within this much are considered the same
+        self.x = 0  # position
+        self.xgoal = 0  # position goal
 
-        self._lastupdate=time()
-        self._stopped=1
+        self.v = 0  # velocity
+        self.maxspeed = .8  # maximum speed, in position/second
+        self.maxaccel = 3  # maximum acceleration, in position/second^2
+        self.eps = .03  # epsilon - numbers within this much are considered the same
 
-    def equal(self,a,b):
-        return abs(a-b)<self.eps
+        self._lastupdate = time()
+        self._stopped = 1
+
+    def equal(self, a, b):
+        return abs(a - b) < self.eps
 
     def stop(self):
-        self.v=0
-        self.xgoal=self.x
-        self._stopped=1
-        
+        self.v = 0
+        self.xgoal = self.x
+        self._stopped = 1
+
     def update(self):
         t0 = self._lastupdate
         tnow = time()
         self._lastupdate = tnow
 
-        dt = tnow-t0
+        dt = tnow - t0
 
-        self.x += self.v*dt
+        self.x += self.v * dt
         # hitting the ends stops the slider
-        if self.x>1: self.v=max(self.v,0); self.x=1
-        if self.x<0: self.v=min(self.v,0); self.x=0
-            
-        if self.equal(self.x,self.xgoal):
-            self.x=self.xgoal # clean up value
+        if self.x > 1:
+            self.v = max(self.v, 0)
+            self.x = 1
+        if self.x < 0:
+            self.v = min(self.v, 0)
+            self.x = 0
+
+        if self.equal(self.x, self.xgoal):
+            self.x = self.xgoal  # clean up value
             self.stop()
             return
-        
-        self._stopped=0
-        dir = (-1.0,1,0)[self.xgoal>self.x]
 
-        if abs(self.xgoal-self.x) < abs(self.v*5*dt):
+        self._stopped = 0
+        dir = (-1.0, 1, 0)[self.xgoal > self.x]
+
+        if abs(self.xgoal - self.x) < abs(self.v * 5 * dt):
             # apply the brakes on the last 5 steps
             dir *= -.5
 
-        self.v += dir*self.maxaccel*dt # velocity changes with acceleration in the right direction
-        self.v = min(max(self.v,-self.maxspeed),self.maxspeed) # clamp velocity
+        self.v += dir * self.maxaccel * dt  # velocity changes with acceleration in the right direction
+        self.v = min(max(self.v, -self.maxspeed),
+                     self.maxspeed)  # clamp velocity
 
         #print "x=%+.03f v=%+.03f a=%+.03f %f" % (self.x,self.v,self.maxaccel,self.xgoal)
 
-    def goto(self,newx):
-        self.xgoal=newx
+    def goto(self, newx):
+        self.xgoal = newx
 
     def ismoving(self):
         return not self._stopped
 
+
 class FlyingFader(Frame):
-    def __init__(self, master, variable, label, fadedur=1.5, font=('Arial', 8), labelwidth=12,
+
+    def __init__(self,
+                 master,
+                 variable,
+                 label,
+                 fadedur=1.5,
+                 font=('Arial', 8),
+                 labelwidth=12,
                  **kw):
         Frame.__init__(self, master)
         self.name = label
         self.variable = variable
 
         self.mass = Mass()
-        
-        self.config({'bd':1, 'relief':'raised'})
-        scaleopts = {'variable' : variable, 'showvalue' : 0, 'from' : 1.0,
-                     'to' : 0, 'res' : 0.001, 'width' : 20, 'length' : 200, 'orient':'vert'}
+
+        self.config({'bd': 1, 'relief': 'raised'})
+        scaleopts = {
+            'variable': variable,
+            'showvalue': 0,
+            'from': 1.0,
+            'to': 0,
+            'res': 0.001,
+            'width': 20,
+            'length': 200,
+            'orient': 'vert'
+        }
         scaleopts.update(kw)
-        if scaleopts['orient']=='vert':
-            side1=TOP
-            side2=BOTTOM
+        if scaleopts['orient'] == 'vert':
+            side1 = TOP
+            side2 = BOTTOM
         else:
-            side1=RIGHT
-            side2=LEFT
-        
+            side1 = RIGHT
+            side2 = LEFT
+
         self.scale = Scale(self, **scaleopts)
         self.vlabel = Label(self, text="0.0", width=6, font=font)
-        self.label = Label(self, text=label, font=font, anchor='w',width=labelwidth) #wraplength=40, )
+        self.label = Label(self,
+                           text=label,
+                           font=font,
+                           anchor='w',
+                           width=labelwidth)  #wraplength=40, )
 
         self.oldtrough = self.scale['troughcolor']
 
@@ -89,8 +116,8 @@
         self.label.pack(side=side2, expand=0, fill=X)
 
         for k in range(1, 10):
-            self.scale.bind("<Key-%d>" % k, 
-                lambda evt, k=k: self.newfade(k / 10.0, evt))
+            self.scale.bind(
+                "<Key-%d>" % k, lambda evt, k=k: self.newfade(k / 10.0, evt))
 
         self.scale.bind("<Key-0>", lambda evt: self.newfade(1.0, evt))
         self.scale.bind("<grave>", lambda evt: self.newfade(0, evt))
@@ -100,10 +127,10 @@
         self.scale.bind("<3>", self.mousefade)
 
         self.trace_ret = self.variable.trace('w', self.updatelabel)
-        self.bind("<Destroy>",self.ondestroy)
+        self.bind("<Destroy>", self.ondestroy)
 
-    def ondestroy(self,*ev):
-        self.variable.trace_vdelete('w',self.trace_ret)
+    def ondestroy(self, *ev):
+        self.variable.trace_vdelete('w', self.trace_ret)
 
     def cancelfade(self, evt):
         self.fadegoal = self.variable.get()
@@ -116,16 +143,15 @@
         self.newfade(target, evt)
 
     def ismoving(self):
-        return self.fadevel!=0 or self.fadeacc!=0
+        return self.fadevel != 0 or self.fadeacc != 0
 
     def newfade(self, newlevel, evt=None, length=None):
 
         # these are currently unused-- Mass needs to accept a speed input
         mult = 1
-        if evt.state & 8 and evt.state & 4: mult = 0.25 # both
-        elif evt.state & 8: mult = 0.5 # alt
-        elif evt.state & 4: mult = 2   # control
-
+        if evt.state & 8 and evt.state & 4: mult = 0.25  # both
+        elif evt.state & 8: mult = 0.5  # alt
+        elif evt.state & 4: mult = 2  # control
 
         self.mass.x = self.variable.get()
         self.mass.goto(newlevel)
@@ -139,9 +165,9 @@
         if not self.mass.ismoving():
             self.scale['troughcolor'] = self.oldtrough
             return
-        
+
         # blink the trough while the thing's moving
-        if time()%.4>.2:
+        if time() % .4 > .2:
             # self.scale.config(troughcolor=self.oldtrough)
             self.scale.config(troughcolor='orange')
         else:
@@ -154,6 +180,8 @@
     def updatelabel(self, *args):
         if self.variable:
             self.vlabel['text'] = "%.3f" % self.variable.get()
+
+
 #        if self.fadetimes[1] == 0: # no fade
 #            self.vlabel['fg'] = 'black'
 #        elif self.curfade[1] > self.curfade[0]:
@@ -167,25 +195,32 @@
     def set(self, val):
         self.scale.set(val)
 
+
 def colorfade(scale, lev):
     low = (255, 255, 255)
     high = (0, 0, 0)
-    out = [int(l+lev*(h-l)) for h, l in zip(high,low)]
-    col="#%02X%02X%02X" % tuple(out)
+    out = [int(l + lev * (h - l)) for h, l in zip(high, low)]
+    col = "#%02X%02X%02X" % tuple(out)
     scale.config(troughcolor=col)
 
+
 if __name__ == '__main__':
     root = Tk()
     root.tk_focusFollowsMouse()
 
-    FlyingFader(root, variable=DoubleVar(), label="suck").pack(side=LEFT, 
-        expand=1, fill=BOTH)
+    FlyingFader(root, variable=DoubleVar(), label="suck").pack(side=LEFT,
+                                                               expand=1,
+                                                               fill=BOTH)
     FlyingFader(root, variable=DoubleVar(), label="moof").pack(side=LEFT,
-        expand=1, fill=BOTH)
+                                                               expand=1,
+                                                               fill=BOTH)
     FlyingFader(root, variable=DoubleVar(), label="zarf").pack(side=LEFT,
-        expand=1, fill=BOTH)
-    FlyingFader(root, variable=DoubleVar(), 
-        label="long name goes here.  got it?").pack(side=LEFT, expand=1, 
-        fill=BOTH)
+                                                               expand=1,
+                                                               fill=BOTH)
+    FlyingFader(root,
+                variable=DoubleVar(),
+                label="long name goes here.  got it?").pack(side=LEFT,
+                                                            expand=1,
+                                                            fill=BOTH)
 
     root.mainloop()
--- a/light9/Patch.py	Tue May 21 23:55:35 2019 +0000
+++ b/light9/Patch.py	Tue May 21 23:56:12 2019 +0000
@@ -8,12 +8,14 @@
     "Ensure that we're talking about the primary name of the light."
     return get_channel_name(get_dmx_channel(channelname))
 
+
 def get_all_channels():
     """returns primary names for all channels (sorted)"""
     prinames = reverse_patch.values()[:]
     prinames.sort()
     return prinames
 
+
 def get_dmx_channel(name):
     if str(name) in patch:
         return patch[str(name)]
@@ -24,6 +26,7 @@
     except ValueError:
         raise ValueError("Invalid channel name: %r" % name)
 
+
 def get_channel_name(dmxnum):
     """if you pass a name, it will get normalized"""
     try:
@@ -31,12 +34,15 @@
     except KeyError:
         return str(dmxnum)
 
+
 def get_channel_uri(name):
     return uri_map[name]
 
+
 def dmx_from_uri(uri):
     return uri_patch[uri]
 
+
 def reload_data():
     global patch, reverse_patch, uri_map, uri_patch
     patch = {}
@@ -67,6 +73,6 @@
                     else:
                         reverse_patch[name] = norm_name
 
+
 # importing patch will load initial data
 reload_data()
-
--- a/light9/ascoltami/player.py	Tue May 21 23:55:35 2019 +0000
+++ b/light9/ascoltami/player.py	Tue May 21 23:56:12 2019 +0000
@@ -1,5 +1,4 @@
 #!/usr/bin/python
-
 """
 alternate to the mpd music player, for ascoltami
 """
@@ -8,10 +7,11 @@
 from gi.repository import GObject, Gst
 from twisted.internet import reactor, task
 
-
 log = logging.getLogger()
 
+
 class Player(object):
+
     def __init__(self, autoStopOffset=4, onEOS=None):
         """autoStopOffset is the number of seconds before the end of
         song before automatically stopping (which is really pausing).
@@ -20,27 +20,27 @@
         It is called with one argument which is the URI of the song that
         just finished."""
         self.autoStopOffset = autoStopOffset
-        self.playbin = self.pipeline = Gst.ElementFactory.make('playbin',None)
+        self.playbin = self.pipeline = Gst.ElementFactory.make('playbin', None)
 
         self.playStartTime = 0
         self.lastWatchTime = 0
         self.autoStopTime = 0
         self.lastSetSongUri = None
         self.onEOS = onEOS
-        
+
         task.LoopingCall(self.watchTime).start(.050)
 
         bus = self.pipeline.get_bus()
         # not working- see notes in pollForMessages
         #self.watchForMessages(bus)
-      
+
     def watchTime(self):
         try:
             self.pollForMessages()
-            
+
             t = self.currentTime()
-            log.debug("watch %s < %s < %s",
-                      self.lastWatchTime, self.autoStopTime, t)
+            log.debug("watch %s < %s < %s", self.lastWatchTime,
+                      self.autoStopTime, t)
             if self.lastWatchTime < self.autoStopTime < t:
                 log.info("autostop")
                 self.pause()
@@ -58,6 +58,7 @@
             print "onEos", args
             if self.onEOS is not None:
                 self.onEOS(self.getSong())
+
         bus.connect('message::eos', onEos)
 
         def onStreamStatus(bus, message):
@@ -65,14 +66,16 @@
             (statusType, _elem) = message.parse_stream_status()
             if statusType == Gst.StreamStatusType.ENTER:
                 self.setupAutostop()
+
         bus.connect('message::stream-status', onStreamStatus)
-            
+
     def pollForMessages(self):
         """bus.add_signal_watch seems to be having no effect, but this works"""
         bus = self.pipeline.get_bus()
         mt = Gst.MessageType
-        msg = bus.poll(mt.EOS | mt.STREAM_STATUS | mt.ERROR,# | mt.ANY,
-                       0)
+        msg = bus.poll(
+            mt.EOS | mt.STREAM_STATUS | mt.ERROR,  # | mt.ANY,
+            0)
         if msg is not None:
             log.debug("bus message: %r %r", msg.src, msg.type)
             # i'm trying to catch here a case where the pulseaudio
@@ -88,7 +91,7 @@
                 (statusType, _elem) = msg.parse_stream_status()
                 if statusType == Gst.StreamStatusType.ENTER:
                     self.setupAutostop()
-            
+
     def seek(self, t):
         isSeekable = self.playbin.seek_simple(
             Gst.Format.TIME,
@@ -149,9 +152,15 @@
         """json-friendly object describing the interesting states of
         the player nodes"""
         success, state, pending = self.playbin.get_state(timeout=0)
-        return {"current": {"name":state.value_nick},
-                "pending": {"name":state.value_nick}}
-        
+        return {
+            "current": {
+                "name": state.value_nick
+            },
+            "pending": {
+                "name": state.value_nick
+            }
+        }
+
     def pause(self):
         self.pipeline.set_state(Gst.State.PAUSED)
 
@@ -161,7 +170,8 @@
         """
         pos = self.currentTime()
         autoStop = self.duration() - self.autoStopOffset
-        return not self.isPlaying() and abs(pos - autoStop) < 1 # i've seen .4 difference here
+        return not self.isPlaying() and abs(
+            pos - autoStop) < 1  # i've seen .4 difference here
 
     def resume(self):
         self.pipeline.set_state(Gst.State.PLAYING)
--- a/light9/ascoltami/playlist.py	Tue May 21 23:55:35 2019 +0000
+++ b/light9/ascoltami/playlist.py	Tue May 21 23:56:12 2019 +0000
@@ -1,15 +1,18 @@
 from light9.showconfig import songOnDisk
 from light9.namespaces import L9
 
+
 class NoSuchSong(ValueError):
     """Raised when a song is requested that doesn't exist (e.g. one
     after the last song in the playlist)."""
 
+
 class Playlist(object):
+
     def __init__(self, graph, playlistUri):
         self.graph = graph
         self.songs = list(graph.items(playlistUri))
-        
+
     def nextSong(self, currentSong):
         """Returns the next song in the playlist or raises NoSuchSong if 
         we are at the end of the playlist."""
@@ -30,14 +33,14 @@
     def allSongs(self):
         """Returns a list of all song URIs in order."""
         return self.songs
-    
+
     def allSongPaths(self):
         """Returns a list of the filesystem paths to all songs in order."""
         paths = []
         for song in self.songs:
             paths.append(songOnDisk(song))
         return paths
-    
+
     def songPath(self, uri):
         """filesystem path to a song"""
         raise NotImplementedError("see showconfig.songOnDisk")
--- a/light9/ascoltami/webapp.py	Tue May 21 23:55:35 2019 +0000
+++ b/light9/ascoltami/webapp.py	Tue May 21 23:56:12 2019 +0000
@@ -9,32 +9,40 @@
 
 from lib.cycloneerr import PrettyErrorHandler
 
-_songUris = {} # locationUri : song
+_songUris = {}  # locationUri : song
+
+
 def songLocation(graph, songUri):
     loc = URIRef("file://%s" % songOnDisk(songUri))
     _songUris[loc] = songUri
     return loc
-    
+
+
 def songUri(graph, locationUri):
     return _songUris[locationUri]
 
+
 class root(PrettyErrorHandler, cyclone.web.RequestHandler):
+
     def get(self):
         self.set_header("Content-Type", "application/xhtml+xml")
         # todo: use a template; embed the show name and the intro/post
         # times into the page
         self.write(render.index(host=socket.gethostname()))
 
+
 def playerSongUri(graph, player):
     """or None"""
-    
+
     playingLocation = player.getSong()
     if playingLocation:
         return songUri(graph, URIRef(playingLocation))
     else:
         return None
 
-class timeResource(PrettyErrorHandler,cyclone.web.RequestHandler):
+
+class timeResource(PrettyErrorHandler, cyclone.web.RequestHandler):
+
     def get(self):
         player = self.settings.app.player
         graph = self.settings.app.graph
@@ -47,14 +55,15 @@
         else:
             nextAction = 'play'
 
-        self.write(json.dumps({
-            "song" : playerSongUri(graph, player),
-            "started" : player.playStartTime,
-            "duration" : player.duration(),
-            "playing" : player.isPlaying(),
-            "t" : player.currentTime(),
-            "state" : player.states(),
-            "next" : nextAction,
+        self.write(
+            json.dumps({
+                "song": playerSongUri(graph, player),
+                "started": player.playStartTime,
+                "duration": player.duration(),
+                "playing": player.isPlaying(),
+                "t": player.currentTime(),
+                "state": player.states(),
+                "next": nextAction,
             }))
 
     def post(self):
@@ -74,28 +83,39 @@
         self.set_header("Content-Type", "text/plain")
         self.write("ok")
 
+
 class songs(PrettyErrorHandler, cyclone.web.RequestHandler):
+
     def get(self):
         graph = self.settings.app.graph
 
         songs = getSongsFromShow(graph, self.settings.app.show)
 
         self.set_header("Content-Type", "application/json")
-        self.write(json.dumps({"songs" : [
-            {"uri" : s,
-             "path" : graph.value(s, L9['showPath']),
-             "label" : graph.label(s)} for s in songs]}))
+        self.write(
+            json.dumps({
+                "songs": [{
+                    "uri": s,
+                    "path": graph.value(s, L9['showPath']),
+                    "label": graph.label(s)
+                } for s in songs]
+            }))
+
 
 class songResource(PrettyErrorHandler, cyclone.web.RequestHandler):
+
     def post(self):
         """post a uri of song to switch to (and start playing)"""
         graph = self.settings.app.graph
 
-        self.settings.app.player.setSong(songLocation(graph, URIRef(self.request.body)))
+        self.settings.app.player.setSong(
+            songLocation(graph, URIRef(self.request.body)))
         self.set_header("Content-Type", "text/plain")
         self.write("ok")
-    
+
+
 class seekPlayOrPause(PrettyErrorHandler, cyclone.web.RequestHandler):
+
     def post(self):
         player = self.settings.app.player
 
@@ -106,12 +126,16 @@
             player.seek(data['t'])
             player.resume()
 
+
 class output(PrettyErrorHandler, cyclone.web.RequestHandler):
+
     def post(self):
         d = json.loads(self.request.body)
         subprocess.check_call(["bin/movesinks", str(d['sink'])])
 
+
 class goButton(PrettyErrorHandler, cyclone.web.RequestHandler):
+
     def post(self):
         """
         if music is playing, this silently does nothing.
@@ -124,10 +148,11 @@
             pass
         else:
             player.resume()
-            
+
         self.set_header("Content-Type", "text/plain")
         self.write("ok")
 
+
 def makeWebApp(app):
     return cyclone.web.Application(handlers=[
         (r"/", root),
@@ -137,5 +162,5 @@
         (r"/seekPlayOrPause", seekPlayOrPause),
         (r"/output", output),
         (r"/go", goButton),
-        ], app=app)
-
+    ],
+                                   app=app)
--- a/light9/chase.py	Tue May 21 23:55:35 2019 +0000
+++ b/light9/chase.py	Tue May 21 23:56:12 2019 +0000
@@ -1,7 +1,13 @@
 from __future__ import division
 
-def chase(t, ontime=0.5, offset=0.2, onval=1.0, 
-          offval=0.0, names=None, combiner=max):
+
+def chase(t,
+          ontime=0.5,
+          offset=0.2,
+          onval=1.0,
+          offval=0.0,
+          names=None,
+          combiner=max):
     names = names or []
     # maybe this is better:
     # period = ontime + ((offset + ontime) * (len(names) - 1))
@@ -26,11 +32,16 @@
             outputs[name] = value
     return outputs
 
+
 if __name__ == "__main__":
     # a little testing
     for x in range(80):
         x /= 20.0
-        output = chase(x, onval='x', offval=' ', ontime=0.1, offset=0.2,
+        output = chase(x,
+                       onval='x',
+                       offval=' ',
+                       ontime=0.1,
+                       offset=0.2,
                        names=('a', 'b', 'c', 'd'))
         output = output.items()
         output.sort()
--- a/light9/clientsession.py	Tue May 21 23:55:35 2019 +0000
+++ b/light9/clientsession.py	Tue May 21 23:56:12 2019 +0000
@@ -6,12 +6,15 @@
 from urllib import quote
 from light9 import showconfig
 
+
 def add_option(parser):
     parser.add_option(
-        '-s', '--session',
+        '-s',
+        '--session',
         help="name of session used for levels and window position",
         default='default')
 
+
 def getUri(appName, opts):
-    return URIRef("%s/sessions/%s/%s" % (showconfig.showUri(), appName,
-                                         quote(opts.session, safe='')))
+    return URIRef("%s/sessions/%s/%s" %
+                  (showconfig.showUri(), appName, quote(opts.session, safe='')))
--- a/light9/collector/collector.py	Tue May 21 23:55:35 2019 +0000
+++ b/light9/collector/collector.py	Tue May 21 23:56:12 2019 +0000
@@ -16,6 +16,7 @@
 
 log = logging.getLogger('collector')
 
+
 def outputMap(graph, outputs):
     # type: (Graph, List[Output]) -> Dict[Tuple[URIRef, URIRef], Tuple[Output, int]]
     """From rdf config graph, compute a map of
@@ -46,8 +47,10 @@
                 ret[(dev, outputAttr)] = (output, index)
                 log.debug('    map %s to %s,%s', outputAttr, output, index)
     return ret
-        
+
+
 class Collector(Generic[ClientType, ClientSessionType]):
+
     def __init__(self, graph, outputs, listeners=None, clientTimeoutSec=10):
         # type: (Graph, List[Output], List[Listener], float) -> None
         self.graph = graph
@@ -60,15 +63,17 @@
         self.graph.addHandler(self.rebuildOutputMap)
 
         # client : (session, time, {(dev,devattr): latestValue})
-        self.lastRequest = {} # type: Dict[Tuple[ClientType, ClientSessionType], Tuple[float, Dict[Tuple[URIRef, URIRef], float]]]
+        self.lastRequest = {
+        }  # type: Dict[Tuple[ClientType, ClientSessionType], Tuple[float, Dict[Tuple[URIRef, URIRef], float]]]
 
         # (dev, devAttr): value to use instead of 0
-        self.stickyAttrs = {} # type: Dict[Tuple[URIRef, URIRef], float] 
+        self.stickyAttrs = {}  # type: Dict[Tuple[URIRef, URIRef], float]
 
     def rebuildOutputMap(self):
-        self.outputMap = outputMap(self.graph, self.outputs) # (device, outputattr) : (output, index)
-        self.deviceType = {} # uri: type that's a subclass of Device
-        self.remapOut = {} # (device, deviceAttr) : (start, end)
+        self.outputMap = outputMap(
+            self.graph, self.outputs)  # (device, outputattr) : (output, index)
+        self.deviceType = {}  # uri: type that's a subclass of Device
+        self.remapOut = {}  # (device, deviceAttr) : (start, end)
         for dc in self.graph.subjects(RDF.type, L9['DeviceClass']):
             for dev in self.graph.subjects(RDF.type, dc):
                 self.allDevices.add(dev)
@@ -93,7 +98,7 @@
     # todo: move to settings.py
     def resolvedSettingsDict(self, settingsList):
         # type: (List[Tuple[URIRef, URIRef, float]]) -> Dict[Tuple[URIRef, URIRef], float]
-        out = {} # type: Dict[Tuple[URIRef, URIRef], float]
+        out = {}  # type: Dict[Tuple[URIRef, URIRef], float]
         for d, da, v in settingsList:
             if (d, da) in out:
                 out[(d, da)] = resolve(d, da, [out[(d, da)], v])
@@ -103,13 +108,15 @@
 
     def _warnOnLateRequests(self, client, now, sendTime):
         requestLag = now - sendTime
-        if requestLag > .1 and now > self.initTime + 10 and getattr(self, '_lastWarnTime', 0) < now - 3:
+        if requestLag > .1 and now > self.initTime + 10 and getattr(
+                self, '_lastWarnTime', 0) < now - 3:
             self._lastWarnTime = now
-            log.warn('collector.setAttrs from %s is running %.1fms after the request was made',
-                     client, requestLag * 1000)
+            log.warn(
+                'collector.setAttrs from %s is running %.1fms after the request was made',
+                client, requestLag * 1000)
 
     def _merge(self, lastRequests):
-        deviceAttrs = {} # device: {deviceAttr: value}       
+        deviceAttrs = {}  # device: {deviceAttr: value}
         for _, lastSettings in lastRequests:
             for (device, deviceAttr), value in lastSettings.iteritems():
                 if (device, deviceAttr) in self.remapOut:
@@ -118,7 +125,8 @@
 
                 attrs = deviceAttrs.setdefault(device, {})
                 if deviceAttr in attrs:
-                    value = resolve(device, deviceAttr, [attrs[deviceAttr], value])
+                    value = resolve(device, deviceAttr,
+                                    [attrs[deviceAttr], value])
                 attrs[deviceAttr] = value
                 # list should come from the graph. these are attrs
                 # that should default to holding the last position,
@@ -131,7 +139,7 @@
             daDict = deviceAttrs.setdefault(d, {})
             if da not in daDict:
                 daDict[da] = v
-                    
+
         return deviceAttrs
 
     def setAttrs(self, client, clientSession, settings, sendTime):
@@ -156,8 +164,8 @@
         self.lastRequest[(client, clientSession)] = (now, uniqueSettings)
 
         deviceAttrs = self._merge(self.lastRequest.itervalues())
-        
-        outputAttrs = {} # device: {outputAttr: value}
+
+        outputAttrs = {}  # device: {outputAttr: value}
         for d in self.allDevices:
             try:
                 devType = self.deviceType[d]
@@ -167,11 +175,12 @@
             try:
                 outputAttrs[d] = toOutputAttrs(devType, deviceAttrs.get(d, {}))
                 if self.listeners:
-                    self.listeners.outputAttrsSet(d, outputAttrs[d], self.outputMap)
+                    self.listeners.outputAttrsSet(d, outputAttrs[d],
+                                                  self.outputMap)
             except Exception as e:
                 log.error('failing toOutputAttrs on %s: %r', d, e)
-        
-        pendingOut = {} # output : values
+
+        pendingOut = {}  # output : values
         for out in self.outputs:
             pendingOut[out] = [0] * out.numChannels
 
@@ -183,9 +192,10 @@
         self.flush(pendingOut)
         dt2 = 1000 * (time.time() - now)
         if dt1 > 30:
-            log.warn("slow setAttrs: %.1fms -> flush -> %.1fms. lr %s da %s oa %s" % (
-                dt1, dt2, len(self.lastRequest), len(deviceAttrs), len(outputAttrs)
-            ))
+            log.warn(
+                "slow setAttrs: %.1fms -> flush -> %.1fms. lr %s da %s oa %s" %
+                (dt1, dt2, len(
+                    self.lastRequest), len(deviceAttrs), len(outputAttrs)))
 
     def setAttr(self, device, outputAttr, value, pendingOut):
         output, index = self.outputMap[(device, outputAttr)]
--- a/light9/collector/collector_client.py	Tue May 21 23:55:35 2019 +0000
+++ b/light9/collector/collector_client.py	Tue May 21 23:56:12 2019 +0000
@@ -3,38 +3,43 @@
 from light9.effect.settings import DeviceSettings
 from twisted.internet import defer
 from txzmq import ZmqEndpoint, ZmqFactory, ZmqPushConnection
-import json, time,logging
+import json, time, logging
 import treq
 
-
 log = logging.getLogger('coll_client')
 
-_zmqClient=None
+_zmqClient = None
+
+
 class TwistedZmqClient(object):
+
     def __init__(self, service):
         zf = ZmqFactory()
         e = ZmqEndpoint('connect', 'tcp://%s:%s' % (service.host, service.port))
         self.conn = ZmqPushConnection(zf, e)
-        
+
     def send(self, msg):
         self.conn.push(msg)
 
 
 def toCollectorJson(client, session, settings):
     assert isinstance(settings, DeviceSettings)
-    return json.dumps({'settings': settings.asList(),
-                       'client': client,
-                       'clientSession': session,
-                       'sendTime': time.time(),
-                  })
-        
+    return json.dumps({
+        'settings': settings.asList(),
+        'client': client,
+        'clientSession': session,
+        'sendTime': time.time(),
+    })
+
+
 def sendToCollectorZmq(msg):
     global _zmqClient
     if _zmqClient is None:
         _zmqClient = TwistedZmqClient(networking.collectorZmq)
     _zmqClient.send(msg)
     return defer.succeed(0)
-        
+
+
 def sendToCollector(client, session, settings, useZmq=False):
     """deferred to the time in seconds it took to get a response from collector"""
     sendTime = time.time()
@@ -44,14 +49,17 @@
         d = sendToCollectorZmq(msg)
     else:
         d = treq.put(networking.collector.path('attrs'), data=msg, timeout=1)
-    
+
     def onDone(result):
         dt = time.time() - sendTime
         if dt > .1:
             log.warn('sendToCollector request took %.1fms', dt * 1000)
         return dt
+
     d.addCallback(onDone)
+
     def onErr(err):
         log.warn('sendToCollector failed: %r', err)
+
     d.addErrback(onErr)
     return d
--- a/light9/collector/collector_test.py	Tue May 21 23:55:35 2019 +0000
+++ b/light9/collector/collector_test.py	Tue May 21 23:56:12 2019 +0000
@@ -35,9 +35,11 @@
 
 '''
 
-t0 = 0 # time
+t0 = 0  # time
+
 
 class MockOutput(object):
+
     def __init__(self, uri, connections):
         self.connections = connections
         self.updates = []
@@ -53,31 +55,41 @@
     def flush(self):
         self.updates.append('flush')
 
-@unittest.skip("outputMap got rewritten and mostly doesn't raise on these cases")
+
+@unittest.skip("outputMap got rewritten and mostly doesn't raise on these cases"
+              )
 class TestOutputMap(unittest.TestCase):
+
     def testWorking(self):
         out0 = MockOutput(UDMX, [(0, DMX0['c1'])])
-        m = outputMap(MockSyncedGraph(PREFIX + '''
+        m = outputMap(
+            MockSyncedGraph(PREFIX + '''
           dmx0:c1 :connectedTo dev:inst1Brightness .
           dev:inst1 a :Device; :brightness dev:inst1Brightness .
         '''), [out0])
         self.assertEqual({(DEV['inst1'], L9['brightness']): (out0, 0)}, m)
-        
+
     def testMissingOutput(self):
         out0 = MockOutput(UDMX, [(0, DMX0['c1'])])
-        self.assertRaises(KeyError, outputMap, MockSyncedGraph(PREFIX + '''
+        self.assertRaises(
+            KeyError, outputMap,
+            MockSyncedGraph(PREFIX + '''
           dev:inst1 a :Device; :brightness dev:inst1Brightness .
         '''), [out0])
 
     def testMissingOutputConnection(self):
         out0 = MockOutput(UDMX, [(0, DMX0['c1'])])
-        self.assertRaises(ValueError, outputMap, MockSyncedGraph(PREFIX + '''
+        self.assertRaises(
+            ValueError, outputMap,
+            MockSyncedGraph(PREFIX + '''
           dev:inst1 a :Device; :brightness dev:inst1Brightness .
         '''), [out0])
 
     def testMultipleOutputConnections(self):
         out0 = MockOutput(UDMX, [(0, DMX0['c1'])])
-        self.assertRaises(ValueError, outputMap, MockSyncedGraph(PREFIX + '''
+        self.assertRaises(
+            ValueError, outputMap,
+            MockSyncedGraph(PREFIX + '''
           dmx0:c1 :connectedTo dev:inst1Brightness .
           dmx0:c2 :connectedTo dev:inst1Brightness .
           dev:inst1 a :Device; :brightness dev:inst1Brightness .
@@ -85,6 +97,7 @@
 
 
 class TestCollector(unittest.TestCase):
+
     def setUp(self):
         self.config = MockSyncedGraph(PREFIX + THEATER + '''
 
@@ -101,10 +114,8 @@
         ''')
 
         self.dmx0 = MockOutput(DMX0[None], [(0, DMX0['c1'])])
-        self.udmx = MockOutput(UDMX[None], [(0, UDMX['c1']),
-                                      (1, UDMX['c2']),
-                                      (2, UDMX['c3']),
-                                      (3, UDMX['c4'])])
+        self.udmx = MockOutput(UDMX[None], [(0, UDMX['c1']), (1, UDMX['c2']),
+                                            (2, UDMX['c3']), (3, UDMX['c4'])])
 
     def testRoutesColorOutput(self):
         c = Collector(self.config, outputs=[self.dmx0, self.udmx])
@@ -123,9 +134,9 @@
         c.setAttrs('client2', 'sess1',
                    [(DEV['colorStrip'], L9['color'], '#333333')], t0)
 
-        self.assertEqual([[215, 255, 0, 0], 'flush',
-                          [215, 255, 51, 51], 'flush'],
-                         self.udmx.updates)
+        self.assertEqual(
+            [[215, 255, 0, 0], 'flush', [215, 255, 51, 51], 'flush'],
+            self.udmx.updates)
         self.assertEqual([[0, 0, 0, 0], 'flush', [0, 0, 0, 0], 'flush'],
                          self.dmx0.updates)
 
@@ -139,36 +150,36 @@
         c.setAttrs('client1', 'sess1',
                    [(DEV['colorStrip'], L9['color'], '#050000')], t0)
 
-        self.assertEqual([[215, 8, 0, 0], 'flush',
-                          [215, 8, 0, 0], 'flush',
-                          [215, 6, 0, 0], 'flush'],
-                         self.udmx.updates)
-        self.assertEqual([[0, 0, 0, 0], 'flush',
-                          [0, 0, 0, 0], 'flush',
-                          [0, 0, 0, 0], 'flush'],
-                         self.dmx0.updates)
+        self.assertEqual([[215, 8, 0, 0], 'flush', [215, 8, 0, 0], 'flush',
+                          [215, 6, 0, 0], 'flush'], self.udmx.updates)
+        self.assertEqual([[0, 0, 0, 0], 'flush', [0, 0, 0, 0], 'flush',
+                          [0, 0, 0, 0], 'flush'], self.dmx0.updates)
 
     def testClientsOnDifferentOutputs(self):
         c = Collector(self.config, outputs=[self.dmx0, self.udmx])
 
-        c.setAttrs('client1', 'sess1', [(DEV['colorStrip'], L9['color'], '#aa0000')], t0)
-        c.setAttrs('client2', 'sess1', [(DEV['inst1'], L9['brightness'], .5)], t0)
+        c.setAttrs('client1', 'sess1',
+                   [(DEV['colorStrip'], L9['color'], '#aa0000')], t0)
+        c.setAttrs('client2', 'sess1', [(DEV['inst1'], L9['brightness'], .5)],
+                   t0)
 
         # ok that udmx is flushed twice- it can screen out its own duplicates
-        self.assertEqual([[215, 170, 0, 0], 'flush',
-                          [215, 170, 0, 0], 'flush'], self.udmx.updates)
-        self.assertEqual([[0, 0, 0, 0], 'flush',
-                          [127, 0, 0, 0], 'flush'], self.dmx0.updates)
+        self.assertEqual([[215, 170, 0, 0], 'flush', [215, 170, 0, 0], 'flush'],
+                         self.udmx.updates)
+        self.assertEqual([[0, 0, 0, 0], 'flush', [127, 0, 0, 0], 'flush'],
+                         self.dmx0.updates)
 
     def testNewSessionReplacesPreviousOutput(self):
         # ..as opposed to getting max'd with it
         c = Collector(self.config, outputs=[self.dmx0, self.udmx])
 
-        c.setAttrs('client1', 'sess1', [(DEV['inst1'], L9['brightness'], .8)], t0)
-        c.setAttrs('client1', 'sess2', [(DEV['inst1'], L9['brightness'], .5)], t0)
+        c.setAttrs('client1', 'sess1', [(DEV['inst1'], L9['brightness'], .8)],
+                   t0)
+        c.setAttrs('client1', 'sess2', [(DEV['inst1'], L9['brightness'], .5)],
+                   t0)
 
-        self.assertEqual([[204, 0, 0, 0], 'flush',
-                          [127, 0, 0, 0], 'flush'], self.dmx0.updates)
+        self.assertEqual([[204, 0, 0, 0], 'flush', [127, 0, 0, 0], 'flush'],
+                         self.dmx0.updates)
 
     def testNewSessionDropsPreviousSettingsOfOtherAttrs(self):
         c = Collector(MockSyncedGraph(PREFIX + THEATER + '''
@@ -183,15 +194,16 @@
         dev:inst1 a :Device, :SimpleDimmer;
           :dmxUniverse dmx0:; :dmxBase 0;
           :level dev:inst1Brightness .
-        '''), outputs=[self.dmx0, self.udmx])
+        '''),
+                      outputs=[self.dmx0, self.udmx])
 
         c.setAttrs('client1', 'sess1',
                    [(DEV['colorStrip'], L9['color'], '#ff0000')], t0)
         c.setAttrs('client1', 'sess2',
                    [(DEV['colorStrip'], L9['color'], '#00ff00')], t0)
 
-        self.assertEqual([[215, 255, 0, 0], 'flush',
-                          [215, 0, 255, 0], 'flush'], self.udmx.updates)
+        self.assertEqual([[215, 255, 0, 0], 'flush', [215, 0, 255, 0], 'flush'],
+                         self.udmx.updates)
 
     def testClientIsForgottenAfterAWhile(self):
         with freeze_time(datetime.datetime.now()) as ft:
@@ -200,37 +212,34 @@
                        time.time())
             ft.tick(delta=datetime.timedelta(seconds=1))
             # this max's with cli1's value so we still see .5
-            c.setAttrs('cli2', 'sess1', [(DEV['inst1'], L9['brightness'], .2)], 
+            c.setAttrs('cli2', 'sess1', [(DEV['inst1'], L9['brightness'], .2)],
                        time.time())
             ft.tick(delta=datetime.timedelta(seconds=9.1))
             # now cli1 is forgotten, so our value appears
-            c.setAttrs('cli2', 'sess1', [(DEV['inst1'], L9['brightness'], .4)], 
+            c.setAttrs('cli2', 'sess1', [(DEV['inst1'], L9['brightness'], .4)],
                        time.time())
-            self.assertEqual([[127, 0, 0, 0], 'flush',
-                              [127, 0, 0, 0], 'flush',
-                              [102, 0, 0, 0], 'flush'],
-                             self.dmx0.updates)
+            self.assertEqual([[127, 0, 0, 0], 'flush', [127, 0, 0, 0], 'flush',
+                              [102, 0, 0, 0], 'flush'], self.dmx0.updates)
 
     def testClientUpdatesAreNotMerged(self):
         # second call to setAttrs forgets the first
         c = Collector(self.config, outputs=[self.dmx0, self.udmx])
         t0 = time.time()
-        c.setAttrs('client1', 'sess1', [(DEV['inst1'], L9['brightness'], .5)], t0)
-        c.setAttrs('client1', 'sess1', [(DEV['inst1'], L9['brightness'], 1)], t0)
-        c.setAttrs('client1', 'sess1', [(DEV['colorStrip'], L9['color'], '#00ff00')], t0)
+        c.setAttrs('client1', 'sess1', [(DEV['inst1'], L9['brightness'], .5)],
+                   t0)
+        c.setAttrs('client1', 'sess1', [(DEV['inst1'], L9['brightness'], 1)],
+                   t0)
+        c.setAttrs('client1', 'sess1',
+                   [(DEV['colorStrip'], L9['color'], '#00ff00')], t0)
 
-        self.assertEqual([[215, 0, 0, 0], 'flush',
-                          [215, 0, 0, 0], 'flush',
-                          [215, 0, 255, 0], 'flush'],
-                         self.udmx.updates)
-        self.assertEqual([[127, 0, 0, 0], 'flush',
-                          [255, 0, 0, 0], 'flush',
-                          [0, 0, 0, 0], 'flush'],
-                         self.dmx0.updates)
+        self.assertEqual([[215, 0, 0, 0], 'flush', [215, 0, 0, 0], 'flush',
+                          [215, 0, 255, 0], 'flush'], self.udmx.updates)
+        self.assertEqual([[127, 0, 0, 0], 'flush', [255, 0, 0, 0], 'flush',
+                          [0, 0, 0, 0], 'flush'], self.dmx0.updates)
 
     def testRepeatedAttributesInOneRequestGetResolved(self):
         c = Collector(self.config, outputs=[self.dmx0, self.udmx])
-        
+
         c.setAttrs('client1', 'sess1', [
             (DEV['inst1'], L9['brightness'], .5),
             (DEV['inst1'], L9['brightness'], .3),
@@ -241,6 +250,5 @@
             (DEV['inst1'], L9['brightness'], .3),
             (DEV['inst1'], L9['brightness'], .5),
         ], t0)
-        self.assertEqual([[127, 0, 0, 0], 'flush',
-                          [127, 0, 0, 0], 'flush'], self.dmx0.updates)
-
+        self.assertEqual([[127, 0, 0, 0], 'flush', [127, 0, 0, 0], 'flush'],
+                         self.dmx0.updates)
--- a/light9/collector/device.py	Tue May 21 23:55:35 2019 +0000
+++ b/light9/collector/device.py	Tue May 21 23:56:12 2019 +0000
@@ -19,7 +19,8 @@
      device attrs:
        color
     """
-        
+
+
 class Mini15(Device):
     """
     plan:
@@ -31,14 +32,18 @@
         goboShake
         imageAim (configured with a file of calibration data)
     """
+
+
 def clamp255(x):
     return min(255, max(0, x))
-    
+
+
 def _8bit(f):
     if not isinstance(f, (int, float)):
         raise TypeError(repr(f))
     return clamp255(int(f * 255))
 
+
 def resolve(deviceType, deviceAttr, values):
     """
     return one value to use for this attr, given a set of them that
@@ -66,6 +71,7 @@
         return Literal(sum(floatVals) / len(floatVals))
     return max(values)
 
+
 def toOutputAttrs(deviceType, deviceAttrSettings):
     """
     Given device attr settings like {L9['color']: Literal('#ff0000')},
@@ -74,6 +80,7 @@
 
     :outputAttrRange happens before we get here.
     """
+
     def floatAttr(attr, default=0):
         out = deviceAttrSettings.get(attr)
         if out is None:
@@ -86,12 +93,10 @@
         return r, g, b
 
     def cmyAttr(attr):
-        rgb = sRGBColor.new_from_rgb_hex(deviceAttrSettings.get(attr, '#000000'))
+        rgb = sRGBColor.new_from_rgb_hex(deviceAttrSettings.get(
+            attr, '#000000'))
         out = colormath.color_conversions.convert_color(rgb, CMYColor)
-        return (
-            _8bit(out.cmy_c),
-            _8bit(out.cmy_m),
-            _8bit(out.cmy_y))
+        return (_8bit(out.cmy_c), _8bit(out.cmy_m), _8bit(out.cmy_y))
 
     def fine16Attr(attr, scale=1.0):
         x = floatAttr(attr) * scale
@@ -106,20 +111,15 @@
         if deviceAttrSettings.get(attr) == L9['g2']:
             return 10
         return 0
-        
+
     if deviceType == L9['ChauvetColorStrip']:
         r, g, b = rgbAttr(L9['color'])
-        return {
-            L9['mode']: 215,
-            L9['red']: r,
-            L9['green']: g,
-            L9['blue']: b
-            }
+        return {L9['mode']: 215, L9['red']: r, L9['green']: g, L9['blue']: b}
     elif deviceType == L9['SimpleDimmer']:
         return {L9['level']: _8bit(floatAttr(L9['brightness']))}
     elif deviceType == L9['Mini15']:
         out = {
-            L9['rotationSpeed']: 0, # seems to have no effect
+            L9['rotationSpeed']: 0,  # seems to have no effect
             L9['dimmer']: 255,
             L9['colorChange']: 0,
             L9['colorSpeed']: 0,
@@ -131,17 +131,18 @@
             L9['mini15Gobo1']: 10,
             L9['mini15Gobo2']: 20,
             L9['mini15Gobo3']: 30,
-            }[deviceAttrSettings.get(L9['mini15GoboChoice'], L9['open'])]
-        
+        }[deviceAttrSettings.get(L9['mini15GoboChoice'], L9['open'])]
+
         out[L9['red']], out[L9['green']], out[L9['blue']] = rgbAttr(L9['color'])
-        out[L9['xRotation']], out[L9['xFine']] = fine16Attr(L9['rx'], 1/540)
-        out[L9['yRotation']], out[L9['yFine']] = fine16Attr(L9['ry'], 1/240)
+        out[L9['xRotation']], out[L9['xFine']] = fine16Attr(L9['rx'], 1 / 540)
+        out[L9['yRotation']], out[L9['yFine']] = fine16Attr(L9['ry'], 1 / 240)
         # didn't find docs on this, but from tests it looks like 64 fine steps takes you to the next coarse step
 
         return out
     elif deviceType == L9['ChauvetHex12']:
         out = {}
-        out[L9['red']], out[L9['green']], out[L9['blue']] = r, g, b = rgbAttr(L9['color'])
+        out[L9['red']], out[L9['green']], out[L9['blue']] = r, g, b = rgbAttr(
+            L9['color'])
         out[L9['amber']] = 0
         out[L9['white']] = min(r, g, b)
         out[L9['uv']] = _8bit(floatAttr(L9['uv']))
@@ -153,7 +154,7 @@
         out[L9['fixed255']] = 255
         for num in range(7):
             out[L9['fixed128_%s' % num]] = 128
-        return out        
+        return out
     elif deviceType == L9['MacAura']:
         out = {
             L9['shutter']: 22,
@@ -184,21 +185,23 @@
         out = {
             L9['dimmerFadeLo']: 0,
             L9['fixtureControl']: 0,
-            L9['fx1Select']:  0,
-            L9['fx1Adjust']:  0,
-            L9['fx2Select']:  0,
-            L9['fx2Adjust']:  0,
-            L9['fxSync']:  0,            
-            }
+            L9['fx1Select']: 0,
+            L9['fx1Adjust']: 0,
+            L9['fx2Select']: 0,
+            L9['fx2Adjust']: 0,
+            L9['fxSync']: 0,
+        }
 
         # note these values are set to 'fade', so they update slowly. Haven't found where to turn that off.
-        out[L9['cyan']], out[L9['magenta']], out[L9['yellow']] = cmyAttr(L9['color'])
-        
+        out[L9['cyan']], out[L9['magenta']], out[L9['yellow']] = cmyAttr(
+            L9['color'])
+
         out[L9['focusHi']], out[L9['focusLo']] = fine16Attr(L9['focus'])
         out[L9['panHi']], out[L9['panLo']] = fine16Attr(L9['rx'])
         out[L9['tiltHi']], out[L9['tiltLo']] = fine16Attr(L9['ry'])
         out[L9['zoomHi']], out[L9['zoomLo']] = fine16Attr(L9['zoom'])
-        out[L9['dimmerFadeHi']] = 0 if deviceAttrSettings.get(L9['color'], '#000000') == '#000000' else 255
+        out[L9['dimmerFadeHi']] = 0 if deviceAttrSettings.get(
+            L9['color'], '#000000') == '#000000' else 255
 
         out[L9['goboChoice']] = {
             L9['open']: 0,
@@ -208,7 +211,7 @@
             L9['brush']: 51,
             L9['whirlpool']: 56,
             L9['stars']: 61,
-            }[deviceAttrSettings.get(L9['quantumGoboChoice'], L9['open'])]
+        }[deviceAttrSettings.get(L9['quantumGoboChoice'], L9['open'])]
 
         # my goboSpeed deviceAttr goes 0=stopped to 1=fastest (using one direction only)
         x = .5 + .5 * floatAttr(L9['goboSpeed'])
@@ -220,13 +223,13 @@
             out[L9['shutter']] = 30
         else:
             out[L9['shutter']] = 50 + int(150 * (strobe - .1) / .9)
-        
-        out.update( {
+
+        out.update({
             L9['colorWheel']: 0,
             L9['goboStaticRotate']: 0,
             L9['prismRotation']: _8bit(floatAttr(L9['prism'])),
-            L9['iris']: _8bit(floatAttr(L9['iris']) * (200/255)),
-            })
+            L9['iris']: _8bit(floatAttr(L9['iris']) * (200 / 255)),
+        })
         return out
     else:
         raise NotImplementedError('device %r' % deviceType)
--- a/light9/collector/device_test.py	Tue May 21 23:55:35 2019 +0000
+++ b/light9/collector/device_test.py	Tue May 21 23:56:12 2019 +0000
@@ -4,50 +4,67 @@
 
 from light9.collector.device import toOutputAttrs, resolve
 
+
 class TestUnknownDevice(unittest.TestCase):
+
     def testFails(self):
         self.assertRaises(NotImplementedError, toOutputAttrs, L9['bogus'], {})
 
+
 class TestColorStrip(unittest.TestCase):
+
     def testConvertDeviceToOutputAttrs(self):
         out = toOutputAttrs(L9['ChauvetColorStrip'],
                             {L9['color']: Literal('#ff0000')})
-        self.assertEqual({L9['mode']: 215,
-                          L9['red']: 255,
-                          L9['green']: 0,
-                          L9['blue']: 0
-                      }, out)
-        
+        self.assertEqual(
+            {
+                L9['mode']: 215,
+                L9['red']: 255,
+                L9['green']: 0,
+                L9['blue']: 0
+            }, out)
+
+
 class TestDimmer(unittest.TestCase):
+
     def testConvert(self):
         self.assertEqual({L9['level']: 127},
-                         toOutputAttrs(L9['SimpleDimmer'], {L9['brightness']: .5}))
+                         toOutputAttrs(L9['SimpleDimmer'],
+                                       {L9['brightness']: .5}))
+
 
 class TestMini15(unittest.TestCase):
+
     def testConvertColor(self):
         out = toOutputAttrs(L9['Mini15'], {L9['color']: '#010203'})
         self.assertEqual(255, out[L9['dimmer']])
         self.assertEqual(1, out[L9['red']])
         self.assertEqual(2, out[L9['green']])
         self.assertEqual(3, out[L9['blue']])
+
     def testConvertRotation(self):
-        out = toOutputAttrs(L9['Mini15'], {L9['rx']: Literal(90), L9['ry']: Literal(45)})
+        out = toOutputAttrs(L9['Mini15'], {
+            L9['rx']: Literal(90),
+            L9['ry']: Literal(45)
+        })
         self.assertEqual(42, out[L9['xRotation']])
         self.assertEqual(127, out[L9['xFine']])
         self.assertEqual(47, out[L9['yRotation']])
         self.assertEqual(207, out[L9['yFine']])
         self.assertEqual(0, out[L9['rotationSpeed']])
-        
+
+
 class TestResolve(unittest.TestCase):
+
     def testMaxes1Color(self):
         # do not delete - this one catches a bug in the rgb_to_hex(...) lines
-        self.assertEqual('#ff0300',
-                         resolve(None, L9['color'], ['#ff0300']))
+        self.assertEqual('#ff0300', resolve(None, L9['color'], ['#ff0300']))
+
     def testMaxes2Colors(self):
         self.assertEqual('#ff0400',
                          resolve(None, L9['color'], ['#ff0300', '#000400']))
+
     def testMaxes3Colors(self):
-        self.assertEqual('#112233',
-                         resolve(None, L9['color'],
-                                 ['#110000', '#002200', '#000033']))
-        
+        self.assertEqual(
+            '#112233',
+            resolve(None, L9['color'], ['#110000', '#002200', '#000033']))
--- a/light9/collector/output.py	Tue May 21 23:55:35 2019 +0000
+++ b/light9/collector/output.py	Tue May 21 23:56:12 2019 +0000
@@ -8,6 +8,7 @@
 from greplin import scales
 log = logging.getLogger('output')
 
+
 # eliminate this: lists are always padded now
 def setListElem(outList, index, value, fill=0, combine=lambda old, new: new):
     if len(outList) < index:
@@ -17,6 +18,7 @@
     else:
         outList[index] = combine(outList[index], value)
 
+
 class Output(object):
     """
     send an array of values to some output device. Call update as
@@ -25,9 +27,10 @@
     """
     uri = None  # type: URIRef
     numChannels = None  # type: int
+
     def __init__(self):
         raise NotImplementedError
-        
+
     def allConnections(self):
         """
         sequence of (index, uri) for the uris we can output, and which
@@ -35,7 +38,6 @@
         """
         raise NotImplementedError
 
-    
     def update(self, values):
         """
         output takes a flattened list of values, maybe dmx channels, or
@@ -53,11 +55,13 @@
         """short string to distinguish outputs"""
         raise NotImplementedError
 
+
 class DummyOutput(Output):
+
     def __init__(self, uri, numChannels=1, **kw):
         self.uri = uri
         self.numChannels = numChannels
-    
+
     def update(self, values):
         pass
 
@@ -67,8 +71,9 @@
     def shortId(self):
         return 'null'
 
-        
+
 class DmxOutput(Output):
+
     def __init__(self, uri, numChannels):
         self.uri = uri
         self.numChannels = numChannels
@@ -85,16 +90,14 @@
                 self.countError()
             else:
                 self.lastSentBuffer = sendingBuffer
-            reactor.callLater(max(0, start + 1/20 - time.time()),
-                              self._loop)
+            reactor.callLater(max(0, start + 1 / 20 - time.time()), self._loop)
 
         d = threads.deferToThread(self.sendDmx, sendingBuffer)
         d.addCallback(done)
-        
+
 
 class EnttecDmx(DmxOutput):
-    stats = scales.collection('/output/enttecDmx',
-                              scales.PmfStat('write'),
+    stats = scales.collection('/output/enttecDmx', scales.PmfStat('write'),
                               scales.PmfStat('update'))
 
     def __init__(self, uri, devicePath='/dev/dmx0', numChannels=80):
@@ -128,16 +131,16 @@
     def shortId(self):
         return 'enttec'
 
-                                  
+
 class Udmx(DmxOutput):
-    stats = scales.collection('/output/udmx',
-                              scales.PmfStat('update'),
+    stats = scales.collection('/output/udmx', scales.PmfStat('update'),
                               scales.PmfStat('write'),
                               scales.IntStat('usbErrors'))
+
     def __init__(self, uri, bus, numChannels):
         DmxOutput.__init__(self, uri, numChannels)
         self._shortId = self.uri.rstrip('/')[-1]
-        
+
         from light9.io.udmx import Udmx
         self.dev = Udmx(bus)
         self.currentBuffer = ''
@@ -173,14 +176,14 @@
             except usb.core.USBError as e:
                 # not in main thread
                 if e.errno != 75:
-                  msg = 'usb: sending %s bytes to %r; error %r' % (len(buf), self.uri, e)
-                  print msg
+                    msg = 'usb: sending %s bytes to %r; error %r' % (
+                        len(buf), self.uri, e)
+                    print msg
                 return False
 
     def countError(self):
         # in main thread
         Udmx.stats.usbErrors += 1
-        
+
     def shortId(self):
         return self._shortId
-
--- a/light9/collector/output_test.py	Tue May 21 23:55:35 2019 +0000
+++ b/light9/collector/output_test.py	Tue May 21 23:56:12 2019 +0000
@@ -2,38 +2,47 @@
 from light9.namespaces import L9
 from light9.collector.output import setListElem, DmxOutput
 
+
 class TestSetListElem(unittest.TestCase):
+
     def testSetExisting(self):
         x = [0, 1]
         setListElem(x, 0, 9)
         self.assertEqual([9, 1], x)
+
     def testSetNext(self):
         x = [0, 1]
         setListElem(x, 2, 9)
         self.assertEqual([0, 1, 9], x)
+
     def testSetBeyond(self):
         x = [0, 1]
         setListElem(x, 3, 9)
         self.assertEqual([0, 1, 0, 9], x)
+
     def testArbitraryFill(self):
         x = [0, 1]
         setListElem(x, 5, 9, fill=8)
         self.assertEqual([0, 1, 8, 8, 8, 9], x)
+
     def testSetZero(self):
         x = [0, 1]
         setListElem(x, 5, 0)
         self.assertEqual([0, 1, 0, 0, 0, 0], x)
+
     def testCombineMax(self):
         x = [0, 1]
         setListElem(x, 1, 0, combine=max)
         self.assertEqual([0, 1], x)
+
     def testCombineHasNoEffectOnNewElems(self):
         x = [0, 1]
         setListElem(x, 2, 1, combine=max)
         self.assertEqual([0, 1, 1], x)
-        
+
+
 class TestDmxOutput(unittest.TestCase):
+
     def testFlushIsNoop(self):
         out = DmxOutput(L9['output/udmx/'], 3)
         out.flush()
-        
--- a/light9/curvecalc/client.py	Tue May 21 23:55:35 2019 +0000
+++ b/light9/curvecalc/client.py	Tue May 21 23:56:12 2019 +0000
@@ -6,16 +6,19 @@
 import urllib
 from run_local import log
 
+
 def sendLiveInputPoint(curve, value):
-    f = cyclone.httpclient.fetch(
-        networking.curveCalc.path('liveInputPoint'),
-        method='POST', timeout=1,
-        postdata=urllib.urlencode({
-            'curve': curve,
-            'value': str(value),
-        }))
+    f = cyclone.httpclient.fetch(networking.curveCalc.path('liveInputPoint'),
+                                 method='POST',
+                                 timeout=1,
+                                 postdata=urllib.urlencode({
+                                     'curve': curve,
+                                     'value': str(value),
+                                 }))
+
     @f.addCallback
     def cb(result):
         if result.code // 100 != 2:
             raise ValueError("curveCalc said %s: %s", result.code, result.body)
+
     return f
--- a/light9/curvecalc/cursors.py	Tue May 21 23:55:35 2019 +0000
+++ b/light9/curvecalc/cursors.py	Tue May 21 23:56:12 2019 +0000
@@ -1,14 +1,16 @@
-
 import logging
 log = logging.getLogger("cursors")
 
 # accept ascii images, read file images, add hotspots, read xbm as
 # cursor with @filename form
 
-_pushed = {} # widget : [old, .., newest]
-def push(widget,new_cursor):
+_pushed = {}  # widget : [old, .., newest]
+
+
+def push(widget, new_cursor):
     global _pushed
-    _pushed.setdefault(widget,[]).append(widget.cget("cursor"))
+    _pushed.setdefault(widget, []).append(widget.cget("cursor"))
+
 
 def pop(widget):
     global _pushed
@@ -18,5 +20,3 @@
         log.debug("cursor pop from empty stack")
         return
     widget.config(cursor=c)
-    
-    
--- a/light9/curvecalc/curve.py	Tue May 21 23:55:35 2019 +0000
+++ b/light9/curvecalc/curve.py	Tue May 21 23:56:12 2019 +0000
@@ -1,6 +1,6 @@
 from __future__ import division
 import glob, time, logging, ast, os
-from bisect import bisect_left,bisect
+from bisect import bisect_left, bisect
 import louie as dispatcher
 from twisted.internet import reactor
 from rdflib import Literal
@@ -13,12 +13,14 @@
 introPad = 4
 postPad = 4
 
+
 class Curve(object):
     """curve does not know its name. see Curveset"""
+
     def __init__(self, uri, pointsStorage='graph'):
         self.uri = uri
         self.pointsStorage = pointsStorage
-        self.points = [] # x-sorted list of (x,y)
+        self.points = []  # x-sorted list of (x,y)
         self._muted = False
 
     def __repr__(self):
@@ -27,24 +29,28 @@
 
     def muted():
         doc = "Whether to currently send levels (boolean, obviously)"
+
         def fget(self):
             return self._muted
+
         def fset(self, val):
             self._muted = val
             dispatcher.send('mute changed', sender=self)
+
         return locals()
+
     muted = property(**muted())
 
     def toggleMute(self):
         self.muted = not self.muted
 
-    def load(self,filename):
-        self.points[:]=[]
+    def load(self, filename):
+        self.points[:] = []
         for line in file(filename):
             x, y = line.split()
             self.points.append((float(x), ast.literal_eval(y)))
         self.points.sort()
-        dispatcher.send("points changed",sender=self)
+        dispatcher.send("points changed", sender=self)
 
     def set_from_string(self, pts):
         self.points[:] = []
@@ -53,22 +59,24 @@
         for x, y in pairs:
             self.points.append((float(x), ast.literal_eval(y)))
         self.points.sort()
-        dispatcher.send("points changed",sender=self)
+        dispatcher.send("points changed", sender=self)
 
     def points_as_string(self):
+
         def outVal(x):
-            if isinstance(x, basestring): # markers
+            if isinstance(x, basestring):  # markers
                 return x
             return "%.4g" % x
-        return ' '.join("%s %s" % (outVal(p[0]), outVal(p[1]))
-                        for p in self.points)
-        
-    def save(self,filename):
+
+        return ' '.join(
+            "%s %s" % (outVal(p[0]), outVal(p[1])) for p in self.points)
+
+    def save(self, filename):
         # this is just around for markers, now
         if filename.endswith('-music') or filename.endswith('_music'):
             print "not saving music track"
             return
-        f = file(filename,'w')
+        f = file(filename, 'w')
         for p in self.points:
             f.write("%s %r\n" % p)
         f.close()
@@ -78,25 +86,26 @@
             return 0
         if not self.points:
             raise ValueError("curve has no points")
-        i = bisect_left(self.points,(t,None))-1
+        i = bisect_left(self.points, (t, None)) - 1
 
         if i == -1:
             return self.points[0][1]
-        if self.points[i][0]>t:
+        if self.points[i][0] > t:
             return self.points[i][1]
-        if i>=len(self.points)-1:
+        if i >= len(self.points) - 1:
             return self.points[i][1]
 
-        p1,p2 = self.points[i],self.points[i+1]
-        frac = (t-p1[0])/(p2[0]-p1[0])
-        y = p1[1]+(p2[1]-p1[1])*frac
+        p1, p2 = self.points[i], self.points[i + 1]
+        frac = (t - p1[0]) / (p2[0] - p1[0])
+        y = p1[1] + (p2[1] - p1[1]) * frac
         return y
-    __call__=eval
+
+    __call__ = eval
 
     def insert_pt(self, new_pt):
         """returns index of new point"""
-        i = bisect(self.points, (new_pt[0],None))
-        self.points.insert(i,new_pt)
+        i = bisect(self.points, (new_pt[0], None))
+        self.points.insert(i, new_pt)
         # missing a check that this isn't the same X as the neighbor point
         dispatcher.send("points changed", sender=self)
         return i
@@ -109,7 +118,7 @@
         self.insert_pt(new_pt)
         dispatcher.send("points changed", sender=self)
         # now simplify to the left
-        
+
     def set_points(self, updates):
         for i, pt in updates:
             self.points[i] = pt
@@ -118,7 +127,7 @@
         # lot. need a new solution.
         #self.checkOverlap()
         dispatcher.send("points changed", sender=self)
-            
+
     def checkOverlap(self):
         x = None
         for p in self.points:
@@ -130,20 +139,20 @@
         p = self.points.pop(i)
         dispatcher.send("points changed", sender=self)
         return p
-            
+
     def remove_point(self, pt):
         self.points.remove(pt)
         dispatcher.send("points changed", sender=self)
-            
+
     def indices_between(self, x1, x2, beyond=0):
-        leftidx = max(0, bisect(self.points, (x1,None)) - beyond)
+        leftidx = max(0, bisect(self.points, (x1, None)) - beyond)
         rightidx = min(len(self.points),
-                       bisect(self.points, (x2,None)) + beyond)
+                       bisect(self.points, (x2, None)) + beyond)
         return range(leftidx, rightidx)
-        
+
     def points_between(self, x1, x2):
         """returns (x,y) points"""
-        return [self.points[i] for i in self.indices_between(x1,x2)]
+        return [self.points[i] for i in self.indices_between(x1, x2)]
 
     def point_before(self, x):
         """(x,y) of the point left of x, or None"""
@@ -153,15 +162,17 @@
         return self.points[leftidx]
 
     def index_before(self, x):
-        leftidx = bisect(self.points, (x,None)) - 1
+        leftidx = bisect(self.points, (x, None)) - 1
         if leftidx < 0:
             return None
         return leftidx
 
+
 class CurveResource(object):
     """
     holds a Curve, deals with graphs
     """
+
     def __init__(self, graph, uri):
         # probably newCurve and loadCurve should be the constructors instead.
         self.graph, self.uri = graph, uri
@@ -177,15 +188,16 @@
         if hasattr(self, 'curve'):
             raise ValueError('CurveResource already has a curve %r' %
                              self.curve)
-        self.graph.patch(Patch(addQuads=[
-            (self.uri, RDF.type, L9['Curve'], ctx),
-            (self.uri, RDFS.label, label, ctx),
+        self.graph.patch(
+            Patch(addQuads=[
+                (self.uri, RDF.type, L9['Curve'], ctx),
+                (self.uri, RDFS.label, label, ctx),
             ]))
         self.curve = Curve(self.uri)
         self.curve.points.extend([(0, 0)])
         self.saveCurve()
         self.watchCurvePointChanges()
-        
+
     def loadCurve(self):
         if hasattr(self, 'curve'):
             raise ValueError('CurveResource already has a curve %r' %
@@ -198,7 +210,7 @@
         else:
             # given a currentState graph
             self.pointsFromGraph()
-        
+
     def pointsFromGraph(self):
         pts = self.graph.value(self.uri, L9['points'])
         if pts is not None:
@@ -222,18 +234,20 @@
             #cur.save("%s-%s" % (basename,name))
             return []
         elif self.curve.pointsStorage == 'graph':
-            return [self.graph.getObjectPatch(
-                self.curvePointsContext(),
-                subject=self.uri,
-                predicate=L9['points'],
-                newObject=Literal(self.curve.points_as_string()))]
+            return [
+                self.graph.getObjectPatch(self.curvePointsContext(),
+                                          subject=self.uri,
+                                          predicate=L9['points'],
+                                          newObject=Literal(
+                                              self.curve.points_as_string()))
+            ]
         else:
             raise NotImplementedError(self.curve.pointsStorage)
 
     def watchCurvePointChanges(self):
         """start watching and saving changes to the graph"""
         dispatcher.connect(self.onChange, 'points changed', sender=self.curve)
-        
+
     def onChange(self):
 
         # Don't write a patch for the edited curve points until they've been
@@ -243,31 +257,34 @@
         # this is just the wrong timing algorithm- it should be a max rate,
         # not a max-hold-still-time.
         HOLD_POINTS_GRAPH_COMMIT_SECS = .1
-        
+
         if getattr(self, 'pendingSave', None):
             self.pendingSave.cancel()
         self.pendingSave = reactor.callLater(HOLD_POINTS_GRAPH_COMMIT_SECS,
                                              self.saveCurve)
-        
+
+
 class Markers(Curve):
     """Marker is like a point but the y value is a string"""
+
     def eval(self):
         raise NotImplementedError()
 
 
-def slope(p1,p2):
+def slope(p1, p2):
     if p2[0] == p1[0]:
         return 0
     return (p2[1] - p1[1]) / (p2[0] - p1[0])
 
-       
+
 class Curveset(object):
+
     def __init__(self, graph, session):
         self.graph, self.session = graph, session
 
         self.currentSong = None
-        self.curveResources = {} # uri : CurveResource
-        
+        self.curveResources = {}  # uri : CurveResource
+
         self.markers = Markers(uri=None, pointsStorage='file')
 
         graph.addHandler(self.loadCurvesForSong)
@@ -285,7 +302,7 @@
         dispatcher.send("clear_curves")
         self.curveResources.clear()
         self.markers = Markers(uri=None, pointsStorage='file')
-        
+
         self.currentSong = self.graph.value(self.session, L9['currentSong'])
         if self.currentSong is None:
             return
@@ -298,11 +315,14 @@
                 curvename = self.graph.label(uri)
                 if not curvename:
                     raise ValueError("curve %r has no label" % uri)
-                dispatcher.send("add_curve", sender=self,
-                                uri=uri, label=curvename, curve=cr.curve)
+                dispatcher.send("add_curve",
+                                sender=self,
+                                uri=uri,
+                                label=curvename,
+                                curve=cr.curve)
             except Exception as e:
                 log.error("loading %s failed: %s", uri, e)
-                
+
         basename = os.path.join(
             showconfig.curvesDir(),
             showconfig.songFilenameFromURI(self.currentSong))
@@ -314,7 +334,7 @@
     def save(self):
         """writes a file for each curve with a name
         like basename-curvename, or saves them to the rdf graph"""
-        basename=os.path.join(
+        basename = os.path.join(
             showconfig.curvesDir(),
             showconfig.songFilenameFromURI(self.currentSong))
 
@@ -326,23 +346,23 @@
         # this will cause reloads that will rebuild our curve list
         for p in patches:
             self.graph.patch(p)
-            
+
     def sorter(self, name):
         return self.curves[name].uri
-        
+
     def curveUrisInOrder(self):
         return sorted(self.curveResources.keys())
 
     def currentCurves(self):
         # deprecated
         for uri, cr in sorted(self.curveResources.items()):
-            with self.graph.currentState(
-                    tripleFilter=(uri, RDFS['label'], None)) as g:
+            with self.graph.currentState(tripleFilter=(uri, RDFS['label'],
+                                                       None)) as g:
                 yield uri, g.label(uri), cr.curve
-        
+
     def globalsdict(self):
         raise NotImplementedError('subterm used to get a dict of name:curve')
-    
+
     def get_time_range(self):
         return 0, dispatcher.send("get max time")[0][1]
 
@@ -358,8 +378,8 @@
         cr.curve.points.extend([(s, 0), (e, 0)])
 
         ctx = self.currentSong
-        self.graph.patch(Patch(addQuads=[
-            (self.currentSong, L9['curve'], uri, ctx),
+        self.graph.patch(
+            Patch(addQuads=[
+                (self.currentSong, L9['curve'], uri, ctx),
             ]))
         cr.saveCurve()
-
--- a/light9/curvecalc/curveedit.py	Tue May 21 23:55:35 2019 +0000
+++ b/light9/curvecalc/curveedit.py	Tue May 21 23:56:12 2019 +0000
@@ -9,40 +9,46 @@
 from run_local import log
 from louie import dispatcher
 
+
 def serveCurveEdit(port, hoverTimeResponse, curveset):
     """
     /hoverTime requests actually are handled by the curvecalc gui
     """
     curveEdit = CurveEdit(curveset)
-    
+
     class HoverTime(PrettyErrorHandler, cyclone.web.RequestHandler):
+
         def get(self):
             hoverTimeResponse(self)
 
     class LiveInputPoint(PrettyErrorHandler, cyclone.web.RequestHandler):
+
         def post(self):
             params = cgi.parse_qs(self.request.body)
             curve = URIRef(params['curve'][0])
             value = float(params['value'][0])
             curveEdit.liveInputPoint(curve, value)
             self.set_status(204)
-            
-    reactor.listenTCP(port, cyclone.web.Application(handlers=[
-        (r'/hoverTime', HoverTime),
-        (r'/liveInputPoint', LiveInputPoint),
-        ], debug=True))
 
-    
+    reactor.listenTCP(
+        port,
+        cyclone.web.Application(handlers=[
+            (r'/hoverTime', HoverTime),
+            (r'/liveInputPoint', LiveInputPoint),
+        ],
+                                debug=True))
+
+
 class CurveEdit(object):
+
     def __init__(self, curveset):
         self.curveset = curveset
         dispatcher.connect(self.inputTime, "input time")
         self.currentTime = 0
-        
+
     def inputTime(self, val):
         self.currentTime = val
-        
+
     def liveInputPoint(self, curveUri, value):
         curve = self.curveset.curveFromUri(curveUri)
         curve.live_input_point((self.currentTime, value), clear_ahead_secs=.5)
-        
--- a/light9/curvecalc/curveview.py	Tue May 21 23:55:35 2019 +0000
+++ b/light9/curvecalc/curveview.py	Tue May 21 23:56:12 2019 +0000
@@ -13,44 +13,48 @@
 
 log = logging.getLogger()
 print "curveview.py toplevel"
+
+
 def vlen(v):
-    return math.sqrt(v[0]*v[0] + v[1]*v[1])
+    return math.sqrt(v[0] * v[0] + v[1] * v[1])
+
 
 def angle_between(base, p0, p1):
     p0 = p0[0] - base[0], p0[1] - base[1]
     p1 = p1[0] - base[0], p1[1] - base[1]
-    p0 = [x/vlen(p0) for x in p0]
-    p1 = [x/vlen(p1) for x in p1]
-    dot = p0[0]*p1[0]+p0[1]*p1[1]
-    dot = max(-1,min(1,dot))
+    p0 = [x / vlen(p0) for x in p0]
+    p1 = [x / vlen(p1) for x in p1]
+    dot = p0[0] * p1[0] + p0[1] * p1[1]
+    dot = max(-1, min(1, dot))
     return math.degrees(math.acos(dot))
 
+
 class Sketch:
     """a sketch motion on a curveview, with temporary points while you
     draw, and simplification when you release"""
-    
-    def __init__(self,curveview,ev):
+
+    def __init__(self, curveview, ev):
         self.curveview = curveview
         self.pts = []
         self.last_x = None
 
-    def motion(self,ev):
+    def motion(self, ev):
         p = self.curveview.world_from_screen(ev.x, ev.y)
-        p = p[0], max(0,min(1,p[1]))
+        p = p[0], max(0, min(1, p[1]))
         if self.last_x is not None and abs(ev.x - self.last_x) < 4:
             return
         self.last_x = ev.x
         self.pts.append(p)
         self.curveview.add_point(p)
 
-    def release(self,ev):
+    def release(self, ev):
         pts = self.pts
         pts.sort()
         finalPoints = pts[:]
 
         dx = .01
         to_remove = []
-        for i in range(1,len(pts)-1):
+        for i in range(1, len(pts) - 1):
             x = pts[i][0]
 
             p_left = (x - dx, self.curveview.curve(x - dx))
@@ -71,19 +75,21 @@
             if abs(self.curveview.curve(p[0]) - p[1]) > .1:
                 self.curveview.add_point(p)
                 finalPoints.append(p)
-            
+
         self.curveview.update_curve()
         self.curveview.select_points(finalPoints)
 
+
 class SelectManip(object):
     """
     selection manipulator is created whenever you have a selection. It
     draws itself on the canvas and edits the points when you drag
     various parts
     """
+
     def __init__(self, parent, getSelectedIndices, getWorldPoint,
-                 getScreenPoint, getCanvasHeight, setPoints,
-                 getWorldTime, getWorldValue, getDragRange):
+                 getScreenPoint, getCanvasHeight, setPoints, getWorldTime,
+                 getWorldValue, getDragRange):
         """parent goocanvas group"""
         self.getSelectedIndices = getSelectedIndices
         self.getWorldPoint = getWorldPoint
@@ -94,34 +100,42 @@
         self.getDragRange = getDragRange
         self.getWorldValue = getWorldValue
         self.grp = GooCanvas.CanvasGroup(parent=parent)
-        
-        self.title = GooCanvas.CanvasText(parent=self.grp, text="selectmanip",
-                                    x=10, y=10, fill_color='white', font="ubuntu 10")
+
+        self.title = GooCanvas.CanvasText(parent=self.grp,
+                                          text="selectmanip",
+                                          x=10,
+                                          y=10,
+                                          fill_color='white',
+                                          font="ubuntu 10")
 
         self.bbox = GooCanvas.CanvasRect(parent=self.grp,
-                                   fill_color_rgba=0xffff0030,
-                                   line_width=0)
+                                         fill_color_rgba=0xffff0030,
+                                         line_width=0)
 
-        self.xTrans = polyline_new_line(parent=self.grp, close_path=True,
-                                         fill_color_rgba=0xffffff88,
-                                         )
-        self.centerScale = polyline_new_line(parent=self.grp, close_path=True,
-                                              fill_color_rgba=0xffffff88,
-                                         )
+        self.xTrans = polyline_new_line(
+            parent=self.grp,
+            close_path=True,
+            fill_color_rgba=0xffffff88,
+        )
+        self.centerScale = polyline_new_line(
+            parent=self.grp,
+            close_path=True,
+            fill_color_rgba=0xffffff88,
+        )
 
-        thickLine = lambda: polyline_new_line(parent=self.grp,
-                                               stroke_color_rgba=0xffffccff,
-                                               line_width=6)
+        thickLine = lambda: polyline_new_line(
+            parent=self.grp, stroke_color_rgba=0xffffccff, line_width=6)
         self.leftScale = thickLine()
         self.rightScale = thickLine()
         self.topScale = thickLine()
-        
-        for grp, name in [(self.xTrans, 'x'),
-                          (self.leftScale, 'left'),
-                          (self.rightScale, 'right'),
-                          (self.topScale, 'top'),
-                          (self.centerScale, 'centerScale'),
-                          ]:
+
+        for grp, name in [
+            (self.xTrans, 'x'),
+            (self.leftScale, 'left'),
+            (self.rightScale, 'right'),
+            (self.topScale, 'top'),
+            (self.centerScale, 'centerScale'),
+        ]:
             grp.connect("button-press-event", self.onPress, name)
             grp.connect("button-release-event", self.onRelease, name)
             grp.connect("motion-notify-event", self.onMotion, name)
@@ -133,10 +147,10 @@
     def onEnter(self, item, target_item, event, param):
         self.prevColor = item.props.stroke_color_rgba
         item.props.stroke_color_rgba = 0xff0000ff
-        
+
     def onLeave(self, item, target_item, event, param):
         item.props.stroke_color_rgba = self.prevColor
-        
+
     def onPress(self, item, target_item, event, param):
         self.dragStartTime = self.getWorldTime(event.x)
         idxs = self.getSelectedIndices()
@@ -147,7 +161,7 @@
 
         if param == 'centerScale':
             self.maxPointMove = min(moveLeft, moveRight)
-        
+
         self.dragRange = (self.dragStartTime - moveLeft,
                           self.dragStartTime + moveRight)
         return True
@@ -165,51 +179,47 @@
 
             def clamp(x, lo, hi):
                 return max(lo, min(hi, x))
-            
+
             mouseT = self.getWorldTime(event.x)
             clampedT = clamp(mouseT, clampLo + dontCross, clampHi - dontCross)
 
             dt = clampedT - self.dragStartTime
 
             if param == 'x':
-                self.setPoints((i, (orig[0] + dt, orig[1]))
-                               for i, orig in origPts)
+                self.setPoints(
+                    (i, (orig[0] + dt, orig[1])) for i, orig in origPts)
             elif param == 'left':
-                self.setPoints((
-                    i,
-                    (left + dt +
-                     (orig[0] - left) / width *
-                     clamp(width - dt, dontCross, right - clampLo - dontCross),
-                     orig[1])) for i, orig in origPts)
+                self.setPoints(
+                    (i,
+                     (left + dt + (orig[0] - left) / width *
+                      clamp(width - dt, dontCross, right - clampLo - dontCross),
+                      orig[1])) for i, orig in origPts)
             elif param == 'right':
-                self.setPoints((
-                    i,
-                    (left +
-                     (orig[0] - left) / width *
-                     clamp(width + dt, dontCross, clampHi - left - dontCross),
-                     orig[1])) for i, orig in origPts)
+                self.setPoints(
+                    (i,
+                     (left + (orig[0] - left) / width *
+                      clamp(width + dt, dontCross, clampHi - left - dontCross),
+                      orig[1])) for i, orig in origPts)
             elif param == 'top':
                 v = self.getWorldValue(event.y)
                 if self.origMaxValue == 0:
                     self.setPoints((i, (orig[0], v)) for i, orig in origPts)
                 else:
-                    scl = max(0, min(1 / self.origMaxValue,
-                                     v / self.origMaxValue))
-                    self.setPoints((i, (orig[0], orig[1] * scl))
-                                   for i, orig in origPts)
+                    scl = max(0,
+                              min(1 / self.origMaxValue, v / self.origMaxValue))
+                    self.setPoints(
+                        (i, (orig[0], orig[1] * scl)) for i, orig in origPts)
 
             elif param == 'centerScale':
                 dt = mouseT - self.dragStartTime
                 rad = width / 2
                 tMid = left + rad
                 maxScl = (rad + self.maxPointMove - dontCross) / rad
-                newWidth = max(dontCross / width,
-                               min((rad + dt) / rad, maxScl)) * width
-                self.setPoints((i,
-                                (tMid +
-                                 ((orig[0] - left) / width - .5) * newWidth,
-                                 orig[1])) for i, orig in origPts)
-                
+                newWidth = max(dontCross / width, min(
+                    (rad + dt) / rad, maxScl)) * width
+                self.setPoints(
+                    (i, (tMid + ((orig[0] - left) / width - .5) * newWidth,
+                         orig[1])) for i, orig in origPts)
 
     def onRelease(self, item, target_item, event, param):
         if hasattr(self, 'dragStartTime'):
@@ -220,17 +230,18 @@
         change, call this to redo the layout of the manip"""
         idxs = self.getSelectedIndices()
         pts = [self.getScreenPoint(i) for i in idxs]
-        
+
         b = self.bbox.props
         b.x = min(p[0] for p in pts) - 5
         b.y = min(p[1] for p in pts) - 5
         margin = 10 if len(pts) > 1 else 0
         b.width = max(p[0] for p in pts) - b.x + margin
-        b.height = min(max(p[1] for p in pts) - b.y + margin,
-                       self.getCanvasHeight() - b.y - 1)
+        b.height = min(
+            max(p[1] for p in pts) - b.y + margin,
+            self.getCanvasHeight() - b.y - 1)
 
-        multi = (GooCanvas.CanvasItemVisibility.VISIBLE if len(pts) > 1 else
-                 GooCanvas.CanvasItemVisibility.INVISIBLE)
+        multi = (GooCanvas.CanvasItemVisibility.VISIBLE
+                 if len(pts) > 1 else GooCanvas.CanvasItemVisibility.INVISIBLE)
         b.visibility = multi
         self.leftScale.props.visibility = multi
         self.rightScale.props.visibility = multi
@@ -245,24 +256,21 @@
         midY = self.getCanvasHeight() * .5
         loY = self.getCanvasHeight() * .8
 
-        self.leftScale.props.points = Points([
-            (b.x, b.y), (b.x, b.y + b.height)])
-        self.rightScale.props.points = Points([
-            (b.x + b.width, b.y), (b.x + b.width, b.y + b.height)])
+        self.leftScale.props.points = Points([(b.x, b.y),
+                                              (b.x, b.y + b.height)])
+        self.rightScale.props.points = Points([(b.x + b.width, b.y),
+                                               (b.x + b.width, b.y + b.height)])
 
-        self.topScale.props.points = Points([
-            (b.x, b.y), (b.x + b.width, b.y)])
+        self.topScale.props.points = Points([(b.x, b.y), (b.x + b.width, b.y)])
 
         self.updateXTrans(centerX, midY)
 
-        self.centerScale.props.points = Points([
-            (centerX - 5, loY - 5),
-            (centerX + 5, loY - 5),
-            (centerX + 5, loY + 5),
-            (centerX - 5, loY + 5)])
-            
+        self.centerScale.props.points = Points([(centerX - 5, loY - 5),
+                                                (centerX + 5, loY - 5),
+                                                (centerX + 5, loY + 5),
+                                                (centerX - 5, loY + 5)])
 
-    def updateXTrans(self, centerX, midY):       
+    def updateXTrans(self, centerX, midY):
         x1 = centerX - 30
         x2 = centerX - 20
         x3 = centerX + 20
@@ -272,25 +280,24 @@
         y3 = midY + 5
         y4 = midY + 10
         shape = [
-            (x1, midY), # left tip
+            (x1, midY),  # left tip
             (x2, y1),
             (x2, y2),
-            
             (x3, y2),
             (x3, y1),
-            (x4, midY), # right tip
+            (x4, midY),  # right tip
             (x3, y4),
             (x3, y3),
-            
             (x2, y3),
             (x2, y4)
-            ]
+        ]
 
         self.xTrans.props.points = Points(shape)
 
     def destroy(self):
         self.grp.remove()
 
+
 class Curveview(object):
     """
     graphical curve widget only. Please pack .widget
@@ -306,7 +313,12 @@
     The canvas x1/x2/y1/y2 coords are updated to match self.widget.
     
     """
-    def __init__(self, curve, markers, knobEnabled=False, isMusic=False,
+
+    def __init__(self,
+                 curve,
+                 markers,
+                 knobEnabled=False,
+                 isMusic=False,
                  zoomControl=None):
         """knobEnabled=True highlights the previous key and ties it to a
         hardware knob"""
@@ -315,46 +327,45 @@
         self.knobEnabled = knobEnabled
         self._isMusic = isMusic
         self.zoomControl = zoomControl
-        
+
         self.redrawsEnabled = False
 
         box = self.createOuterWidgets()
         self.canvas = self.createCanvasWidget(box)
         self.trackWidgetSize()
         self.update_curve()
-        
+
         self._time = -999
         self.last_mouse_world = None
-        self.entered = False # is the mouse currently over this widget
-        self.selected_points=[] # idx of points being dragged
+        self.entered = False  # is the mouse currently over this widget
+        self.selected_points = []  # idx of points being dragged
         self.dots = {}
         # self.bind("<Enter>",self.focus)
         dispatcher.connect(self.playPause, "onPlayPause")
         dispatcher.connect(self.input_time, "input time")
         dispatcher.connect(self.update_curve, "zoom changed")
-        dispatcher.connect(self.update_curve, "points changed",
+        dispatcher.connect(self.update_curve,
+                           "points changed",
                            sender=self.curve)
-        dispatcher.connect(self.update_curve, "mute changed", 
-                           sender=self.curve)
+        dispatcher.connect(self.update_curve, "mute changed", sender=self.curve)
         dispatcher.connect(self.select_between, "select between")
         dispatcher.connect(self.acls, "all curves lose selection")
         if self.knobEnabled:
             dispatcher.connect(self.knob_in, "knob in")
             dispatcher.connect(self.slider_in, "set key")
 
-
         # todo: hold control to get a [+] cursor
         #        def curs(ev):
         #            print ev.state
         #        self.bind("<KeyPress>",curs)
         #        self.bind("<KeyRelease-Control_L>",lambda ev: curs(0))
-      
+
         # this binds on c-a-b1, etc
-        if 0: # unported
+        if 0:  # unported
             self.regionzoom = RegionZoom(self, self.world_from_screen,
                                          self.screen_from_world)
 
-        self.sketch = None # an in-progress sketch
+        self.sketch = None  # an in-progress sketch
 
         self.dragging_dots = False
         self.selecting = False
@@ -393,6 +404,7 @@
             size-allocate seems right but i get into infinite bounces
             between two sizes
         """
+
         def sizeEvent(w, alloc):
             p = self.canvas.props
             if (alloc.width, alloc.height) != (p.x2, p.y2):
@@ -401,12 +413,13 @@
                 # calling update_curve in this event usually doesn't work
                 reactor.callLater(0, self.update_curve)
             return False
-            
+
         #self.widget.connect('size-allocate', sizeEvent) # see docstring
 
         def visEvent(w, alloc):
             self.setCanvasToWidgetSize()
             return False
+
         self.widget.add_events(Gdk.EventMask.VISIBILITY_NOTIFY_MASK)
         self.widget.connect('visibility-notify-event', visEvent)
 
@@ -449,15 +462,15 @@
 
     def onAny(self, w, event):
         print "   %s on %s" % (event, w)
-        
+
     def onFocusIn(self, *args):
-        dispatcher.send('curve row focus change')     
+        dispatcher.send('curve row focus change')
         dispatcher.send("all curves lose selection", butNot=self)
 
         self.widget.modify_bg(Gtk.StateFlags.NORMAL, Gdk.color_parse("red"))
 
     def onFocusOut(self, widget=None, event=None):
-        dispatcher.send('curve row focus change')                  
+        dispatcher.send('curve row focus change')
         self.widget.modify_bg(Gtk.StateFlags.NORMAL, Gdk.color_parse("gray30"))
 
         # you'd think i'm unselecting when we lose focus, but we also
@@ -475,8 +488,7 @@
     def onDelete(self):
         if self.selected_points:
             self.remove_point_idx(*self.selected_points)
-        
-            
+
     def onCanvasPress(self, item, target_item, event):
         # when we support multiple curves per canvas, this should find
         # the close one and add a point to that. Binding to the line
@@ -494,7 +506,7 @@
         else:
             self.select_press(event)
 
-        # this stops some other handler that wants to unfocus 
+        # this stops some other handler that wants to unfocus
         return True
 
     def playPause(self):
@@ -563,13 +575,14 @@
                 self.canvas.get_root_item(),
                 getSelectedIndices=lambda: sorted(self.selected_points),
                 getWorldPoint=lambda i: self.curve.points[i],
-                getScreenPoint=lambda i: self.screen_from_world(self.curve.points[i]),
+                getScreenPoint=lambda i: self.screen_from_world(self.curve.
+                                                                points[i]),
                 getWorldTime=lambda x: self.world_from_screen(x, 0)[0],
                 getWorldValue=lambda y: self.world_from_screen(0, y)[1],
                 getCanvasHeight=lambda: self.canvas.props.y2,
                 setPoints=self.setPoints,
                 getDragRange=self.getDragRange,
-                )
+            )
         if not self.selected_points and self.selectManip:
             self.selectManip.destroy()
             self.selectManip = None
@@ -600,12 +613,12 @@
 
     def setPoints(self, updates):
         self.curve.set_points(updates)
-        
+
     def selectionChanged(self):
         if self.selectManip:
             self.selectManip.update()
 
-    def select_press(self,ev):
+    def select_press(self, ev):
         # todo: these select_ handlers are getting called on c-a-drag
         # zooms too. the dispatching should be more selective than
         # just calling both handlers all the time
@@ -614,37 +627,37 @@
             return
         if not self.selecting:
             self.selecting = True
-            self.select_start = self.world_from_screen(ev.x,0)[0]
+            self.select_start = self.world_from_screen(ev.x, 0)[0]
             #cursors.push(self,"gumby")
-        
-    def select_motion(self,ev):
+
+    def select_motion(self, ev):
         if not self.selecting:
             return
         start = self.select_start
         cur = self.world_from_screen(ev.x, 0)[0]
         self.select_between(start, cur)
-        
-    def select_release(self,ev):
+
+    def select_release(self, ev):
         self.print_state("select_release")
 
         # dotrelease never gets called, but I can clear that state here
         self.dragging_dots = False
-        
+
         if not self.selecting:
             return
         #cursors.pop(self)
         self.selecting = False
         self.select_between(self.select_start,
-                            self.world_from_screen(ev.x,0)[0])
+                            self.world_from_screen(ev.x, 0)[0])
 
-    def sketch_press(self,ev):
-        self.sketch = Sketch(self,ev)
+    def sketch_press(self, ev):
+        self.sketch = Sketch(self, ev)
 
-    def sketch_motion(self,ev):
+    def sketch_motion(self, ev):
         if self.sketch:
             self.sketch.motion(ev)
 
-    def sketch_release(self,ev):
+    def sketch_release(self, ev):
         if self.sketch:
             self.sketch.release(ev)
             self.sketch = None
@@ -658,13 +671,13 @@
         marginBottom = 3 if ht > 40 else 0
         marginTop = marginBottom
         return z, ht, marginBottom, marginTop
-        
-    def screen_from_world(self,p):
+
+    def screen_from_world(self, p):
         z, ht, marginBottom, marginTop = self._coords()
         return ((p[0] - z.start) / (z.end - z.start) * self.canvas.props.x2,
                 (ht - marginBottom) - p[1] * (ht - (marginBottom + marginTop)))
 
-    def world_from_screen(self,x,y):
+    def world_from_screen(self, x, y):
         z, ht, marginBottom, marginTop = self._coords()
         return (x / self.canvas.props.x2 * (z.end - z.start) + z.start,
                 ((ht - marginBottom) - y) / (ht - (marginBottom + marginTop)))
@@ -681,33 +694,38 @@
         # when the widget is gone. Correct solution would be to stop
         # and free those handlers when the widget is gone.
         return self.canvas.is_visible()
-        
+
     def update_time_bar(self, t):
         if not self.alive():
             return
         if not getattr(self, 'timelineLine', None):
             self.timelineGroup = GooCanvas.CanvasGroup(
                 parent=self.canvas.get_root_item())
-            self.timelineLine = polyline_new_line(
-                parent=self.timelineGroup,
-                points=Points([(0,0), (0,0)]),
-                line_width=2, stroke_color='red')
+            self.timelineLine = polyline_new_line(parent=self.timelineGroup,
+                                                  points=Points([(0, 0),
+                                                                 (0, 0)]),
+                                                  line_width=2,
+                                                  stroke_color='red')
 
         try:
-            pts = [self.screen_from_world((t, 0)),
-                   self.screen_from_world((t, 1))]
+            pts = [
+                self.screen_from_world((t, 0)),
+                self.screen_from_world((t, 1))
+            ]
         except ZeroDivisionError:
             pts = [(-1, -1), (-1, -1)]
         self.timelineLine.set_property('points', Points(pts))
-        
+
         self._time = t
         if self.knobEnabled:
             self.delete('knob')
             prevKey = self.curve.point_before(t)
             if prevKey is not None:
                 pos = self.screen_from_world(prevKey)
-                self.create_oval(pos[0] - 8, pos[1] - 8,
-                                 pos[0] + 8, pos[1] + 8,
+                self.create_oval(pos[0] - 8,
+                                 pos[1] - 8,
+                                 pos[0] + 8,
+                                 pos[1] + 8,
                                  outline='#800000',
                                  tags=('knob',))
                 dispatcher.send("knob out", value=prevKey[1], curve=self.curve)
@@ -716,7 +734,7 @@
         if not getattr(self, '_pending_update', False):
             self._pending_update = True
             reactor.callLater(.01, self._update_curve)
-        
+
     def _update_curve(self):
         try:
             self._update_curve2()
@@ -734,16 +752,18 @@
             print "no redrawsEnabled, skipping", self
             return
 
-        visible_x = (self.world_from_screen(0,0)[0],
+        visible_x = (self.world_from_screen(0, 0)[0],
                      self.world_from_screen(self.canvas.props.x2, 0)[0])
 
-        visible_idxs = self.curve.indices_between(visible_x[0], visible_x[1],
+        visible_idxs = self.curve.indices_between(visible_x[0],
+                                                  visible_x[1],
                                                   beyond=1)
         visible_points = [self.curve.points[i] for i in visible_idxs]
-        
+
         if getattr(self, 'curveGroup', None):
             self.curveGroup.remove()
-        self.curveGroup = GooCanvas.CanvasGroup(parent=self.canvas.get_root_item())
+        self.curveGroup = GooCanvas.CanvasGroup(
+            parent=self.canvas.get_root_item())
         self.curveGroup.lower(None)
 
         self.canvas.set_property("background-color",
@@ -752,15 +772,15 @@
         self.update_time_bar(self._time)
         self._draw_line(visible_points, area=True)
         self._draw_markers(
-            self.markers.points[i] for i in
-            self.markers.indices_between(visible_x[0], visible_x[1]))
+            self.markers.points[i]
+            for i in self.markers.indices_between(visible_x[0], visible_x[1]))
         if self.canvas.props.y2 > 80:
             self._draw_time_tics(visible_x)
 
-            self.dots = {} # idx : canvas rectangle
+            self.dots = {}  # idx : canvas rectangle
 
             if len(visible_points) < 50 and not self.curve.muted:
-                self._draw_handle_points(visible_idxs,visible_points)
+                self._draw_handle_points(visible_idxs, visible_points)
 
         self.selectionChanged()
 
@@ -771,63 +791,69 @@
 
     def _draw_markers(self, pts):
         colorMap = {
-            'q':'#598522',
-            'w':'#662285',
-            'e':'#852922',
-            'r':'#85225C',
-            't':'#856B22',
-            'y':'#227085',
-            }
+            'q': '#598522',
+            'w': '#662285',
+            'e': '#852922',
+            'r': '#85225C',
+            't': '#856B22',
+            'y': '#227085',
+        }
         for t, name in pts:
-            x = int(self.screen_from_world((t,0))[0]) + .5
+            x = int(self.screen_from_world((t, 0))[0]) + .5
             polyline_new_line(self.curveGroup,
-                              x, 0, x, self.canvas.props.y2,
+                              x,
+                              0,
+                              x,
+                              self.canvas.props.y2,
                               line_width=.4 if name in 'rty' else .8,
                               stroke_color=colorMap.get(name, 'gray'))
 
-    def _draw_time_tics(self,visible_x):
+    def _draw_time_tics(self, visible_x):
         tic = self._draw_one_tic
 
         tic(0, "0")
-        t1,t2=visible_x
-        if t2-t1<30:
-            for t in range(int(t1),int(t2)+1):
-                tic(t,str(t))
+        t1, t2 = visible_x
+        if t2 - t1 < 30:
+            for t in range(int(t1), int(t2) + 1):
+                tic(t, str(t))
         tic(introPad, str(introPad))
 
         endtimes = dispatcher.send("get max time")
         if endtimes:
             endtime = endtimes[0][1]
-            tic(endtime, "end %.1f"%endtime)
+            tic(endtime, "end %.1f" % endtime)
             tic(endtime - postPad, "post %.1f" % (endtime - postPad))
-        
-    def _draw_one_tic(self,t,label):
+
+    def _draw_one_tic(self, t, label):
         try:
-            x = self.screen_from_world((t,0))[0]
+            x = self.screen_from_world((t, 0))[0]
             if not 0 <= x < self.canvas.props.x2:
                 return
-            x = max(5, x) # cheat left-edge stuff onscreen
+            x = max(5, x)  # cheat left-edge stuff onscreen
         except ZeroDivisionError:
             x = -100
-            
+
         ht = self.canvas.props.y2
         polyline_new_line(self.curveGroup,
-                                    x, ht,
-                                    x, ht - 20,
-                                    line_width=.5,
-                                    stroke_color='gray70')
+                          x,
+                          ht,
+                          x,
+                          ht - 20,
+                          line_width=.5,
+                          stroke_color='gray70')
         GooCanvas.CanvasText(parent=self.curveGroup,
-                       fill_color="white",
-                       anchor=GooCanvas.CanvasAnchorType.SOUTH,
-                       font="ubuntu 7",
-                       x=x+3, y=ht-20,
-                       text=label)
+                             fill_color="white",
+                             anchor=GooCanvas.CanvasAnchorType.SOUTH,
+                             font="ubuntu 7",
+                             x=x + 3,
+                             y=ht - 20,
+                             text=label)
 
     def _draw_line(self, visible_points, area=False):
         if not visible_points:
             return
-        linepts=[]
-        step=1
+        linepts = []
+        step = 1
         linewidth = 1.5
         maxPointsToDraw = self.canvas.props.x2 / 2
         if len(visible_points) > maxPointsToDraw:
@@ -835,7 +861,7 @@
             linewidth = .8
         for p in visible_points[::step]:
             try:
-                x,y = self.screen_from_world(p)
+                x, y = self.screen_from_world(p)
             except ZeroDivisionError:
                 x = y = -100
             linepts.append((int(x) + .5, int(y) + .5))
@@ -855,75 +881,77 @@
             if len(areapts) >= 1:
                 areapts.insert(0, (0, areapts[0][1]))
                 areapts.append((self.canvas.props.x2, areapts[-1][1]))
-            polyline_new_line(parent=self.curveGroup,
-                              points=Points(
-                                  [(areapts[0][0], base)] +
-                                  areapts +
-                                  [(areapts[-1][0], base)]),
-                              close_path=True,
-                              line_width=0,
-                              # transparent as a workaround for
-                              # covering some selectmanips (which
-                              # become unclickable)
-                              fill_color_rgba=0x00800080,
+            polyline_new_line(
+                parent=self.curveGroup,
+                points=Points([(areapts[0][0], base)] + areapts +
+                              [(areapts[-1][0], base)]),
+                close_path=True,
+                line_width=0,
+                # transparent as a workaround for
+                # covering some selectmanips (which
+                # become unclickable)
+                fill_color_rgba=0x00800080,
             )
 
-        self.pl = polyline_new_line(parent=self.curveGroup,
-                                     points=Points(linepts),
-                                     line_width=linewidth,
-                                     stroke_color=fill,
-                                     )
-                
-            
-    def _draw_handle_points(self,visible_idxs,visible_points):
-        for i,p in zip(visible_idxs,visible_points):
-            rad=6
+        self.pl = polyline_new_line(
+            parent=self.curveGroup,
+            points=Points(linepts),
+            line_width=linewidth,
+            stroke_color=fill,
+        )
+
+    def _draw_handle_points(self, visible_idxs, visible_points):
+        for i, p in zip(visible_idxs, visible_points):
+            rad = 6
             worldp = p
             try:
                 p = self.screen_from_world(p)
             except ZeroDivisionError:
                 p = (-100, -100)
-            dot = GooCanvas.CanvasRect(parent=self.curveGroup,
-                                 x=int(p[0] - rad) + .5,
-                                 y=int(p[1] - rad) + .5,
-                                 width=rad * 2, height=rad * 2,
-                                 stroke_color='gray90',
-                                 fill_color='blue',
-                                 line_width=1,
-                                 #tags=('curve','point', 'handle%d' % i)
-                                 )
+            dot = GooCanvas.CanvasRect(
+                parent=self.curveGroup,
+                x=int(p[0] - rad) + .5,
+                y=int(p[1] - rad) + .5,
+                width=rad * 2,
+                height=rad * 2,
+                stroke_color='gray90',
+                fill_color='blue',
+                line_width=1,
+                #tags=('curve','point', 'handle%d' % i)
+            )
 
             if worldp[1] == 0:
                 rad += 3
-                GooCanvas.CanvasEllipse(parent=self.curveGroup,
-                                  center_x=p[0],
-                                  center_y=p[1],
-                                  radius_x=rad,
-                                  radius_y=rad,
-                                  line_width=2,
-                                  stroke_color='#00a000',
-                                  #tags=('curve','point', 'handle%d' % i)
-                                  )
+                GooCanvas.CanvasEllipse(
+                    parent=self.curveGroup,
+                    center_x=p[0],
+                    center_y=p[1],
+                    radius_x=rad,
+                    radius_y=rad,
+                    line_width=2,
+                    stroke_color='#00a000',
+                    #tags=('curve','point', 'handle%d' % i)
+                )
             dot.connect("button-press-event", self.dotpress, i)
             #self.tag_bind('handle%d' % i,"<ButtonPress-1>",
             #              lambda ev,i=i: self.dotpress(ev,i))
             #self.tag_bind('handle%d' % i, "<Key-d>",
             #              lambda ev, i=i: self.remove_point_idx(i))
-                      
-            self.dots[i]=dot
+
+            self.dots[i] = dot
 
         self.highlight_selected_dots()
 
-    def find_index_near(self,x,y):
+    def find_index_near(self, x, y):
         tags = self.gettags(self.find_closest(x, y))
         try:
             handletags = [t for t in tags if t.startswith('handle')]
             return int(handletags[0][6:])
         except IndexError:
             raise ValueError("no point found")
-        
+
     def new_point_at_mouse(self, ev):
-        p = self.world_from_screen(ev.x,ev.y)
+        p = self.world_from_screen(ev.x, ev.y)
         x = p[0]
         y = self.curve.eval(x)
         self.add_point((x, y))
@@ -931,13 +959,13 @@
     def add_points(self, pts):
         idxs = [self.curve.insert_pt(p) for p in pts]
         self.select_indices(idxs)
-        
+
     def add_point(self, p):
         self.add_points([p])
 
     def add_marker(self, p):
         self.markers.insert_pt(p)
-        
+
     def remove_point_idx(self, *idxs):
         idxs = list(idxs)
         while idxs:
@@ -960,17 +988,17 @@
 
             self.select_indices(newsel)
             idxs[:] = newidxs
-            
+
     def highlight_selected_dots(self):
         if not self.redrawsEnabled:
             return
 
-        for i,d in self.dots.items():
+        for i, d in self.dots.items():
             if i in self.selected_points:
                 d.set_property('fill_color', 'red')
             else:
                 d.set_property('fill_color', 'blue')
-        
+
     def dotpress(self, r1, r2, ev, dotidx):
         self.print_state("dotpress")
         if dotidx not in self.selected_points:
@@ -979,10 +1007,10 @@
         self.last_mouse_world = self.world_from_screen(ev.x, ev.y)
         self.dragging_dots = True
 
-    def select_between(self,start,end):
+    def select_between(self, start, end):
         if start > end:
             start, end = end, start
-        self.select_indices(self.curve.indices_between(start,end))
+        self.select_indices(self.curve.indices_between(start, end))
 
     def onEnter(self, widget, event):
         self.entered = True
@@ -993,16 +1021,16 @@
     def onMotion(self, widget, event):
         self.lastMouseX = event.x
 
-        if event.state & Gdk.ModifierType.SHIFT_MASK and 1: # and B1
+        if event.state & Gdk.ModifierType.SHIFT_MASK and 1:  # and B1
             self.sketch_motion(event)
             return
 
         self.select_motion(event)
-        
+
         if not self.dragging_dots:
             return
         if not event.state & 256:
-            return # not lmb-down
+            return  # not lmb-down
 
         # this way is accumulating error and also making it harder to
         # undo (e.g. if the user moves far out of the window or
@@ -1013,58 +1041,60 @@
             delta = (cur[0] - self.last_mouse_world[0],
                      cur[1] - self.last_mouse_world[1])
         else:
-            delta = 0,0
+            delta = 0, 0
         self.last_mouse_world = cur
 
         self.translate_points(delta)
-        
 
     def translate_points(self, delta):
         moved = False
-        
+
         cp = self.curve.points
         updates = []
         for idx in self.selected_points:
 
             newp = [cp[idx][0] + delta[0], cp[idx][1] + delta[1]]
-            
-            newp[1] = max(0,min(1,newp[1]))
-            
-            if idx>0 and newp[0] <= cp[idx-1][0]:
+
+            newp[1] = max(0, min(1, newp[1]))
+
+            if idx > 0 and newp[0] <= cp[idx - 1][0]:
                 continue
-            if idx<len(cp)-1 and newp[0] >= cp[idx+1][0]:
+            if idx < len(cp) - 1 and newp[0] >= cp[idx + 1][0]:
                 continue
             moved = True
             updates.append((idx, tuple(newp)))
         self.curve.set_points(updates)
         return moved
-            
+
     def unselect(self):
         self.select_indices([])
 
     def onScroll(self, widget, event):
         t = self.world_from_screen(event.x, 0)[0]
         self.zoomControl.zoom_about_mouse(
-            t, factor=1.5 if event.direction == Gdk.ScrollDirection.DOWN else 1/1.5)
+            t,
+            factor=1.5 if event.direction == Gdk.ScrollDirection.DOWN else 1 /
+            1.5)
         # Don't actually scroll the canvas! (it shouldn't have room to
         # scroll anyway, but it does because of some coordinate errors
         # and borders and stuff)
-        return True 
-        
+        return True
+
     def onRelease(self, widget, event):
         self.print_state("dotrelease")
 
-        if event.state & Gdk.ModifierType.SHIFT_MASK: # relese-B1
+        if event.state & Gdk.ModifierType.SHIFT_MASK:  # relese-B1
             self.sketch_release(event)
             return
 
         self.select_release(event)
- 
+
         if not self.dragging_dots:
             return
         self.last_mouse_world = None
         self.dragging_dots = False
 
+
 class CurveRow(object):
     """
     one of the repeating curve rows (including widgets on the left)
@@ -1073,6 +1103,7 @@
 
     please pack self.box
     """
+
     def __init__(self, graph, name, curve, markers, zoomControl):
         self.graph = graph
         self.name = name
@@ -1081,23 +1112,24 @@
 
         self.cols = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 0)
         self.box.add(self.cols)
-        
+
         controls = Gtk.Frame()
         controls.set_size_request(160, -1)
         controls.set_shadow_type(Gtk.ShadowType.OUT)
         self.cols.pack_start(controls, expand=False, fill=True, padding=0)
         self.setupControls(controls, name, curve)
 
-        self.curveView = Curveview(curve, markers,
+        self.curveView = Curveview(curve,
+                                   markers,
                                    isMusic=name in ['music', 'smooth_music'],
                                    zoomControl=zoomControl)
-        
+
         self.initCurveView()
         dispatcher.connect(self.rebuild, "all curves rebuild")
 
     def isFocus(self):
         return self.curveView.widget.is_focus()
-        
+
     def rebuild(self):
         raise NotImplementedError('obsolete, if curves are drawing right')
         self.curveView.rebuild()
@@ -1108,32 +1140,36 @@
         self.curveView.entered = False  # help suppress bad position events
         del self.curveView
         self.box.destroy()
-        
+
     def initCurveView(self):
         self.curveView.widget.show()
         self.setHeight(100)
-        self.cols.pack_start(self.curveView.widget, expand=True, fill=True, padding=0)       
+        self.cols.pack_start(self.curveView.widget,
+                             expand=True,
+                             fill=True,
+                             padding=0)
 
     def setHeight(self, h):
         self.curveView.widget.set_size_request(-1, h)
 
         # this should have been automatic when the size changed, but
         # the signals for that are wrong somehow.
-        reactor.callLater(0, self.curveView.setCanvasToWidgetSize) 
-        
+        reactor.callLater(0, self.curveView.setCanvasToWidgetSize)
+
     def setupControls(self, controls, name, curve):
         box = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 0)
         controls.add(box)
 
         curve_name_label = Gtk.LinkButton()
         print "need to truncate this name length somehow"
+
         def update_label():
             # todo: abort if we don't still exist...
             p = curve_name_label.props
             p.uri = curve.uri
             p.label = self.graph.label(curve.uri)
+
         self.graph.addHandler(update_label)
-        
 
         self.muted = Gtk.CheckButton("M")
         self.muted.connect("toggled", self.sync_mute_to_curve)
@@ -1144,7 +1180,7 @@
 
     def onDelete(self):
         self.curveView.onDelete()
-        
+
     def sync_mute_to_curve(self, *args):
         """send value from CheckButton to the master attribute inside Curve"""
         new_mute = self.muted.get_active()
@@ -1172,6 +1208,7 @@
     """
     
     """
+
     def __init__(self, graph, curvesVBox, zoomControlBox, curveset):
         self.graph = graph
         self.live = True
@@ -1184,13 +1221,13 @@
         self.zoomControl.redrawzoom()
 
         for uri, label, curve in curveset.currentCurves():
-            self.add_curve(uri, label, curve) 
+            self.add_curve(uri, label, curve)
 
         dispatcher.connect(self.clear_curves, "clear_curves")
         dispatcher.connect(self.add_curve, "add_curve", sender=self.curveset)
         dispatcher.connect(self.set_featured_curves, "set_featured_curves")
         dispatcher.connect(self.song_has_changed, "song_has_changed")
-        
+
         self.newcurvename = Gtk.EntryBuffer.new("", 0)
 
         eventBox = self.curvesVBox.get_parent()
@@ -1200,7 +1237,7 @@
         self.watchCurveAreaHeight()
 
     def __del__(self):
-        print "del curvesetview", id(self) 
+        print "del curvesetview", id(self)
 
     def initZoomControl(self, zoomControlBox):
         import light9.curvecalc.zoomcontrol
@@ -1209,7 +1246,7 @@
         zoomControlBox.add(zoomControl.widget)
         zoomControl.widget.show_all()
         return zoomControl
-        
+
     def clear_curves(self):
         """curveset is about to re-add all new curves"""
         while self.allCurveRows:
@@ -1217,7 +1254,7 @@
 
     def song_has_changed(self):
         self.zoomControl.redrawzoom()
-        
+
     def takeFocus(self, *args):
         """the whole curveset's eventbox is what gets the focus, currently, so
         keys like 'c' can work in it"""
@@ -1233,17 +1270,17 @@
     def set_featured_curves(self, curveNames):
         """bring these curves to the top of the stack"""
         for n in curveNames[::-1]:
-            self.curvesVBox.reorder_child(self.curveRow_from_name(n).box,
-                                          Gtk.PACK_START)
-        
+            self.curvesVBox.reorder_child(
+                self.curveRow_from_name(n).box, Gtk.PACK_START)
+
     def onKeyPress(self, widget, event):
-        if not self.live: # workaround for old instances living past reload()
+        if not self.live:  # workaround for old instances living past reload()
             return
 
         r = self.row_under_mouse()
         key = event.string
-        pass # no handlers right now
- 
+        pass  # no handlers right now
+
     def row_under_mouse(self):
         x, y = self.curvesVBox.get_pointer()
         for r in self.allCurveRows:
@@ -1259,7 +1296,7 @@
     def new_curve(self, event):
         self.curveset.new_curve(self.newcurvename.get())
         self.newcurvename.set('')
-        
+
     def add_curve(self, uri, label, curve):
         if isinstance(label, Literal):
             label = str(label)
@@ -1273,6 +1310,7 @@
         f.curveView.goLive()
 
     def watchCurveAreaHeight(self):
+
         def sizeEvent(w, size):
             # this is firing really often
             if self.visibleHeight == size.height:
@@ -1285,7 +1323,7 @@
         visibleArea.connect('size-allocate', sizeEvent)
 
         dispatcher.connect(self.setRowHeights, "curve row focus change")
-        
+
     def setRowHeights(self):
         nRows = len(self.allCurveRows)
         if not nRows:
@@ -1296,14 +1334,13 @@
         if anyFocus:
             focusHeight = max(100, evenHeight)
             if nRows > 1:
-                otherHeight = max(14,
-                                  (self.visibleHeight - focusHeight) //
+                otherHeight = max(14, (self.visibleHeight - focusHeight) //
                                   (nRows - 1)) - 3
         else:
             otherHeight = evenHeight
         for row in self.allCurveRows:
             row.setHeight(focusHeight if row.isFocus() else otherHeight)
-            
+
     def row(self, name):
         if isinstance(name, Literal):
             name = str(name)
@@ -1316,13 +1353,10 @@
     def goLive(self):
         """for startup performance, none of the curves redraw
         themselves until this is called once (and then they're normal)"""
-        
+
         for cr in self.allCurveRows:
             cr.curveView.goLive()
 
     def onDelete(self):
         for r in self.allCurveRows:
             r.onDelete()
-
-
-        
--- a/light9/curvecalc/musicaccess.py	Tue May 21 23:55:35 2019 +0000
+++ b/light9/curvecalc/musicaccess.py	Tue May 21 23:56:12 2019 +0000
@@ -5,14 +5,16 @@
 from twisted.internet import reactor
 from twisted.web.client import Agent
 from twisted.internet.protocol import Protocol
-from twisted.internet.defer import Deferred     
+from twisted.internet.defer import Deferred
 from zope.interface import implements
 from twisted.internet.defer import succeed
 from twisted.web.iweb import IBodyProducer
 
+
 class GatherJson(Protocol):
     """calls back the 'finished' deferred with the parsed json data we
     received"""
+
     def __init__(self, finished):
         self.finished = finished
         self.buf = ""
@@ -23,6 +25,7 @@
     def connectionLost(self, reason):
         self.finished.callback(json.loads(self.buf))
 
+
 class StringProducer(object):
     # http://twistedmatrix.com/documents/current/web/howto/client.html
     implements(IBodyProducer)
@@ -41,12 +44,14 @@
     def stopProducing(self):
         pass
 
+
 class Music:
+
     def __init__(self):
-        self.recenttime=0
+        self.recenttime = 0
         self.player = Agent(reactor)
         self.timePath = networking.musicPlayer.path("time")
-        
+
     def current_time(self):
         """return deferred which gets called with the current
         time. This gets called really often"""
@@ -65,8 +70,8 @@
             dispatcher.send("input time", val=data['t'])
         if 'song' in data and data['song']:
             dispatcher.send("current_player_song", song=URIRef(data['song']))
-        return data['t'] # pass along to the real receiver
-    
+        return data['t']  # pass along to the real receiver
+
     def playOrPause(self, t=None):
         if t is None:
             # could be better
@@ -74,4 +79,5 @@
         else:
             self.player.request("POST",
                                 networking.musicPlayer.path("seekPlayOrPause"),
-                                bodyProducer=StringProducer(json.dumps({"t" : t})))
+                                bodyProducer=StringProducer(json.dumps({"t":
+                                                                        t})))
--- a/light9/curvecalc/output.py	Tue May 21 23:55:35 2019 +0000
+++ b/light9/curvecalc/output.py	Tue May 21 23:56:12 2019 +0000
@@ -7,33 +7,35 @@
 from louie import dispatcher
 log = logging.getLogger("output")
 
+
 class Output(object):
-    lastsendtime=0
-    lastsendlevs=None
+    lastsendtime = 0
+    lastsendlevs = None
+
     def __init__(self, graph, session, music, curveset, currentSubterms):
         self.graph, self.session, self.music = graph, session, music
         self.currentSubterms = currentSubterms
         self.curveset = curveset
 
-        self.recent_t=[]
+        self.recent_t = []
         self.later = None
 
         self.update()
-        
+
     def update(self):
         d = self.music.current_time()
         d.addCallback(self.update2)
         d.addErrback(self.updateerr)
-        
-    def updateerr(self,e):
+
+    def updateerr(self, e):
 
         print e.getTraceback()
         dispatcher.send("update status", val=e.getErrorMessage())
         if self.later and not self.later.cancelled and not self.later.called:
             self.later.cancel()
         self.later = reactor.callLater(1, self.update)
-        
-    def update2(self,t):
+
+    def update2(self, t):
         # spot alsa soundcard offset is always 0, we get times about a
         # second ahead of what's really getting played
         #t = t - .7
@@ -44,28 +46,29 @@
 
         self.later = reactor.callLater(.02, self.update)
 
-        self.recent_t = self.recent_t[-50:]+[t]
+        self.recent_t = self.recent_t[-50:] + [t]
         period = (self.recent_t[-1] - self.recent_t[0]) / len(self.recent_t)
         dispatcher.send("update period", val=period)
         self.send_dmx(t)
-        
+
     def send_dmx(self, t):
         dispatcher.send("curves to sliders", t=t)
 
         if not self.currentSubterms:
             return
-            
-        scaledsubs=[]
+
+        scaledsubs = []
         for st in self.currentSubterms:
             scl = st.scaled(t)
             scaledsubs.append(scl)
-                
+
         out = Submaster.sub_maxes(*scaledsubs)
         levs = out.get_levels()
-        now=time.time()
-        if now-self.lastsendtime>5 or levs!=self.lastsendlevs:
-            dispatcher.send("output levels",val=levs)
+        now = time.time()
+        if now - self.lastsendtime > 5 or levs != self.lastsendlevs:
+            dispatcher.send("output levels", val=levs)
             dmxclient.outputlevels(out.get_dmx_list(),
-                                   twisted=1,clientid='curvecalc')
+                                   twisted=1,
+                                   clientid='curvecalc')
             self.lastsendtime = now
             self.lastsendlevs = levs
--- a/light9/curvecalc/subterm.py	Tue May 21 23:55:35 2019 +0000
+++ b/light9/curvecalc/subterm.py	Tue May 21 23:56:12 2019 +0000
@@ -8,24 +8,26 @@
 from light9.namespaces import L9
 log = logging.getLogger()
 
+
 class Expr(object):
     """singleton, provides functions for use in subterm expressions,
     e.g. chases"""
+
     def __init__(self):
         self.effectGlobals = light9.Effects.configExprGlobals()
-    
+
     def exprGlobals(self, startDict, t):
         """globals dict for use by expressions"""
 
         glo = startDict.copy()
-        
+
         # add in functions from Effects
         glo.update(self.effectGlobals)
 
         def chan(name):
-            return Submaster.Submaster(
-                name=name,
-                levels={get_dmx_channel(name) : 1.0})
+            return Submaster.Submaster(name=name,
+                                       levels={get_dmx_channel(name): 1.0})
+
         glo['chan'] = chan
         glo['within'] = lambda a, b: a < t < b
         glo['bef'] = lambda x: t < x
@@ -36,20 +38,24 @@
             if left < t < right:
                 return light9.Effects.smoove((t - left) / (right - left))
             return t > x
+
         glo['aft'] = lambda x, smooth=0: aft(t, x, smooth)
 
         glo['smooth_random'] = lambda speed=1: glo['smooth_random2'](t, speed)
         glo['notch_random'] = lambda speed=1: glo['notch_random2'](t, speed)
-        
+
         glo['noise'] = glo['smooth_random']
         glo['notch'] = glo['notch_random']
 
         return glo
 
+
 exprglo = Expr()
-        
+
+
 class Subterm(object):
     """one Submaster and its expression evaluator"""
+
     def __init__(self, graph, subterm, saveContext, curveset):
         self.graph, self.uri = graph, subterm
         self.saveContext = saveContext
@@ -57,16 +63,19 @@
         self.ensureExpression(saveContext)
 
         self.submasters = Submaster.get_global_submasters(self.graph)
-        
+
     def ensureExpression(self, saveCtx):
-        with self.graph.currentState(tripleFilter=(self.uri, None, None)) as current:
+        with self.graph.currentState(tripleFilter=(self.uri, None,
+                                                   None)) as current:
             if current.value(self.uri, L9['expression']) is None:
-                self.graph.patch(Patch(addQuads=[
-                    (self.uri, L9['expression'], Literal("..."), saveCtx),
+                self.graph.patch(
+                    Patch(addQuads=[
+                        (self.uri, L9['expression'], Literal("..."), saveCtx),
                     ]))
 
     def scaled(self, t):
-        with self.graph.currentState(tripleFilter=(self.uri, None, None)) as current:
+        with self.graph.currentState(tripleFilter=(self.uri, None,
+                                                   None)) as current:
             subexpr_eval = self.eval(current, t)
             # we prevent any exceptions from escaping, since they cause us to
             # stop sending levels
@@ -75,7 +84,7 @@
                     # if the expression returns a submaster, just return it
                     return subexpr_eval
                 else:
-                    # otherwise, return our submaster multiplied by the value 
+                    # otherwise, return our submaster multiplied by the value
                     # returned
                     if subexpr_eval == 0:
                         return Submaster.Submaster("zero", {})
@@ -89,7 +98,8 @@
     def curves_used_by_expr(self):
         """names of curves that are (maybe) used in this expression"""
 
-        with self.graph.currentState(tripleFilter=(self.uri, None, None)) as current:
+        with self.graph.currentState(tripleFilter=(self.uri, None,
+                                                   None)) as current:
             expr = current.value(self.uri, L9['expression'])
 
         used = []
@@ -106,21 +116,24 @@
         if len(objs) > 1:
             raise ValueError("found multiple expressions for %s: %s" %
                              (self.uri, objs))
-        
+
         expr = current.value(self.uri, L9['expression'])
         if not expr:
-            dispatcher.send("expr_error", sender=self.uri, exc="no expr, using 0")
+            dispatcher.send("expr_error",
+                            sender=self.uri,
+                            exc="no expr, using 0")
             return 0
         glo = self.curveset.globalsdict()
         glo['t'] = t
 
         glo = exprglo.exprGlobals(glo, t)
         glo['getsub'] = lambda name: self.submasters.get_sub_by_name(name)
-        glo['chan'] = lambda name: Submaster.Submaster("chan", {get_dmx_channel(name): 1})
-        
+        glo['chan'] = lambda name: Submaster.Submaster(
+            "chan", {get_dmx_channel(name): 1})
+
         try:
             self.lasteval = eval(expr, glo)
-        except Exception,e:
+        except Exception, e:
             dispatcher.send("expr_error", sender=self.uri, exc=e)
             return Submaster.Submaster("zero", {})
         else:
--- a/light9/curvecalc/subtermview.py	Tue May 21 23:55:35 2019 +0000
+++ b/light9/curvecalc/subtermview.py	Tue May 21 23:56:12 2019 +0000
@@ -9,7 +9,9 @@
 # keeping a ref to the __dict__ of the object stops it from getting zeroed
 keep = []
 
+
 class Subexprview(object):
+
     def __init__(self, graph, ownerSubterm, saveContext, curveset):
         self.graph, self.ownerSubterm = graph, ownerSubterm
         self.saveContext = saveContext
@@ -30,8 +32,10 @@
         self.entryBuffer.connect("inserted-text", self.entry_changed)
 
         self.entry.connect("focus-in-event", self.onFocus)
-        
-        dispatcher.connect(self.exprError, "expr_error", sender=self.ownerSubterm)
+
+        dispatcher.connect(self.exprError,
+                           "expr_error",
+                           sender=self.ownerSubterm)
         keep.append(self.__dict__)
 
     def onFocus(self, *args):
@@ -40,30 +44,31 @@
 
         usedCurves = [n for n in curveNames if n in currentExpr]
         usedCurves.sort()
-        
+
         dispatcher.send("set_featured_curves", curveNames=usedCurves)
-        
+
     def exprError(self, exc):
         self.error.set_text(str(exc))
-        
+
     def set_expression_from_graph(self):
         e = str(self.graph.value(self.ownerSubterm, L9['expression']))
         print "from graph, set to %r" % e
 
         if e != self.entryBuffer.get_text():
             self.entryBuffer.set_text(e, len(e))
-            
+
     def entry_changed(self, *args):
         log.info("want to patch to %r", self.entryBuffer.get_text())
-        self.graph.patchObject(self.saveContext,
-                               self.ownerSubterm,
+        self.graph.patchObject(self.saveContext, self.ownerSubterm,
                                L9['expression'],
                                Literal(self.entryBuffer.get_text()))
-            
+
+
 class Subtermview(object):
     """
     has .label and .exprView widgets for you to put in a table
     """
+
     def __init__(self, st, curveset):
         self.subterm = st
         self.graph = st.graph
@@ -72,19 +77,18 @@
         self.graph.addHandler(self.setName)
 
         self.label.drag_dest_set(flags=Gtk.DEST_DEFAULT_ALL,
-                            targets=[('text/uri-list', 0, 0)],
-                            actions=Gtk.gdk.ACTION_COPY)
+                                 targets=[('text/uri-list', 0, 0)],
+                                 actions=Gtk.gdk.ACTION_COPY)
         self.label.connect("drag-data-received", self.onDataReceivedOnLabel)
-        
-        sev = Subexprview(self.graph, self.subterm.uri, self.subterm.saveContext, curveset)
+
+        sev = Subexprview(self.graph, self.subterm.uri,
+                          self.subterm.saveContext, curveset)
         self.exprView = sev.box
 
     def onDataReceivedOnLabel(self, widget, context, x, y, selection,
-                       targetType, time):
-        self.graph.patchObject(self.subterm.saveContext,
-                               self.subterm.uri,
-                               L9['sub'],
-                               URIRef(selection.data.strip()))
+                              targetType, time):
+        self.graph.patchObject(self.subterm.saveContext, self.subterm.uri,
+                               L9['sub'], URIRef(selection.data.strip()))
 
     def setName(self):
         # some of this could be pushed into Submaster
@@ -99,22 +103,24 @@
             return
         self.label.set_text(label)
 
+
 def add_one_subterm(subterm, curveset, master, show=False):
     stv = Subtermview(subterm, curveset)
-    
+
     y = master.get_property('n-rows')
     master.attach(stv.label, 0, 1, y, y + 1, xoptions=0, yoptions=0)
     master.attach(stv.exprView, 1, 2, y, y + 1, yoptions=0)
-    scrollToRowUponAdd(stv.label)  
+    scrollToRowUponAdd(stv.label)
     if show:
         master.show_all()
 
+
 def scrollToRowUponAdd(widgetInRow):
     """when this table widget is ready, scroll the table so we can see it"""
-    
+
     # this doesn't work right, yet
     return
-    
+
     vp = widgetInRow
     while vp.get_name() != 'GtkViewport':
         log.info("walk %s", vp.get_name())
@@ -125,5 +131,5 @@
         log.info("scroll %s", adj.props.value)
         adj.props.value = adj.props.upper
         widgetInRow.disconnect(handler)
-        
+
     handler = widgetInRow.connect('expose-event', firstExpose, adj, widgetInRow)
--- a/light9/curvecalc/zoomcontrol.py	Tue May 21 23:55:35 2019 +0000
+++ b/light9/curvecalc/zoomcontrol.py	Tue May 21 23:56:12 2019 +0000
@@ -3,10 +3,11 @@
 from gi.repository import GObject
 from gi.repository import GooCanvas
 import louie as dispatcher
-from light9.curvecalc import cursors 
+from light9.curvecalc import cursors
 from lib.goocanvas_compat import Points, polyline_new_line
 from twisted.internet import reactor
 
+
 class ZoomControl(object):
     """
     please pack .widget
@@ -16,43 +17,63 @@
 
     def maxtime():
         doc = "seconds at the right edge of the bar"
-        def fget(self): return self._maxtime
+
+        def fget(self):
+            return self._maxtime
+
         def fset(self, value):
             self._maxtime = value
             self.redrawzoom()
+
         return locals()
+
     maxtime = property(**maxtime())
 
     _end = _start = 0
+
     def start():
-        def fget(self): return self._start
-        def fset(self,v):
-            v = max(self.mintime,v)
+
+        def fget(self):
+            return self._start
+
+        def fset(self, v):
+            v = max(self.mintime, v)
             # don't protect for start<end since zooming sometimes sets
             # start temporarily after end
             self._start = v
+
         return locals()
+
     start = property(**start())
 
     def end():
-        def fget(self): return self._end
-        def fset(self,v):
-            v = min(self.maxtime,v)
+
+        def fget(self):
+            return self._end
+
+        def fset(self, v):
+            v = min(self.maxtime, v)
             self._end = v
+
         return locals()
+
     end = property(**end())
 
     def offset():
         doc = "virtual attr that adjusts start and end together"
+
         def fget(self):
             # work off the midpoint so that "crushing works equally
             # well in both directions
             return (self.start + self.end) / 2
+
         def fset(self, value):
-            d = self.end-self.start
+            d = self.end - self.start
             self.start = value - d / 2
             self.end = value + d / 2
+
         return locals()
+
     offset = property(**offset())
 
     def __init__(self, **kw):
@@ -72,43 +93,45 @@
 
         self.root = self.widget.get_root_item()
         self.leftbrack = polyline_new_line(parent=self.root,
-                                            line_width=5, stroke_color='black')
+                                           line_width=5,
+                                           stroke_color='black')
         self.rightbrack = polyline_new_line(parent=self.root,
-                                             line_width=5, stroke_color='black')
+                                            line_width=5,
+                                            stroke_color='black')
         self.shade = GooCanvas.CanvasRect(parent=self.root,
-                                    fill_color='gray70',
-                                    line_width=.5)
+                                          fill_color='gray70',
+                                          line_width=.5)
         self.time = polyline_new_line(parent=self.root,
-                                       line_width=2,
-                                       stroke_color='red')
+                                      line_width=2,
+                                      stroke_color='red')
 
         self.redrawzoom()
         self.widget.connect("size-allocate", self.redrawzoom)
 
         self.widget.connect("motion-notify-event", self.adjust)
         self.widget.connect("button-release-event", self.release)
-        self.leftbrack.connect("button-press-event",
-                               lambda i, t, ev: self.press(ev, 'start'))
-        self.rightbrack.connect("button-press-event",
-                                lambda i, t, ev: self.press(ev, 'end'))
-        self.shade.connect("button-press-event",
-                           lambda i, t, ev: self.press(ev, 'offset'))
-        
-        dispatcher.connect(self.input_time,"input time")
+        self.leftbrack.connect(
+            "button-press-event", lambda i, t, ev: self.press(ev, 'start'))
+        self.rightbrack.connect(
+            "button-press-event", lambda i, t, ev: self.press(ev, 'end'))
+        self.shade.connect(
+            "button-press-event", lambda i, t, ev: self.press(ev, 'offset'))
+
+        dispatcher.connect(self.input_time, "input time")
         dispatcher.connect(self.max_time, "max time")
         dispatcher.connect(self.zoom_about_mouse, "zoom about mouse")
         dispatcher.connect(self.see_time, "see time")
         dispatcher.connect(self.see_time_until_end, "see time until end")
         dispatcher.connect(self.show_all, "show all")
         dispatcher.connect(self.zoom_to_range, "zoom to range")
-        self.created=1
+        self.created = 1
         self.lastTime = 0
 
     def max_time(self, maxtime):
         self.maxtime = maxtime
         self.redrawzoom()
-    
-    def zoom_to_range(self,start,end):
+
+    def zoom_to_range(self, start, end):
         self.start = start
         self.end = end
         self.redrawzoom()
@@ -118,9 +141,9 @@
         self.end = self.maxtime
         self.redrawzoom()
 
-    def zoom_about_mouse(self,t,factor):
-        self.start = t - factor*(t-self.start)
-        self.end = t + factor*(self.end-t)
+    def zoom_about_mouse(self, t, factor):
+        self.start = t - factor * (t - self.start)
+        self.end = t + factor * (self.end - t)
         self.redrawzoom()
 
     def see_time(self, t=None):
@@ -144,7 +167,7 @@
         self.end = self.maxtime
 
         self.redrawzoom()
-            
+
     def input_time(self, val):
         """move time cursor to this time"""
         self.lastTime = val
@@ -152,47 +175,48 @@
             x = self.can_for_t(self.lastTime)
         except ZeroDivisionError:
             x = -100
-        self.time.set_property("points",
-                               Points([(x, 0), (x, self.size.height)]))
-        
-    def press(self,ev,attr):
+        self.time.set_property("points", Points([(x, 0),
+                                                 (x, self.size.height)]))
+
+    def press(self, ev, attr):
         self.adjustingattr = attr
-        
+
     def release(self, widget, ev):
-        if hasattr(self,'adjustingattr'):
+        if hasattr(self, 'adjustingattr'):
             del self.adjustingattr
-        if hasattr(self,'lastx'):
+        if hasattr(self, 'lastx'):
             del self.lastx
-        
+
     def adjust(self, widget, ev):
 
-        if not hasattr(self,'adjustingattr'):
+        if not hasattr(self, 'adjustingattr'):
             return
         attr = self.adjustingattr
-        
-        if not hasattr(self,'lastx'):
+
+        if not hasattr(self, 'lastx'):
             self.lastx = ev.x
-        new = self.can_for_t(getattr(self,attr)) + (ev.x - self.lastx)
+        new = self.can_for_t(getattr(self, attr)) + (ev.x - self.lastx)
         self.lastx = ev.x
-        setattr(self,attr,self.t_for_can(new))
+        setattr(self, attr, self.t_for_can(new))
         self.redrawzoom()
 
-    def can_for_t(self,t):
+    def can_for_t(self, t):
         a, b = self.mintime, self.maxtime
         return (t - a) / (b - a) * (self.size.width - 30) + 20
-    def t_for_can(self,x):
+
+    def t_for_can(self, x):
         a, b = self.mintime, self.maxtime
         return (x - 20) / (self.size.width - 30) * (b - a) + a
-        
-    def redrawzoom(self,*args):
+
+    def redrawzoom(self, *args):
         # often, this was clearing the zoom widget and not repainting right
         reactor.callLater(0, self._redrawzoom)
-        
+
     def _redrawzoom(self):
         """redraw pieces based on start/end"""
         self.size = self.widget.get_allocation()
         dispatcher.send("zoom changed")
-        if not hasattr(self,'created'):
+        if not hasattr(self, 'created'):
             return
         y1, y2 = 3, self.size.height - 3
         lip = 6
@@ -203,46 +227,44 @@
             # todo: set the zoom to some clear null state
             return
 
-        self.leftbrack.set_property("points", Points([
-            (scan + lip, y1),
-            (scan, y1),
-            (scan, y2),
-            (scan + lip, y2)]))
-        self.rightbrack.set_property("points", Points([
-            (ecan - lip, y1),
-            (ecan, y1),
-            (ecan, y2),
-            (ecan - lip, y2)]))
-        self.shade.set_properties(
-            x=scan + 5,
-            y=y1 + lip,
-            width=max(0, ecan - 5 - (scan + 5)),
-            height=max(0, y2 - lip - (y1 + lip)))
+        self.leftbrack.set_property(
+            "points",
+            Points([(scan + lip, y1), (scan, y1), (scan, y2),
+                    (scan + lip, y2)]))
+        self.rightbrack.set_property(
+            "points",
+            Points([(ecan - lip, y1), (ecan, y1), (ecan, y2),
+                    (ecan - lip, y2)]))
+        self.shade.set_properties(x=scan + 5,
+                                  y=y1 + lip,
+                                  width=max(0, ecan - 5 - (scan + 5)),
+                                  height=max(0, y2 - lip - (y1 + lip)))
 
         self.redrawTics()
-        
+
     def redrawTics(self):
         if hasattr(self, 'ticsGroup'):
             self.ticsGroup.remove()
         self.ticsGroup = GooCanvas.CanvasGroup(parent=self.root)
 
-        lastx =- 1000
+        lastx = -1000
 
-        for t in range(0,int(self.maxtime)):
+        for t in range(0, int(self.maxtime)):
             x = self.can_for_t(t)
             if 0 < x < self.size.width and x - lastx > 30:
                 txt = str(t)
                 if lastx == -1000:
                     txt = txt + "sec"
                 GooCanvas.CanvasPolyline(parent=self.ticsGroup,
-                                   points=Points([(x, 0), (x, 15)]),
-                                   line_width=.8,
-                                   stroke_color='black')
+                                         points=Points([(x, 0), (x, 15)]),
+                                         line_width=.8,
+                                         stroke_color='black')
                 GooCanvas.CanvasText(parent=self.ticsGroup,
-                               x=x, y=self.size.height-1,
-                               anchor=GooCanvas.CanvasAnchorType.SOUTH,
-                               text=txt,
-                               font='ubuntu 7')
+                                     x=x,
+                                     y=self.size.height - 1,
+                                     anchor=GooCanvas.CanvasAnchorType.SOUTH,
+                                     text=txt,
+                                     font='ubuntu 7')
                 lastx = x
 
 
@@ -251,22 +273,23 @@
 
     this is used with Curveview
     """
+
     def __init__(self, canvas, world_from_screen, screen_from_world):
         self.canvas, self.world_from_screen = canvas, world_from_screen
         self.screen_from_world = screen_from_world
 
-        for evtype, method in [("ButtonPress-1",self.press),
-                               ("Motion",self.motion),
-                               ("ButtonRelease-1",self.release)]:
+        for evtype, method in [("ButtonPress-1", self.press),
+                               ("Motion", self.motion),
+                               ("ButtonRelease-1", self.release)]:
             #canvas.bind("<Control-Alt-%s>" % evtype, method, add=True)
             if 1 or evtype != "ButtonPress-1":
-                canvas.bind("<%s>" % evtype, method,add=True)
+                canvas.bind("<%s>" % evtype, method, add=True)
 
         canvas.bind("<Leave>", self.finish)
         self.start_t = self.old_cursor = None
         self.state = self.mods = None
 
-    def press(self,ev):
+    def press(self, ev):
         if self.state is not None:
             self.finish()
 
@@ -277,19 +300,24 @@
             # sketching handler gets the event
             self.mods = "c-s-a"
         elif ev.state == 0:
-            return # no 
+            return  # no
             self.mods = "none"
         else:
             return
         self.state = "buttonpress"
-            
-        self.start_t = self.end_t = self.world_from_screen(ev.x,0)[0]
+
+        self.start_t = self.end_t = self.world_from_screen(ev.x, 0)[0]
         self.start_x = ev.x
         can = self.canvas
 
-        for pos in ('start_t','end_t','hi','lo'):
-            can.create_line(0,0,50,50, width=3, fill='yellow',
-                            tags=("regionzoom",pos))
+        for pos in ('start_t', 'end_t', 'hi', 'lo'):
+            can.create_line(0,
+                            0,
+                            50,
+                            50,
+                            width=3,
+                            fill='yellow',
+                            tags=("regionzoom", pos))
         # if updatelines isn't called here, subsequent updatelines
         # will fail for reasons i don't understand
         self.updatelines()
@@ -297,41 +325,41 @@
         # todo: just holding the modifiers ought to turn on the zoom
         # cursor (which is not finished)
         cursors.push(can, "@light9/cursor1.xbm")
-        
+
     def updatelines(self):
 
         # better would be a gray25 rectangle over the region
-        
+
         can = self.canvas
         pos_x = {}
         height = can.winfo_height()
         for pos in ('start_t', 'end_t'):
-            pos_x[pos] = x = self.screen_from_world((getattr(self,pos),0))[0]
+            pos_x[pos] = x = self.screen_from_world((getattr(self, pos), 0))[0]
             cid = can.find_withtag("regionzoom && %s" % pos)
             can.coords(cid, x, 0, x, height)
-            
-        for tag,frac in [('hi',.1),('lo',.9)]:
+
+        for tag, frac in [('hi', .1), ('lo', .9)]:
             cid = can.find_withtag("regionzoom && %s" % tag)
-            can.coords(cid, pos_x['start_t'], frac * height,
-                       pos_x['end_t'], frac * height)
+            can.coords(cid, pos_x['start_t'], frac * height, pos_x['end_t'],
+                       frac * height)
 
-    def motion(self,ev):
+    def motion(self, ev):
         if self.state != "buttonpress":
             return
 
-        self.end_t = self.world_from_screen(ev.x,0)[0]
+        self.end_t = self.world_from_screen(ev.x, 0)[0]
         self.updatelines()
 
-    def release(self,ev):
+    def release(self, ev):
         if self.state != "buttonpress":
             return
-        
+
         if abs(self.start_x - ev.x) < 10:
             # clicked
             if self.mods in ("c-a", "c-s-a"):
-                factor = 1/1.5
+                factor = 1 / 1.5
                 if self.mods == "c-s-a":
-                    factor = 1.5 # c-s-a-b1 zooms out
+                    factor = 1.5  # c-s-a-b1 zooms out
                 dispatcher.send("zoom about mouse",
                                 t=self.start_t,
                                 factor=factor)
@@ -339,13 +367,14 @@
             self.finish()
             return
 
-        start,end = min(self.start_t, self.end_t),max(self.start_t, self.end_t)
+        start, end = min(self.start_t, self.end_t), max(self.start_t,
+                                                        self.end_t)
         if self.mods == "c-a":
             dispatcher.send("zoom to range", start=start, end=end)
         elif self.mods == "none":
             dispatcher.send("select between", start=start, end=end)
         self.finish()
-        
+
     def finish(self, *ev):
         if self.state is not None:
             self.state = None
--- a/light9/dmxchanedit.py	Tue May 21 23:55:35 2019 +0000
+++ b/light9/dmxchanedit.py	Tue May 21 23:56:12 2019 +0000
@@ -16,7 +16,7 @@
 - we have to stop packing these into the names. Names should be like 'b33' or 'blue3' or just '44'. maybe 'blacklight'.
 
 """
-from __future__ import nested_scopes,division
+from __future__ import nested_scopes, division
 import Tkinter as tk
 from rdflib import RDF, Literal
 import math, logging
@@ -25,10 +25,12 @@
 log = logging.getLogger('dmxchanedit')
 stdfont = ('Arial', 7)
 
-def gradient(lev, low=(80,80,180), high=(255,55,50)):
-     out = [int(l+lev*(h-l)) for h,l in zip(high,low)]
-     col="#%02X%02X%02X" % tuple(out)
-     return col
+
+def gradient(lev, low=(80, 80, 180), high=(255, 55, 50)):
+    out = [int(l + lev * (h - l)) for h, l in zip(high, low)]
+    col = "#%02X%02X%02X" % tuple(out)
+    return col
+
 
 class Onelevel(tk.Frame):
     """a name/level pair
@@ -48,80 +50,102 @@
     which I'm doing for what I think is efficiency. Unclear why I
     didn't use Observable for that API.
     """
+
     def __init__(self, parent, graph, channelUri, onLevelChange):
-        tk.Frame.__init__(self,parent, height=20)
+        tk.Frame.__init__(self, parent, height=20)
         self.graph = graph
         self.onLevelChange = onLevelChange
         self.uri = channelUri
-        self.currentLevel = 0 # the level we're displaying, 0..1
+        self.currentLevel = 0  # the level we're displaying, 0..1
 
         # no statement yet
         self.channelnum = int(
-             self.graph.value(self.uri, L9['output']).rsplit('/c')[-1])
+            self.graph.value(self.uri, L9['output']).rsplit('/c')[-1])
 
         # 3 widgets, left-to-right:
 
         # channel number -- will turn yellow when being altered
-        self.num_lab = tk.Label(self, text=str(self.channelnum),
-                                width=3, bg='grey40',
+        self.num_lab = tk.Label(self,
+                                text=str(self.channelnum),
+                                width=3,
+                                bg='grey40',
                                 fg='white',
                                 font=stdfont,
-                                padx=0, pady=0, bd=0, height=1)
+                                padx=0,
+                                pady=0,
+                                bd=0,
+                                height=1)
         self.num_lab.pack(side='left')
 
         # text description of channel
-        self.desc_lab=tk.Label(self,
-                               width=14,
-                               font=stdfont,
-                               anchor='w',
-                               padx=0, pady=0, bd=0,
-                 height=1, bg='black', fg='white')
+        self.desc_lab = tk.Label(self,
+                                 width=14,
+                                 font=stdfont,
+                                 anchor='w',
+                                 padx=0,
+                                 pady=0,
+                                 bd=0,
+                                 height=1,
+                                 bg='black',
+                                 fg='white')
         self.graph.addHandler(self.updateLabel)
         self.desc_lab.pack(side='left')
 
         # current level of channel, shows intensity with color
-        self.level_lab = tk.Label(self, width=3, bg='lightBlue',
-                                  anchor='e', font=stdfont,
-                                  padx=1, pady=0, bd=0, height=1)
+        self.level_lab = tk.Label(self,
+                                  width=3,
+                                  bg='lightBlue',
+                                  anchor='e',
+                                  font=stdfont,
+                                  padx=1,
+                                  pady=0,
+                                  bd=0,
+                                  height=1)
         self.level_lab.pack(side='left')
 
         self.setupmousebindings()
 
     def updateLabel(self):
-         self.desc_lab.config(text=self.graph.label(self.uri))
+        self.desc_lab.config(text=self.graph.label(self.uri))
 
     def setupmousebindings(self):
+
         def b1down(ev):
             self.desc_lab.config(bg='cyan')
-            self._start_y=ev.y
-            self._start_lev=self.currentLevel
+            self._start_y = ev.y
+            self._start_lev = self.currentLevel
+
         def b1motion(ev):
-            delta=self._start_y-ev.y
-            self.setlevel(max(0, min(1, self._start_lev+delta*.005)))
+            delta = self._start_y - ev.y
+            self.setlevel(max(0, min(1, self._start_lev + delta * .005)))
+
         def b1up(ev):
             self.desc_lab.config(bg='black')
+
         def b3up(ev):
             self.setlevel(0.0)
+
         def b3down(ev):
             self.setlevel(1.0)
-        def b2down(ev): # same thing for now
+
+        def b2down(ev):  # same thing for now
             self.setlevel(1.0)
 
         # make the buttons work in the child windows
         for w in self.winfo_children():
-            for e,func in (('<ButtonPress-1>',b1down),
-                           ('<B1-Motion>',b1motion),
-                           ('<ButtonRelease-1>',b1up),
-                           ('<ButtonPress-2>', b2down),
-                           ('<ButtonRelease-3>', b3up),
-                           ('<ButtonPress-3>', b3down)):
+            for e, func in (('<ButtonPress-1>',
+                             b1down), ('<B1-Motion>',
+                                       b1motion), ('<ButtonRelease-1>', b1up),
+                            ('<ButtonPress-2>',
+                             b2down), ('<ButtonRelease-3>',
+                                       b3up), ('<ButtonPress-3>', b3down)):
 
-                w.bind(e,func)
+                w.bind(e, func)
 
     def colorlabel(self):
         """color the level label based on its own text (which is 0..100)"""
-        txt=self.level_lab['text'] or "0"
-        lev=float(txt)/100
+        txt = self.level_lab['text'] or "0"
+        lev = float(txt) / 100
         self.level_lab.config(bg=gradient(lev))
 
     def setlevel(self, newlev):
@@ -132,7 +156,7 @@
         """levelbox saw a change in the graph"""
         self.currentLevel = min(1, max(0, newLevel))
         newLevel = "%d" % (self.currentLevel * 100)
-        olddisplay=self.level_lab.cget('text')
+        olddisplay = self.level_lab.cget('text')
         if newLevel != olddisplay:
             self.level_lab.config(text=newLevel)
             self.colorlabel()
@@ -142,37 +166,40 @@
     """
     this also watches all the levels in the sub and sets the boxes when they change
     """
+
     def __init__(self, parent, graph, currentSub):
         """
         currentSub is an Observable(PersistentSubmaster)
         """
-        tk.Frame.__init__(self,parent)
+        tk.Frame.__init__(self, parent)
 
         self.currentSub = currentSub
         self.graph = graph
         graph.addHandler(self.updateChannels)
 
-        self.currentSub.subscribe(lambda _: graph.addHandler(self.updateLevelValues))
+        self.currentSub.subscribe(lambda _: graph.addHandler(self.
+                                                             updateLevelValues))
 
     def updateChannels(self):
         """(re)make Onelevel boxes for the defined channels"""
 
         [ch.destroy() for ch in self.winfo_children()]
-        self.levelFromUri = {} # channel : OneLevel
+        self.levelFromUri = {}  # channel : OneLevel
 
         chans = list(self.graph.subjects(RDF.type, L9.Channel))
-        chans.sort(key=lambda c: int(self.graph.value(c, L9.output).rsplit('/c')[-1]))
+        chans.sort(
+            key=lambda c: int(self.graph.value(c, L9.output).rsplit('/c')[-1]))
         cols = 2
         rows = int(math.ceil(len(chans) / cols))
 
         def make_frame(parent):
-             f = tk.Frame(parent, bd=0, bg='black')
-             f.pack(side='left')
-             return f
+            f = tk.Frame(parent, bd=0, bg='black')
+            f.pack(side='left')
+            return f
 
         columnFrames = [make_frame(self) for x in range(cols)]
 
-        for i, channel in enumerate(chans): # sort?
+        for i, channel in enumerate(chans):  # sort?
             # frame for this channel
             f = Onelevel(columnFrames[i // rows], self.graph, channel,
                          self.onLevelChange)
@@ -193,19 +220,19 @@
         for ll in self.graph.objects(sub, L9['lightLevel']):
             chan = self.graph.value(ll, L9['channel'])
             try:
-                 lev = self.graph.value(ll, L9['level']).toPython()
+                lev = self.graph.value(ll, L9['level']).toPython()
             except AttributeError as e:
-                 log.error('on lightlevel %r:', ll)
-                 log.exception(e)
-                 continue
+                log.error('on lightlevel %r:', ll)
+                log.exception(e)
+                continue
             if isinstance(lev, Decimal):
-                 lev = float(lev)
+                lev = float(lev)
             assert isinstance(lev, (int, long, float)), repr(lev)
             try:
-                 self.levelFromUri[chan].setTo(lev)
-                 remaining.remove(chan)
+                self.levelFromUri[chan].setTo(lev)
+                remaining.remove(chan)
             except KeyError as e:
-                 log.exception(e)
+                log.exception(e)
         for channel in remaining:
             self.levelFromUri[channel].setTo(0)
 
@@ -214,4 +241,3 @@
         if self.currentSub() is None:
             raise ValueError("no currentSub in Levelbox")
         self.currentSub().editLevel(chan, newLevel)
-
--- a/light9/dmxclient.py	Tue May 21 23:55:35 2019 +0000
+++ b/light9/dmxclient.py	Tue May 21 23:56:12 2019 +0000
@@ -10,25 +10,34 @@
 import json
 
 from light9 import networking
-_dmx=None
+_dmx = None
 log = logging.getLogger('dmxclient')
 
 procname = os.path.basename(sys.argv[0])
 procname = procname.replace('.py', '')
 _id = "%s-%s-%s" % (procname, socket.gethostname(), os.getpid())
 
+
 class TwistedZmqClient(object):
+
     def __init__(self, service):
         zf = ZmqFactory()
         e = ZmqEndpoint('connect', 'tcp://%s:%s' % (service.host, service.port))
+
         class Push(ZmqPushConnection):
-            pass # highWaterMark = 3000
+            pass  # highWaterMark = 3000
+
         self.conn = Push(zf, e)
-        
+
     def send(self, clientid, levellist):
-        self.conn.push(json.dumps({'clientid': clientid, 'levellist': levellist}))
+        self.conn.push(
+            json.dumps({
+                'clientid': clientid,
+                'levellist': levellist
+            }))
 
-def outputlevels(levellist,twisted=0,clientid=_id):
+
+def outputlevels(levellist, twisted=0, clientid=_id):
     """present a list of dmx channel levels, each scaled from
     0..1. list can be any length- it will apply to the first len() dmx
     channels.
@@ -51,15 +60,17 @@
         except socket.error, e:
             log.error("dmx server error %s, waiting" % e)
             time.sleep(1)
-        except xmlrpclib.Fault,e:
+        except xmlrpclib.Fault, e:
             log.error("outputlevels had xml fault: %s" % e)
             time.sleep(1)
     else:
         _dmx.send(clientid, levellist)
         return defer.succeed(None)
-    
+
+
 dummy = os.getenv('DMXDUMMY')
 if dummy:
     print "dmxclient: DMX is in dummy mode."
+
     def outputlevels(*args, **kw):
         pass
--- a/light9/editchoice.py	Tue May 21 23:55:35 2019 +0000
+++ b/light9/editchoice.py	Tue May 21 23:56:12 2019 +0000
@@ -2,11 +2,13 @@
 from rdflib import URIRef
 from light9.tkdnd import dragSourceRegister, dropTargetRegister
 
+
 class Local(object):
     """placeholder for the local uri that EditChoice does not
     manage. Set resourceObservable to Local to indicate that you're
     unlinked"""
 
+
 class EditChoice(object):
     """
     Observable <-> linker UI
@@ -52,6 +54,7 @@
 
     - list of recent resources that this choice was set to
     """
+
     def __init__(self, parent, graph, resourceObservable, label="Editing:"):
         """
         getResource is called to get the URI of the currently
@@ -63,9 +66,12 @@
         self.currentLinkFrame = tk.Frame(self.frame)
         self.currentLinkFrame.pack(side='left')
 
-        self.subIcon = tk.Label(self.currentLinkFrame, text="...",
-                                borderwidth=2, relief='raised',
-                                padx=10, pady=10)
+        self.subIcon = tk.Label(self.currentLinkFrame,
+                                text="...",
+                                borderwidth=2,
+                                relief='raised',
+                                padx=10,
+                                pady=10)
         self.subIcon.pack()
 
         self.resourceObservable = resourceObservable
@@ -74,12 +80,14 @@
         # when the value is local, this should stop being a drag source
         dragSourceRegister(self.subIcon, 'copy', 'text/uri-list',
                            self.resourceObservable)
+
         def onEv(ev):
             self.resourceObservable(URIRef(ev.data))
             return "link"
+
         self.onEv = onEv
 
-        b=tk.Button(self.frame, text="Unlink", command=self.switchToLocalSub)
+        b = tk.Button(self.frame, text="Unlink", command=self.switchToLocalSub)
         b.pack(side='left')
 
         # it would be nice if I didn't receive my own drags here, and
@@ -87,7 +95,9 @@
         for target in ([self.frame, self.currentLinkFrame] +
                        self.frame.winfo_children() +
                        self.currentLinkFrame.winfo_children()):
-            dropTargetRegister(target, typeList=["*"], onDrop=onEv,
+            dropTargetRegister(target,
+                               typeList=["*"],
+                               onDrop=onEv,
                                hoverStyle=dict(background="#555500"))
 
     def uriChanged(self, newUri):
@@ -106,4 +116,3 @@
 
     def switchToLocalSub(self):
         self.resourceObservable(Local)
-
--- a/light9/editchoicegtk.py	Tue May 21 23:55:35 2019 +0000
+++ b/light9/editchoicegtk.py	Tue May 21 23:56:12 2019 +0000
@@ -4,15 +4,18 @@
 from rdflib import URIRef
 log = logging.getLogger('editchoicegtk')
 
+
 class Local(object):
     """placeholder for the local uri that EditChoice does not
     manage. Set resourceObservable to Local to indicate that you're
     unlinked"""
 
+
 class EditChoice(Gtk.HBox):
     """
     this is a gtk port of editchoice.EditChoice
     """
+
     def __init__(self, graph, resourceObservable, label="Editing:"):
         """
         getResource is called to get the URI of the currently
@@ -22,23 +25,19 @@
         # the outer box should have a distinctive border so it's more
         # obviously a special drop target
         Gtk.HBox.__init__(self)
-        self.pack_start(Gtk.Label(label),
-                        False, True, 0) #expand, fill, pad
+        self.pack_start(Gtk.Label(label), False, True, 0)  #expand, fill, pad
 
         # this is just a label, but it should look like a physical
         # 'thing' (and gtk labels don't work as drag sources)
         self.currentLink = Gtk.Button("http://bar")
 
-        self.pack_start(self.currentLink,
-                        True, True, 0) #expand, fill, pad
-
+        self.pack_start(self.currentLink, True, True, 0)  #expand, fill, pad
 
         self.unlinkButton = Gtk.Button(label="Unlink")
-        self.pack_start(self.unlinkButton,
-                        False, True, 0) #expand, fill pad
+        self.pack_start(self.unlinkButton, False, True, 0)  #expand, fill pad
 
         self.unlinkButton.connect("clicked", self.onUnlink)
-        
+
         self.show_all()
 
         self.resourceObservable = resourceObservable
@@ -46,8 +45,9 @@
 
         self.makeDragSource()
         self.makeDropTarget()
-         
+
     def makeDropTarget(self):
+
         def ddr(widget, drag_context, x, y, selection_data, info, timestamp):
             dtype = selection_data.get_data_type()
             if dtype.name() not in ['text/uri-list', 'TEXT']:
@@ -55,28 +55,30 @@
             data = selection_data.get_data().strip()
             log.debug('drag_data_received data=%r', data)
             self.resourceObservable(URIRef(data))
-        
+
         self.currentLink.drag_dest_set(
             flags=Gtk.DestDefaults.ALL,
-            targets=[Gtk.TargetEntry.new('text/uri-list', 0, 0),
-                     Gtk.TargetEntry.new('TEXT', 0, 0), # getting this from chrome :(
-                 ],
-            actions=Gdk.DragAction.LINK  | Gdk.DragAction.COPY)
+            targets=[
+                Gtk.TargetEntry.new('text/uri-list', 0, 0),
+                Gtk.TargetEntry.new('TEXT', 0,
+                                    0),  # getting this from chrome :(
+            ],
+            actions=Gdk.DragAction.LINK | Gdk.DragAction.COPY)
         self.currentLink.connect("drag_data_received", ddr)
-                
+
     def makeDragSource(self):
         self.currentLink.drag_source_set(
             start_button_mask=Gdk.ModifierType.BUTTON1_MASK,
-            targets=[Gtk.TargetEntry.new(target='text/uri-list',
-                                         flags=0, info=0)],
-            actions=Gdk.DragAction.LINK  | Gdk.DragAction.COPY)
+            targets=[
+                Gtk.TargetEntry.new(target='text/uri-list', flags=0, info=0)
+            ],
+            actions=Gdk.DragAction.LINK | Gdk.DragAction.COPY)
 
         def source_drag_data_get(btn, context, selection_data, info, time):
             selection_data.set_uris([self.resourceObservable()])
 
         self.currentLink.connect("drag_data_get", source_drag_data_get)
 
-                
     def uriChanged(self, newUri):
         # if this resource had a type icon or a thumbnail, those would be
         # cool to show in here too
--- a/light9/effect/edit.py	Tue May 21 23:55:35 2019 +0000
+++ b/light9/effect/edit.py	Tue May 21 23:56:12 2019 +0000
@@ -8,22 +8,25 @@
 from rdfdb.patch import Patch
 from light9.curvecalc.curve import CurveResource
 
+
 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))
+    returnValue(
+        json.loads(
+            (yield cyclone.httpclient.fetch(networking.musicPlayer.path('time'),
+                                            timeout=.5)).body))
+
 
 @inlineCallbacks
 def songEffectPatch(graph, dropped, song, event, ctx):
     """
     some uri was 'dropped' in the curvecalc timeline. event is 'default' or 'start' or 'end'.
     """
-    with graph.currentState(
-            tripleFilter=(dropped, None, None)) as g:
+    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']))
@@ -44,17 +47,17 @@
             (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])
+            ] + [(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)" %
@@ -67,6 +70,7 @@
         print qq
     returnValue(Patch(addQuads=quads))
 
+
 @inlineCallbacks
 def songNotePatch(graph, dropped, song, event, ctx, note=None):
     """
@@ -74,8 +78,7 @@
 
     ported from timeline.coffee makeNewNote
     """
-    with graph.currentState(
-            tripleFilter=(dropped, None, None)) as g:
+    with graph.currentState(tripleFilter=(dropped, None, None)) as g:
         droppedTypes = list(g.objects(dropped, RDF.type))
 
     quads = []
@@ -89,7 +92,8 @@
         if L9['Effect'] in droppedTypes:
             musicStatus = yield getMusicStatus()
             songTime = musicStatus['t']
-            note = _makeNote(graph, song, note, quads, ctx, dropped, songTime, event, fade)
+            note = _makeNote(graph, song, note, quads, ctx, dropped, songTime,
+                             event, fade)
         else:
             raise NotImplementedError
 
@@ -97,11 +101,10 @@
 
 
 def _point(ctx, uri, t, v):
-    return [
-        (uri, L9['time'], Literal(round(t, 3)), ctx),
-        (uri, L9['value'], Literal(round(v, 3)), ctx)
-        ]
-    
+    return [(uri, L9['time'], Literal(round(t, 3)), ctx),
+            (uri, L9['value'], Literal(round(v, 3)), ctx)]
+
+
 def _finishCurve(graph, note, quads, ctx, songTime):
     with graph.currentState() as g:
         origin = g.value(note, L9['originTime']).toPython()
@@ -109,11 +112,11 @@
 
     pt2 = graph.sequentialUri(curve + 'p')
     pt3 = graph.sequentialUri(curve + 'p')
-    quads.extend(
-        [(curve, L9['point'], pt2, ctx)] + _point(ctx, pt2, songTime - origin, 1) +
-        [(curve, L9['point'], pt3, ctx)] + _point(ctx, pt3, songTime - origin + .5, 0)
-        )
-    
+    quads.extend([(curve, L9['point'], pt2, ctx)] +
+                 _point(ctx, pt2, songTime - origin, 1) +
+                 [(curve, L9['point'], pt3, ctx)] +
+                 _point(ctx, pt3, songTime - origin + .5, 0))
+
 
 def _makeNote(graph, song, note, quads, ctx, dropped, songTime, event, fade):
     note = graph.sequentialUri(song + '/n')
@@ -126,28 +129,34 @@
         (note, L9['originTime'], Literal(songTime), ctx),
         (curve, RDF.type, L9['Curve'], ctx),
         (curve, L9['attr'], L9['strength'], ctx),
-        ])
+    ])
     if event == 'default':
         coords = [(0 - fade, 0), (0, 1), (20, 1), (20 + fade, 0)]
     elif event == 'start':
-        coords = [(0 - fade, 0), (0, 1), ]
-    elif event == 'end': # probably unused- goes to _finishCurve instead
+        coords = [
+            (0 - fade, 0),
+            (0, 1),
+        ]
+    elif event == 'end':  # probably unused- goes to _finishCurve instead
         coords = [(20, 1), (20 + fade, 0)]
     else:
         raise NotImplementedError(event)
-    for t,v in coords:
+    for t, v in coords:
         pt = graph.sequentialUri(curve + 'p')
         quads.extend([(curve, L9['point'], pt, ctx)] + _point(ctx, pt, t, v))
     return note
-    
+
+
 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
+    return False  # todo
+
 
 def musicCurveForSong(uri):
     return URIRef(uri + 'music')
-    
+
+
 def _newEffect(graph, song, ctx):
     effect = graph.sequentialUri(song + "/effect-")
     quads = [
@@ -156,42 +165,43 @@
     ]
     print "_newEffect", effect, quads
     return effect, quads
-    
+
+
 @inlineCallbacks
 def _newEnvelopeCurve(graph, ctx, uri, label, fade=2):
     """this does its own patch to the graph"""
-    
+
     cr = CurveResource(graph, uri)
     cr.newCurve(ctx, label=Literal(label))
     yield _insertEnvelopePoints(cr.curve, fade)
     cr.saveCurve()
 
+
 @inlineCallbacks
 def _insertEnvelopePoints(curve, fade=2):
     # wrong: we might not be adding to the currently-playing song.
     musicStatus = yield getMusicStatus()
-    songTime=musicStatus['t']
-    songDuration=musicStatus['duration']
-    
+    songTime = musicStatus['t']
+    songDuration = musicStatus['duration']
+
     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 _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)
-                ])
+            quads.extend([(effect, L9['code'],
+                           Literal('music = %s' % musicCurveForSong(song).n3()),
+                           ctx)])
             break
--- a/light9/effect/effecteval.py	Tue May 21 23:55:35 2019 +0000
+++ b/light9/effect/effecteval.py	Tue May 21 23:56:12 2019 +0000
@@ -17,28 +17,48 @@
 
 log = logging.getLogger('effecteval')
 
+
 def literalColor(rnorm, gnorm, bnorm):
-    return Literal(rgb_to_hex([int(rnorm * 255), int(gnorm * 255), int(bnorm * 255)]))
+    return Literal(
+        rgb_to_hex([int(rnorm * 255),
+                    int(gnorm * 255),
+                    int(bnorm * 255)]))
+
 
 def literalColorHsv(h, s, v):
     return literalColor(*hsv_to_rgb(h, s, v))
-    
-def nsin(x): return (math.sin(x * (2 * math.pi)) + 1) / 2
-def ncos(x): return (math.cos(x * (2 * math.pi)) + 1) / 2
+
+
+def nsin(x):
+    return (math.sin(x * (2 * math.pi)) + 1) / 2
+
+
+def ncos(x):
+    return (math.cos(x * (2 * math.pi)) + 1) / 2
+
+
 def nsquare(t, on=.5):
     return (t % 1.0) < on
+
+
 def lerp(a, b, t):
     return a + (b - a) * t
+
+
 def noise(t):
     return pnoise1(t % 1000.0, 2)
+
+
 def clamp(lo, hi, x):
     return max(lo, min(hi, x))
-    
+
+
 class EffectEval(object):
     """
     runs one effect's code to turn effect attr settings into output
     device settings. No state; suitable for reload().
     """
+
     def __init__(self, graph, effect, simpleOutputs):
         self.graph = graph
         self.effect = effect
@@ -51,17 +71,21 @@
         the effect code.
         """
         # both callers need to apply note overrides
-        effectSettings = dict(effectSettings) # we should make everything into nice float and Color objects too
+        effectSettings = dict(
+            effectSettings
+        )  # we should make everything into nice float and Color objects too
 
         strength = float(effectSettings[L9['strength']])
         if strength <= 0:
             return DeviceSettings(self.graph, []), {'zero': True}
 
         report = {}
-        out = {} # (dev, attr): value
+        out = {}  # (dev, attr): value
 
-        out.update(self.simpleOutputs.values(
-            self.effect, strength, effectSettings.get(L9['colorScale'], None)))
+        out.update(
+            self.simpleOutputs.values(
+                self.effect, strength,
+                effectSettings.get(L9['colorScale'], None)))
 
         if self.effect.startswith(L9['effect/']):
             tail = 'effect_' + self.effect[len(L9['effect/']):]
@@ -74,46 +98,48 @@
 
         outList = [(d, a, v) for (d, a), v in out.iteritems()]
         return DeviceSettings(self.graph, outList), report
-                  
+
 
 def effect_Curtain(effectSettings, strength, songTime, noteTime):
-    return {
-        (L9['device/lowPattern%s' % n], L9['color']):
-        literalColor(strength, strength, strength)
-        for n in range(301,308+1)
-        }
-    
+    return {(L9['device/lowPattern%s' % n], L9['color']):
+            literalColor(strength, strength, strength)
+            for n in range(301, 308 + 1)}
+
+
 def effect_animRainbow(effectSettings, strength, songTime, noteTime):
     out = {}
     tint = effectSettings.get(L9['tint'], '#ffffff')
     tintStrength = float(effectSettings.get(L9['tintStrength'], 0))
     tr, tg, tb = hex_to_rgb(tint)
-    for n in range(1, 5+1):
+    for n in range(1, 5 + 1):
         scl = strength * nsin(songTime + n * .3)**3
         col = literalColor(
-            scl * lerp(nsin(songTime + n * .2), tr/255, tintStrength),
-            scl * lerp(nsin(songTime + n * .2 + .3), tg/255, tintStrength),
-            scl * lerp(nsin(songTime + n * .3 + .6), tb/255, tintStrength))
+            scl * lerp(nsin(songTime + n * .2), tr / 255, tintStrength),
+            scl * lerp(nsin(songTime + n * .2 + .3), tg / 255, tintStrength),
+            scl * lerp(nsin(songTime + n * .3 + .6), tb / 255, tintStrength))
 
         dev = L9['device/aura%s' % n]
         out.update({
             (dev, L9['color']): col,
             (dev, L9['zoom']): .9,
-            })
+        })
         ang = songTime * 4
         out.update({
-        (dev, L9['rx']): lerp(.27, .7, (n-1)/4) + .2 * math.sin(ang+n),
-        (dev, L9['ry']): lerp(.46, .52, (n-1)/4) + .5 * math.cos(ang+n),
-            })
+            (dev, L9['rx']):
+            lerp(.27, .7, (n - 1) / 4) + .2 * math.sin(ang + n),
+            (dev, L9['ry']):
+            lerp(.46, .52, (n - 1) / 4) + .5 * math.cos(ang + n),
+        })
     return out
 
+
 def effect_auraSparkles(effectSettings, strength, songTime, noteTime):
     out = {}
     tint = effectSettings.get(L9['tint'], '#ffffff')
     tintStrength = float(effectSettings.get(L9['tintStrength'], 0))
     print effectSettings
     tr, tg, tb = hex_to_rgb(tint)
-    for n in range(1, 5+1):
+    for n in range(1, 5 + 1):
         scl = strength * ((int(songTime * 10) % n) < 1)
         col = literalColorHsv((songTime + (n / 5)) % 1, 1, scl)
 
@@ -121,14 +147,17 @@
         out.update({
             (dev, L9['color']): col,
             (dev, L9['zoom']): .95,
-            })
+        })
         ang = songTime * 4
         out.update({
-        (dev, L9['rx']): lerp(.27, .8, (n-1)/4) + .2 * math.sin(ang+n),
-        (dev, L9['ry']): lerp(.46, .52, (n-1)/4) + .4 * math.cos(ang+n),
-            })
+            (dev, L9['rx']):
+            lerp(.27, .8, (n - 1) / 4) + .2 * math.sin(ang + n),
+            (dev, L9['ry']):
+            lerp(.46, .52, (n - 1) / 4) + .4 * math.cos(ang + n),
+        })
     return out
 
+
 def effect_qpan(effectSettings, strength, songTime, noteTime):
     dev = L9['device/q2']
     dur = 4
@@ -142,57 +171,59 @@
         (dev, L9['zoom']): 0.714,
     }
 
+
 def effect_pulseRainbow(effectSettings, strength, songTime, noteTime):
     out = {}
     tint = effectSettings.get(L9['tint'], '#ffffff')
     tintStrength = float(effectSettings.get(L9['tintStrength'], 0))
     tr, tg, tb = hex_to_rgb(tint)
-    for n in range(1, 5+1):
-        scl = strength 
+    for n in range(1, 5 + 1):
+        scl = strength
         col = literalColor(
-            scl * lerp(nsin(songTime + n * .2), tr/255, tintStrength),
-            scl * lerp(nsin(songTime + n * .2 + .3), tg/255, tintStrength),
-            scl * lerp(nsin(songTime + n * .3 + .6), tb/255, tintStrength))
+            scl * lerp(nsin(songTime + n * .2), tr / 255, tintStrength),
+            scl * lerp(nsin(songTime + n * .2 + .3), tg / 255, tintStrength),
+            scl * lerp(nsin(songTime + n * .3 + .6), tb / 255, tintStrength))
 
         dev = L9['device/aura%s' % n]
         out.update({
             (dev, L9['color']): col,
             (dev, L9['zoom']): .5,
-            })
+        })
         out.update({
-        (dev, L9['rx']): lerp(.27, .7, (n-1)/4),
-        (dev, L9['ry']): lerp(.46, .52, (n-1)/4),
-            })
+            (dev, L9['rx']): lerp(.27, .7, (n - 1) / 4),
+            (dev, L9['ry']): lerp(.46, .52, (n - 1) / 4),
+        })
     return out
 
 
 def effect_aurawash(effectSettings, strength, songTime, noteTime):
     out = {}
     scl = strength
-    period = float(effectSettings.get(L9['period'], 125/60/4))
+    period = float(effectSettings.get(L9['period'], 125 / 60 / 4))
     if period < .05:
         quantTime = songTime
     else:
         quantTime = int(songTime / period) * period
     noisePos = quantTime * 6.3456
-    
+
     col = literalColorHsv(noise(noisePos), 1, scl)
     col = scale(col, effectSettings.get(L9['colorScale']) or '#ffffff')
-                
+
     print songTime, quantTime, col
 
-    for n in range(1, 5+1):
+    for n in range(1, 5 + 1):
         dev = L9['device/aura%s' % n]
         out.update({
             (dev, L9['color']): col,
             (dev, L9['zoom']): .5,
-            })
+        })
         out.update({
-        (dev, L9['rx']): lerp(.27, .7, (n-1)/4),
-        (dev, L9['ry']): lerp(.46, .52, (n-1)/4),
-            })
+            (dev, L9['rx']): lerp(.27, .7, (n - 1) / 4),
+            (dev, L9['ry']): lerp(.46, .52, (n - 1) / 4),
+        })
     return out
 
+
 def effect_qsweep(effectSettings, strength, songTime, noteTime):
     out = {}
     period = float(effectSettings.get(L9['period'], 2))
@@ -200,20 +231,21 @@
     col = effectSettings.get(L9['colorScale'], '#ffffff')
     col = scale(col, effectSettings.get(L9['strength'], 1))
 
-    
-    for n in range(1, 3+1):
+    for n in range(1, 3 + 1):
         dev = L9['device/q%s' % n]
         out.update({
             (dev, L9['color']): col,
             (dev, L9['zoom']): effectSettings.get(L9['zoom'], .5),
-            })
+        })
         out.update({
             (dev, L9['rx']):
             lerp(.3, .8, nsin(songTime / period + n / 4)),
-        (dev, L9['ry']): effectSettings.get(L9['ry'], .2),
-            })
+            (dev, L9['ry']):
+            effectSettings.get(L9['ry'], .2),
+        })
     return out
 
+
 def effect_qsweepusa(effectSettings, strength, songTime, noteTime):
     out = {}
     period = float(effectSettings.get(L9['period'], 2))
@@ -223,43 +255,48 @@
         2: '#998888',
         3: '#0050ff',
     }
-    
-    for n in range(1, 3+1):
+
+    for n in range(1, 3 + 1):
         dev = L9['device/q%s' % n]
         out.update({
-            (dev, L9['color']): scale(colmap[n], effectSettings.get(L9['strength'], 1)),
-            (dev, L9['zoom']): effectSettings.get(L9['zoom'], .5),
-            })
+            (dev, L9['color']):
+            scale(colmap[n], effectSettings.get(L9['strength'], 1)),
+            (dev, L9['zoom']):
+            effectSettings.get(L9['zoom'], .5),
+        })
         out.update({
             (dev, L9['rx']):
             lerp(.3, .8, nsin(songTime / period + n / 4)),
-            (dev, L9['ry']): effectSettings.get(L9['ry'], .5),
-            })
+            (dev, L9['ry']):
+            effectSettings.get(L9['ry'], .5),
+        })
     return out
 
+
 chase1_members = [
-        DEV['backlight1'],
-        DEV['lip1'],
-        DEV['backlight2'],
-        DEV['down2'],
-        DEV['lip2'],
-        DEV['backlight3'],
-        DEV['down3'],
-        DEV['lip3'],
-        DEV['backlight4'],
-        DEV['down4'],
-        DEV['lip4'],
-        DEV['backlight5'],
-        DEV['down5Edge'],
-        DEV['lip5'],
-        #DEV['upCenter'],
-        ]
+    DEV['backlight1'],
+    DEV['lip1'],
+    DEV['backlight2'],
+    DEV['down2'],
+    DEV['lip2'],
+    DEV['backlight3'],
+    DEV['down3'],
+    DEV['lip3'],
+    DEV['backlight4'],
+    DEV['down4'],
+    DEV['lip4'],
+    DEV['backlight5'],
+    DEV['down5Edge'],
+    DEV['lip5'],
+    #DEV['upCenter'],
+]
 chase2_members = chase1_members * 10
 random.shuffle(chase2_members)
 
+
 def effect_chase1(effectSettings, strength, songTime, noteTime):
     members = chase1_members + chase1_members[-2:0:-1]
-    
+
     out = {}
     period = float(effectSettings.get(L9['period'], 2 / len(members)))
 
@@ -271,15 +308,16 @@
             col = effectSettings.get(L9['colorScale'], '#ffffff')
             col = scale(col, effectSettings.get(L9['strength'], 1))
             col = scale(col, (1 - dist / radius))
-        
+
             out.update({
                 (dev, L9['color']): col,
             })
     return out
 
+
 def effect_chase2(effectSettings, strength, songTime, noteTime):
     members = chase2_members
-    
+
     out = {}
     period = float(effectSettings.get(L9['period'], 0.3))
 
@@ -291,37 +329,40 @@
             col = effectSettings.get(L9['colorScale'], '#ffffff')
             col = scale(col, effectSettings.get(L9['strength'], 1))
             col = scale(col, (1 - dist / radius))
-        
+
             out.update({
                 (dev, L9['color']): col,
             })
     return out
-    
+
+
 def effect_whirlscolor(effectSettings, strength, songTime, noteTime):
     out = {}
 
     col = effectSettings.get(L9['colorScale'], '#ffffff')
     col = scale(col, effectSettings.get(L9['strength'], 1))
-    
+
     for n in (1, 3):
         dev = L9['device/q%s' % n]
         scl = strength
         col = literalColorHsv(((songTime / 5) + (n / 5)) % 1, 1, scl)
         out.update({
             (dev, L9['color']): col,
-            })
+        })
 
     return out
 
 
 def effect_orangeSearch(effectSettings, strength, songTime, noteTime):
     dev = L9['device/auraStage']
-    return {(dev, L9['color']): '#a885ff',
-            (dev, L9['rx']): lerp(.65, 1, nsin(songTime / 2.0)),
-            (dev, L9['ry']): .6,
-            (dev, L9['zoom']): 1,
-            }
-    
+    return {
+        (dev, L9['color']): '#a885ff',
+        (dev, L9['rx']): lerp(.65, 1, nsin(songTime / 2.0)),
+        (dev, L9['ry']): .6,
+        (dev, L9['zoom']): 1,
+    }
+
+
 def effect_Strobe(effectSettings, strength, songTime, noteTime):
     rate = 2
     duty = .3
@@ -331,16 +372,16 @@
     col = rgb_to_hex([int(c * 255), int(c * 255), int(c * 255)])
     return {(L9['device/colorStrip'], L9['color']): Literal(col)}
 
+
 def effect_lightning(effectSettings, strength, songTime, noteTime):
-    devs = [L9['device/veryLow1'], L9['device/veryLow2'],
-            L9['device/veryLow3'], L9['device/veryLow4'],
-            L9['device/veryLow5'], L9['device/backlight1'],
-            L9['device/backlight2'], L9['device/backlight3'],
-            L9['device/backlight4'], L9['device/backlight5'],
-            L9['device/down2'], L9['device/down3'],
-            L9['device/down4'], L9['device/hexLow3'],
-            L9['device/hexLow5'],
-            L9['device/postL1'], L9['device/postR1']]
+    devs = [
+        L9['device/veryLow1'], L9['device/veryLow2'], L9['device/veryLow3'],
+        L9['device/veryLow4'], L9['device/veryLow5'], L9['device/backlight1'],
+        L9['device/backlight2'], L9['device/backlight3'],
+        L9['device/backlight4'], L9['device/backlight5'], L9['device/down2'],
+        L9['device/down3'], L9['device/down4'], L9['device/hexLow3'],
+        L9['device/hexLow5'], L9['device/postL1'], L9['device/postR1']
+    ]
     out = {}
     col = rgb_to_hex([int(255 * strength)] * 3)
     for i, dev in enumerate(devs):
--- a/light9/effect/scale.py	Tue May 21 23:55:35 2019 +0000
+++ b/light9/effect/scale.py	Tue May 21 23:56:12 2019 +0000
@@ -10,16 +10,16 @@
 
     if isinstance(value, Decimal):
         value = float(value)
-        
+
     if isinstance(value, basestring):
         if value[0] == '#':
             if strength == '#ffffff':
                 return value
-            r,g,b = hex_to_rgb(value)
+            r, g, b = hex_to_rgb(value)
             if isinstance(strength, Literal):
                 strength = strength.toPython()
             if isinstance(strength, basestring):
-                sr, sg, sb = [v/255 for v in hex_to_rgb(strength)]
+                sr, sg, sb = [v / 255 for v in hex_to_rgb(strength)]
             else:
                 sr = sg = sb = strength
             return rgb_to_hex([int(r * sr), int(g * sg), int(b * sb)])
@@ -27,4 +27,3 @@
         return value * strength
 
     raise NotImplementedError("%r,%r" % (value, strength))
-    
--- a/light9/effect/sequencer.py	Tue May 21 23:55:35 2019 +0000
+++ b/light9/effect/sequencer.py	Tue May 21 23:56:12 2019 +0000
@@ -22,15 +22,17 @@
 from greplin import scales
 
 log = logging.getLogger('sequencer')
-stats = scales.collection('/sequencer/',
-                          scales.PmfStat('update'),
-                          scales.PmfStat('compileGraph'),
-                          scales.PmfStat('compileSong'),
-                          scales.DoubleStat('recentFps'),
+stats = scales.collection(
+    '/sequencer/',
+    scales.PmfStat('update'),
+    scales.PmfStat('compileGraph'),
+    scales.PmfStat('compileSong'),
+    scales.DoubleStat('recentFps'),
 )
 
 
 class Note(object):
+
     def __init__(self, graph, uri, effectevalModule, simpleOutputs):
         g = self.graph = graph
         self.uri = uri
@@ -41,7 +43,7 @@
             settingValues = dict(g.predicate_objects(s))
             ea = settingValues[L9['effectAttr']]
             self.baseEffectSettings[ea] = settingValues[L9['value']]
-            
+
         floatVal = lambda s, p: float(g.value(s, p).toPython())
         originTime = floatVal(uri, L9['originTime'])
         self.points = []
@@ -57,11 +59,10 @@
             return []
         for point in [row[1] for row in po if row[0] == L9['point']]:
             po2 = dict(self.graph.predicate_objects(point))
-            points.append((
-                originTime + float(po2[L9['time']]),
-                float(po2[L9['value']])))
+            points.append(
+                (originTime + float(po2[L9['time']]), float(po2[L9['value']])))
         return points
-            
+
     def activeAt(self, t):
         return self.points[0][0] <= t <= self.points[-1][0]
 
@@ -79,19 +80,19 @@
         frac = (t - p1[0]) / (p2[0] - p1[0])
         y = p1[1] + (p2[1] - p1[1]) * frac
         return y
-        
+
     def outputSettings(self, t):
         """
         list of (device, attr, value), and a report for web
         """
-        report = {'note': str(self.uri),
-                  'effectClass': self.effectEval.effect,
+        report = {
+            'note': str(self.uri),
+            'effectClass': self.effectEval.effect,
         }
         effectSettings = self.baseEffectSettings.copy()
         effectSettings[L9['strength']] = self.evalCurve(t)
         report['effectSettings'] = dict(
-            (str(k), str(v))
-            for k,v in sorted(effectSettings.items()))
+            (str(k), str(v)) for k, v in sorted(effectSettings.items()))
         report['nonZero'] = effectSettings[L9['strength']] > 0
         out, evalReport = self.effectEval.outputFromEffect(
             effectSettings.items(),
@@ -103,26 +104,29 @@
 
 
 class CodeWatcher(object):
+
     def __init__(self, onChange):
         self.onChange = onChange
 
         self.notifier = INotify()
         self.notifier.startReading()
-        self.notifier.watch(
-            FilePath(effecteval.__file__.replace('.pyc', '.py')),
-            callbacks=[self.codeChange])
+        self.notifier.watch(FilePath(effecteval.__file__.replace('.pyc',
+                                                                 '.py')),
+                            callbacks=[self.codeChange])
 
     def codeChange(self, watch, path, mask):
+
         def go():
             log.info("reload effecteval")
             reload(effecteval)
             self.onChange()
+
         # in case we got an event at the start of the write
-        reactor.callLater(.1, go) 
-    
-        
+        reactor.callLater(.1, go)
+
 
 class Sequencer(object):
+
     def __init__(self, graph, sendToCollector, fps=40):
         self.graph = graph
         self.fps = fps
@@ -132,7 +136,7 @@
         self.recentUpdateTimes = []
         self.lastStatLog = 0
         self._compileGraphCall = None
-        self.notes = {} # song: [notes]
+        self.notes = {}  # song: [notes]
         self.simpleOutputs = SimpleOutputs(self.graph)
         self.graph.addHandler(self.compileGraph)
         self.updateLoop()
@@ -149,29 +153,36 @@
         for song in g.subjects(RDF.type, L9['Song']):
             self.graph.addHandler(lambda song=song: self.compileSong(song))
         log.info('compileGraph took %.2f ms', 1000 * (time.time() - t1))
-        
+
     @stats.compileSong.time()
     def compileSong(self, song):
         t1 = time.time()
 
         self.notes[song] = []
         for note in self.graph.objects(song, L9['note']):
-            self.notes[song].append(Note(self.graph, note, effecteval,
-                                         self.simpleOutputs))
+            self.notes[song].append(
+                Note(self.graph, note, effecteval, self.simpleOutputs))
         log.info('  compile %s took %.2f ms', song, 1000 * (time.time() - t1))
 
-
     def updateLoop(self):
         # print "updateLoop"
         now = time.time()
         self.recentUpdateTimes = self.recentUpdateTimes[-40:] + [now]
-        stats.recentFps = len(self.recentUpdateTimes) / (self.recentUpdateTimes[-1] - self.recentUpdateTimes[0] + .0001)
+        stats.recentFps = len(self.recentUpdateTimes) / (
+            self.recentUpdateTimes[-1] - self.recentUpdateTimes[0] + .0001)
         if now > self.lastStatLog + .2:
-            dispatcher.send('state', update={
-                'recentDeltas': sorted([round(t1 - t0, 4) for t0, t1 in
-                                 zip(self.recentUpdateTimes[:-1],
-                                     self.recentUpdateTimes[1:])]),
-                'recentFps': stats.recentFps})
+            dispatcher.send(
+                'state',
+                update={
+                    'recentDeltas':
+                    sorted([
+                        round(t1 - t0, 4)
+                        for t0, t1 in zip(self.recentUpdateTimes[:-1],
+                                          self.recentUpdateTimes[1:])
+                    ]),
+                    'recentFps':
+                    stats.recentFps
+                })
             self.lastStatLog = now
 
         def done(sec):
@@ -180,19 +191,21 @@
             # print 'cl', delay
             delay = 0.005
             reactor.callLater(delay, self.updateLoop)
+
         def err(e):
             log.warn('updateLoop: %r', e)
             reactor.callLater(2, self.updateLoop)
-            
+
         d = self.update()
         d.addCallbacks(done, err)
-        
+
     @stats.update.time()
     def update(self):
         # print "update"
         try:
             musicState = self.music.getLatest()
-            song = URIRef(musicState['song']) if musicState.get('song') else None
+            song = URIRef(
+                musicState['song']) if musicState.get('song') else None
             if 't' not in musicState:
                 return defer.succeed(0)
             t = musicState['t']
@@ -206,25 +219,27 @@
                 noteReports.append(report)
                 settings.append(s)
             dispatcher.send('state', update={'songNotes': noteReports})
-            return self.sendToCollector(DeviceSettings.fromList(self.graph, settings))
+            return self.sendToCollector(
+                DeviceSettings.fromList(self.graph, settings))
         except Exception:
             traceback.print_exc()
             raise
 
+
 class Updates(cyclone.sse.SSEHandler):
+
     def __init__(self, application, request, **kwargs):
-        cyclone.sse.SSEHandler.__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)
-        
+
     def bind(self):
         self.numConnected += 1
-        
+
         if self.numConnected == 1:
             self.loop()
 
@@ -233,8 +248,6 @@
             return
         self.sendEvent(self.state)
         reactor.callLater(.1, self.loop)
-        
+
     def unbind(self):
         self.numConnected -= 1
-
-    
--- a/light9/effect/settings.py	Tue May 21 23:55:35 2019 +0000
+++ b/light9/effect/settings.py	Tue May 21 23:56:12 2019 +0000
@@ -13,15 +13,20 @@
 log = logging.getLogger('settings')
 from light9.collector.device import resolve
 
+
 def parseHex(h):
     if h[0] != '#': raise ValueError(h)
-    return [int(h[i:i+2], 16) for i in 1, 3, 5]
+    return [int(h[i:i + 2], 16) for i in 1, 3, 5]
+
 
 def parseHexNorm(h):
     return [x / 255 for x in parseHex(h)]
-    
+
+
 def toHex(rgbFloat):
-    return '#%02x%02x%02x' % tuple(max(0, min(255, int(v * 255))) for v in rgbFloat)
+    return '#%02x%02x%02x' % tuple(
+        max(0, min(255, int(v * 255))) for v in rgbFloat)
+
 
 def getVal(graph, subj):
     lit = graph.value(subj, L9['value']) or graph.value(subj, L9['scaledValue'])
@@ -30,26 +35,28 @@
         ret = float(ret)
     return ret
 
+
 class _Settings(object):
     """
     default values are 0 or '#000000'. Internal rep must not store zeros or some
     comparisons will break.
     """
+
     def __init__(self, graph, settingsList):
-        self.graph = graph # for looking up all possible attrs
-        self._compiled = {} # dev: { attr: val }; val is number or colorhex
+        self.graph = graph  # for looking up all possible attrs
+        self._compiled = {}  # dev: { attr: val }; val is number or colorhex
         for row in settingsList:
             self._compiled.setdefault(row[0], {})[row[1]] = row[2]
         # self._compiled may not be final yet- see _fromCompiled
         self._delZeros()
-        
+
     @classmethod
     def _fromCompiled(cls, graph, compiled):
         obj = cls(graph, [])
         obj._compiled = compiled
         obj._delZeros()
         return obj
-            
+
     @classmethod
     def fromResource(cls, graph, subj):
         settingsList = []
@@ -67,7 +74,7 @@
         i = 0
         for (d, a) in cls(graph, [])._vectorKeys(deviceAttrFilter):
             if a == L9['color']:
-                v = toHex(vector[i:i+3])
+                v = toHex(vector[i:i + 3])
                 i += 3
             else:
                 v = vector[i]
@@ -82,7 +89,7 @@
         for s in others:
             if not isinstance(s, cls):
                 raise TypeError(s)
-            for row in s.asList(): # could work straight from s._compiled
+            for row in s.asList():  # could work straight from s._compiled
                 if row[0] is None:
                     raise TypeError('bad row %r' % (row,))
                 dev, devAttr, value = row
@@ -100,20 +107,21 @@
         for weight, s in others:
             if not isinstance(s, cls):
                 raise TypeError(s)
-            for row in s.asList(): # could work straight from s._compiled
+            for row in s.asList():  # could work straight from s._compiled
                 if row[0] is None:
                     raise TypeError('bad row %r' % (row,))
                 dd = out._compiled.setdefault(row[0], {})
 
                 if isinstance(row[2], basestring):
                     prev = parseHexNorm(dd.get(row[1], '#000000'))
-                    newVal = toHex(prev + weight * numpy.array(parseHexNorm(row[2])))
+                    newVal = toHex(prev +
+                                   weight * numpy.array(parseHexNorm(row[2])))
                 else:
                     newVal = dd.get(row[1], 0) + weight * row[2]
                 dd[row[1]] = newVal
         out._delZeros()
         return out
-        
+
     def _zeroForAttr(self, attr):
         if attr == L9['color']:
             return '#000000'
@@ -126,15 +134,17 @@
                     del av[attr]
             if not av:
                 del self._compiled[dev]
-        
+
     def __hash__(self):
-        itemed = tuple([(d, tuple([(a, v) for a, v in sorted(av.items())]))
+        itemed = tuple([(d, tuple([(a, v)
+                                   for a, v in sorted(av.items())]))
                         for d, av in sorted(self._compiled.items())])
         return hash(itemed)
 
     def __eq__(self, other):
         if not issubclass(other.__class__, self.__class__):
-            raise TypeError("can't compare %r to %r" % (self.__class__, other.__class__))
+            raise TypeError("can't compare %r to %r" %
+                            (self.__class__, other.__class__))
         return self._compiled == other._compiled
 
     def __ne__(self, other):
@@ -142,18 +152,20 @@
 
     def __nonzero__(self):
         return bool(self._compiled)
-        
+
     def __repr__(self):
         words = []
+
         def accum():
             for dev, av in self._compiled.iteritems():
                 for attr, val in sorted(av.iteritems()):
-                    words.append('%s.%s=%s' % (dev.rsplit('/')[-1],
-                                               attr.rsplit('/')[-1],
-                                               val))
+                    words.append(
+                        '%s.%s=%s' %
+                        (dev.rsplit('/')[-1], attr.rsplit('/')[-1], val))
                     if len(words) > 5:
                         words.append('...')
                         return
+
         accum()
         return '<%s %s>' % (self.__class__.__name__, ' '.join(words))
 
@@ -174,7 +186,7 @@
 
     def devices(self):
         return self._compiled.keys()
-        
+
     def toVector(self, deviceAttrFilter=None):
         out = []
         for dev, attr in self._vectorKeys(deviceAttrFilter):
@@ -192,7 +204,7 @@
     def ofDevice(self, dev):
         return self.__class__._fromCompiled(self.graph,
                                             {dev: self._compiled.get(dev, {})})
-        
+
     def distanceTo(self, other):
         diff = numpy.array(self.toVector()) - other.toVector()
         d = numpy.linalg.norm(diff, ord=None)
@@ -210,27 +222,29 @@
             settingHash = hash((dev, attr, val)) % 9999999
             setting = URIRef('%sset%s' % (settingRoot, settingHash))
             add.append((subj, L9['setting'], setting, ctx))
-            if setting in settingsSubgraphCache:              
+            if setting in settingsSubgraphCache:
                 continue
-                
+
             scaledAttributeTypes = [L9['color'], L9['brightness'], L9['uv']]
-            settingType = L9['scaledValue'] if attr in scaledAttributeTypes else L9['value']
+            settingType = L9[
+                'scaledValue'] if attr in scaledAttributeTypes else L9['value']
             if not isinstance(val, URIRef):
                 val = Literal(val)
             add.extend([
                 (setting, L9['device'], dev, ctx),
                 (setting, L9['deviceAttr'], attr, ctx),
                 (setting, settingType, val, ctx),
-                ])
+            ])
             settingsSubgraphCache.add(setting)
-            
+
         return add
 
 
 class DeviceSettings(_Settings):
+
     def _vectorKeys(self, deviceAttrFilter=None):
         with self.graph.currentState() as g:
-            devs = set() # devclass, dev
+            devs = set()  # devclass, dev
             for dc in g.subjects(RDF.type, L9['DeviceClass']):
                 for dev in g.subjects(RDF.type, dc):
                     devs.add((dc, dev))
@@ -243,4 +257,3 @@
                         continue
                     keys.append(key)
         return keys
-    
--- a/light9/effect/settings_test.py	Tue May 21 23:55:35 2019 +0000
+++ b/light9/effect/settings_test.py	Tue May 21 23:56:12 2019 +0000
@@ -4,12 +4,13 @@
 from rdfdb.localsyncedgraph import LocalSyncedGraph
 from light9.namespaces import RDF, L9, DEV
 from light9.effect.settings import DeviceSettings
-             
-        
+
+
 class TestDeviceSettings(unittest.TestCase):
+
     def setUp(self):
-        self.graph = LocalSyncedGraph(files=['test/cam/lightConfig.n3',
-                                             'test/cam/bg.n3'])
+        self.graph = LocalSyncedGraph(
+            files=['test/cam/lightConfig.n3', 'test/cam/bg.n3'])
 
     def testToVectorZero(self):
         ds = DeviceSettings(self.graph, [])
@@ -29,50 +30,57 @@
 
     def testMissingFieldsEqZero(self):
         self.assertEqual(
-            DeviceSettings(self.graph, [(L9['aura1'], L9['rx'], 0),]),
-            DeviceSettings(self.graph, []))
+            DeviceSettings(self.graph, [
+                (L9['aura1'], L9['rx'], 0),
+            ]), DeviceSettings(self.graph, []))
 
     def testFalseIfZero(self):
-        self.assertTrue(DeviceSettings(self.graph, [(L9['aura1'], L9['rx'], 0.1)]))
+        self.assertTrue(
+            DeviceSettings(self.graph, [(L9['aura1'], L9['rx'], 0.1)]))
         self.assertFalse(DeviceSettings(self.graph, []))
-        
+
     def testFromResource(self):
         ctx = L9['']
-        self.graph.patch(Patch(addQuads=[
-            (L9['foo'], L9['setting'], L9['foo_set0'], ctx),
-            (L9['foo_set0'], L9['device'], L9['light1'], ctx),
-            (L9['foo_set0'], L9['deviceAttr'], L9['brightness'], ctx),
-            (L9['foo_set0'], L9['value'], Literal(0.1), ctx),
-            (L9['foo'], L9['setting'], L9['foo_set1'], ctx),
-            (L9['foo_set1'], L9['device'], L9['light1'], ctx),
-            (L9['foo_set1'], L9['deviceAttr'], L9['speed'], ctx),
-            (L9['foo_set1'], L9['scaledValue'], Literal(0.2), ctx),
-        ]))
+        self.graph.patch(
+            Patch(addQuads=[
+                (L9['foo'], L9['setting'], L9['foo_set0'], ctx),
+                (L9['foo_set0'], L9['device'], L9['light1'], ctx),
+                (L9['foo_set0'], L9['deviceAttr'], L9['brightness'], ctx),
+                (L9['foo_set0'], L9['value'], Literal(0.1), ctx),
+                (L9['foo'], L9['setting'], L9['foo_set1'], ctx),
+                (L9['foo_set1'], L9['device'], L9['light1'], ctx),
+                (L9['foo_set1'], L9['deviceAttr'], L9['speed'], ctx),
+                (L9['foo_set1'], L9['scaledValue'], Literal(0.2), ctx),
+            ]))
         s = DeviceSettings.fromResource(self.graph, L9['foo'])
 
-        self.assertEqual(DeviceSettings(self.graph, [
-            (L9['light1'], L9['brightness'], 0.1),
-            (L9['light1'], L9['speed'], 0.2),
-        ]), s)
+        self.assertEqual(
+            DeviceSettings(self.graph, [
+                (L9['light1'], L9['brightness'], 0.1),
+                (L9['light1'], L9['speed'], 0.2),
+            ]), s)
 
     def testToVector(self):
         v = DeviceSettings(self.graph, [
             (DEV['aura1'], L9['rx'], 0.5),
             (DEV['aura1'], L9['color'], '#00ff00'),
         ]).toVector()
-        self.assertEqual(
-            [0, 1, 0, 0.5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
-            v)
-        
+        self.assertEqual([
+            0, 1, 0, 0.5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+            0, 0, 0, 0, 0, 0, 0, 0
+        ], v)
+
     def testFromVector(self):
-        s = DeviceSettings.fromVector(
-            self.graph,
-            [0, 1, 0, 0.5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
-        
-        self.assertEqual(DeviceSettings(self.graph, [
-            (DEV['aura1'], L9['rx'], 0.5),
-            (DEV['aura1'], L9['color'], '#00ff00'),
-        ]), s)                            
+        s = DeviceSettings.fromVector(self.graph, [
+            0, 1, 0, 0.5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+            0, 0, 0, 0, 0, 0, 0, 0
+        ])
+
+        self.assertEqual(
+            DeviceSettings(self.graph, [
+                (DEV['aura1'], L9['rx'], 0.5),
+                (DEV['aura1'], L9['color'], '#00ff00'),
+            ]), s)
 
     def testAsList(self):
         sets = [
@@ -85,23 +93,23 @@
         s = DeviceSettings(self.graph, [
             (DEV['aura1'], L9['rx'], 0),
             (DEV['aura2'], L9['rx'], 0.1),
-            ])
+        ])
         # aura1 is all defaults (zeros), so it doesn't get listed
         self.assertItemsEqual([DEV['aura2']], s.devices())
 
     def testAddStatements(self):
         s = DeviceSettings(self.graph, [
             (DEV['aura2'], L9['rx'], 0.1),
-            ])
+        ])
         stmts = s.statements(L9['foo'], L9['ctx1'], L9['s_'], set())
-        self.maxDiff=None
+        self.maxDiff = None
         self.assertItemsEqual([
             (L9['foo'], L9['setting'], L9['s_set4350023'], L9['ctx1']),
             (L9['s_set4350023'], L9['device'], DEV['aura2'], L9['ctx1']),
             (L9['s_set4350023'], L9['deviceAttr'], L9['rx'], L9['ctx1']),
             (L9['s_set4350023'], L9['value'], Literal(0.1), L9['ctx1']),
         ], stmts)
-        
+
     def testDistanceTo(self):
         s1 = DeviceSettings(self.graph, [
             (DEV['aura1'], L9['rx'], 0.1),
@@ -112,25 +120,31 @@
             (DEV['aura1'], L9['ry'], 0.3),
         ])
         self.assertEqual(0.36055512754639896, s1.distanceTo(s2))
-        
+
 
 L1 = L9['light1']
 ZOOM = L9['zoom']
+
+
 class TestFromBlend(unittest.TestCase):
+
     def setUp(self):
-        self.graph = LocalSyncedGraph(files=['test/cam/lightConfig.n3',
-                                             'test/cam/bg.n3'])
+        self.graph = LocalSyncedGraph(
+            files=['test/cam/lightConfig.n3', 'test/cam/bg.n3'])
+
     def testSingle(self):
         self.assertEqual(
             DeviceSettings(self.graph, [(L1, ZOOM, 0.5)]),
-            DeviceSettings.fromBlend(self.graph, [
-                (1, DeviceSettings(self.graph, [(L1, ZOOM, 0.5)]))]))
+            DeviceSettings.fromBlend(
+                self.graph,
+                [(1, DeviceSettings(self.graph, [(L1, ZOOM, 0.5)]))]))
 
     def testScale(self):
         self.assertEqual(
             DeviceSettings(self.graph, [(L1, ZOOM, 0.1)]),
-            DeviceSettings.fromBlend(self.graph, [
-                (.2, DeviceSettings(self.graph, [(L1, ZOOM, 0.5)]))]))
+            DeviceSettings.fromBlend(
+                self.graph,
+                [(.2, DeviceSettings(self.graph, [(L1, ZOOM, 0.5)]))]))
 
     def testMixFloats(self):
         self.assertEqual(
--- a/light9/effect/simple_outputs.py	Tue May 21 23:55:35 2019 +0000
+++ b/light9/effect/simple_outputs.py	Tue May 21 23:56:12 2019 +0000
@@ -3,15 +3,17 @@
 from light9.namespaces import L9, RDF
 from light9.effect.scale import scale
 
+
 class SimpleOutputs(object):
+
     def __init__(self, graph):
         self.graph = graph
-        
+
         # effect : [(dev, attr, value, isScaled)]
         self.effectOutputs = {}
 
         self.graph.addHandler(self.updateEffectsFromGraph)
-        
+
     def updateEffectsFromGraph(self):
         for effect in self.graph.subjects(RDF.type, L9['Effect']):
             settings = []
@@ -37,7 +39,6 @@
             if settings:
                 self.effectOutputs[effect] = settings
             # also have to read eff :effectAttr [ :tint x; :tintStrength y ]
-        
 
     def values(self, effect, strength, colorScale):
         out = {}
@@ -48,4 +49,3 @@
                 value = scale(value, colorScale)
             out[(dev, devAttr)] = value
         return out
-        
--- a/light9/effecteval/effect.py	Tue May 21 23:55:35 2019 +0000
+++ b/light9/effecteval/effect.py	Tue May 21 23:56:12 2019 +0000
@@ -5,16 +5,19 @@
 from light9.curvecalc.curve import CurveResource
 from light9 import prof
 from light9 import Submaster
-from light9 import Effects # gets reload() later
+from light9 import Effects  # gets reload() later
 log = logging.getLogger('effect')
 
 # consider http://waxeye.org/ for a parser that can be used in py and js
 
+
 class CouldNotConvert(TypeError):
     pass
 
+
 class CodeLine(object):
     """code string is immutable"""
+
     def __init__(self, graph, code):
         self.graph, self.code = graph, code
 
@@ -36,21 +39,23 @@
         resources = {}
 
         def alreadyInFunc(prefix, s, i):
-            return i >= len(prefix) and s[i-len(prefix):i] == prefix
+            return i >= len(prefix) and s[i - len(prefix):i] == prefix
 
         def repl(m):
             v = '_res%s' % self.uriCounter
             self.uriCounter += 1
             r = resources[v] = URIRef(m.group(1))
             for uriTypeMatches, wrapFuncName, addlArgs in [
-                    (self._uriIsCurve(r), 'curve', ', t'),
+                (self._uriIsCurve(r), 'curve', ', t'),
                     # I'm pretty sure this shouldn't be auto-applied: it's reasonable to refer to a sub and not want its current value
                     #(self._uriIsSub(r), 'currentSubLevel', ''),
             ]:
                 if uriTypeMatches:
-                    if not alreadyInFunc(wrapFuncName + '(', m.string, m.start()):
+                    if not alreadyInFunc(wrapFuncName + '(', m.string,
+                                         m.start()):
                         return '%s(%s%s)' % (wrapFuncName, v, addlArgs)
             return v
+
         outExpr = re.sub(r'<(http\S*?)>', repl, expr)
         return lname, expr, outExpr, resources
 
@@ -60,14 +65,14 @@
         tokens = set(re.findall(r'\b([a-zA-Z_]\w*)\b', withoutUris))
         tokens.discard('None')
         return tokens
-        
+
     def _uriIsCurve(self, uri):
         # this result could vary with graph changes (rare)
         return self.graph.contains((uri, RDF.type, L9['Curve']))
 
     def _uriIsSub(self, uri):
         return self.graph.contains((uri, RDF.type, L9['Submaster']))
-        
+
     @prof.logTime
     def _resourcesAsPython(self, resources):
         """
@@ -77,7 +82,7 @@
         out = {}
         subs = prof.logTime(Submaster.get_global_submasters)(self.graph)
         for localVar, uri in resources.items():
-            
+
             for rdfClass in self.graph.objects(uri, RDF.type):
                 if rdfClass == L9['Curve']:
                     cr = CurveResource(self.graph, uri)
@@ -95,8 +100,10 @@
                 out[localVar] = CouldNotConvert(uri)
 
         return out
-        
+
+
 class EffectNode(object):
+
     def __init__(self, graph, uri):
         self.graph, self.uri = graph, uri
         # this is not expiring at the right time, when an effect goes away
@@ -127,7 +134,9 @@
             inNames = c.possibleVars.intersection(codeFromOutput.keys())
             inNames.discard(outName)
             deps[outName] = inNames
-        self.codes = [codeFromOutput[n] for n in toposort.toposort_flatten(deps)]
+        self.codes = [
+            codeFromOutput[n] for n in toposort.toposort_flatten(deps)
+        ]
 
     def _currentSubSettingValues(self, sub):
         """what KC subSettings are setting levels right now?"""
@@ -149,25 +158,26 @@
             raise TypeError("got %r" % uri)
 
         foundLevels = list(self._currentSubSettingValues(uri))
-        
+
         if not foundLevels:
             return 0
-        
+
         return max(foundLevels)
-        
+
     def eval(self, songTime):
         ns = {'t': songTime}
         ns.update(self.otherFuncs)
 
-        ns.update(dict(
-            curve=lambda c, t: c.eval(t),
-            currentSubLevel=self.currentSubLevel,
+        ns.update(
+            dict(
+                curve=lambda c, t: c.eval(t),
+                currentSubLevel=self.currentSubLevel,
             ))
 
         # I think this is slowing effecteval. Could we cache results
         # that we know haven't changed, like if a curve returns 0
         # again, we can skip an eval() call on the line that uses it
-        
+
         for c in self.codes:
             codeNs = ns.copy()
             codeNs.update(c.pyResources)
@@ -180,4 +190,3 @@
         if 'out' not in ns:
             log.error("ran code for %s, didn't make an 'out' value", self.uri)
         return ns['out']
-
--- a/light9/effecteval/effectloop.py	Tue May 21 23:55:35 2019 +0000
+++ b/light9/effecteval/effectloop.py	Tue May 21 23:56:12 2019 +0000
@@ -16,23 +16,26 @@
 from light9 import prof
 log = logging.getLogger('effectloop')
 
+
 class EffectLoop(object):
     """maintains a collection of the current EffectNodes, gets time from
     music player, sends dmx"""
+
     def __init__(self, graph, stats):
         self.graph, self.stats = graph, stats
         self.currentSong = None
-        self.currentEffects = [] # EffectNodes for the current song plus the submaster ones
+        self.currentEffects = [
+        ]  # EffectNodes for the current song plus the submaster ones
         self.lastLogTime = 0
         self.lastLogMsg = ""
         self.lastErrorLog = 0
         self.graph.addHandler(self.setEffects)
         self.period = 1 / 30
-        self.coastSecs = .3 # main reason to keep this low is to notice play/pause
+        self.coastSecs = .3  # main reason to keep this low is to notice play/pause
         self.songTimeFetch = 0
         self.songIsPlaying = False
         self.songTimeFromRequest = 0
-        self.requestTime = 0 # unix sec for when we fetched songTime
+        self.requestTime = 0  # unix sec for when we fetched songTime
         self.initOutput()
 
     def initOutput(self):
@@ -49,17 +52,16 @@
         log.info('setEffects currentSong=%s', self.currentSong)
         if self.currentSong is None:
             return
-        
+
         for effectUri in self.graph.objects(self.currentSong, L9['effect']):
             self.currentEffects.append(EffectNode(self.graph, effectUri))
 
-
         for sub in self.graph.subjects(RDF.type, L9['Submaster']):
             for effectUri in self.graph.objects(sub, L9['drivesEffect']):
                 self.currentEffects.append(EffectNode(self.graph, effectUri))
-                
+
         log.info('now we have %s effects', len(self.currentEffects))
-        
+
     @inlineCallbacks
     def getSongTime(self):
         now = time.time()
@@ -74,15 +76,14 @@
                 self.requestTime = now
                 self.currentPlaying = response['playing']
                 self.songTimeFromRequest = response['t']
-                returnValue(
-                    (response['t'], (response['song'] and URIRef(response['song']))))
+                returnValue((response['t'], (response['song'] and
+                                             URIRef(response['song']))))
 
         estimated = self.songTimeFromRequest
         if self.currentSong is not None and self.currentPlaying:
             estimated += now - self.requestTime
         returnValue((estimated, self.currentSong))
 
-
     @inlineCallbacks
     def updateTimeFromMusic(self):
         t1 = time.time()
@@ -96,7 +97,8 @@
             self.graph.addHandler(self.setEffects)
 
         elapsed = time.time() - t1
-        reactor.callLater(max(0, self.period - elapsed), self.updateTimeFromMusic)
+        reactor.callLater(max(0, self.period - elapsed),
+                          self.updateTimeFromMusic)
 
     def estimatedSongTime(self):
         now = time.time()
@@ -108,21 +110,23 @@
     @inlineCallbacks
     def sendLevels(self):
         t1 = time.time()
-        log.debug("time since last call: %.1f ms" % (1000 * (t1 - self.lastSendLevelsTime)))
+        log.debug("time since last call: %.1f ms" %
+                  (1000 * (t1 - self.lastSendLevelsTime)))
         self.lastSendLevelsTime = t1
         try:
             with self.stats.sendLevels.time():
                 if self.currentSong is not None:
                     log.debug('allEffectOutputs')
                     with self.stats.evals.time():
-                        outputs = self.allEffectOutputs(self.estimatedSongTime())
+                        outputs = self.allEffectOutputs(
+                            self.estimatedSongTime())
                     log.debug('combineOutputs')
                     combined = self.combineOutputs(outputs)
                     self.logLevels(t1, combined)
                     log.debug('sendOutput')
                     with self.stats.sendOutput.time():
                         yield self.sendOutput(combined)
-                
+
                 elapsed = time.time() - t1
                 dt = max(0, self.period - elapsed)
         except Exception:
@@ -138,12 +142,12 @@
         out = Submaster.sub_maxes(*outputs)
 
         return out
-        
+
     @inlineCallbacks
     def sendOutput(self, combined):
         dmx = combined.get_dmx_list()
         yield dmxclient.outputlevels(dmx, twisted=True)
-        
+
     def allEffectOutputs(self, songTime):
         outputs = []
         for e in self.currentEffects:
@@ -160,10 +164,11 @@
                         log.exception('in expression %r', exc.expr)
                     log.error("effect %s: %s" % (e.uri, exc))
                     self.lastErrorLog = now
-        log.debug('eval %s effects, got %s outputs', len(self.currentEffects), len(outputs))
-                    
+        log.debug('eval %s effects, got %s outputs', len(self.currentEffects),
+                  len(outputs))
+
         return outputs
-        
+
     def logLevels(self, now, out):
         # this would look nice on the top of the effecteval web pages too
         if log.isEnabledFor(logging.DEBUG):
@@ -175,23 +180,28 @@
                     log.info(msg)
                     self.lastLogMsg = msg
                 self.lastLogTime = now
-                
+
     def logMessage(self, out):
-        return ("send dmx: {%s}" %
-                ", ".join("%r: %.3g" % (str(k), v)
-                          for k,v in out.get_levels().items()))
+        return ("send dmx: {%s}" % ", ".join(
+            "%r: %.3g" % (str(k), v) for k, v in out.get_levels().items()))
+
 
 Z = numpy.zeros((50, 3), dtype=numpy.float16)
 
+
 class ControlBoard(object):
-    def __init__(self, dev='/dev/serial/by-id/usb-FTDI_FT232R_USB_UART_A7027NYX-if00-port0'):
+
+    def __init__(
+            self,
+            dev='/dev/serial/by-id/usb-FTDI_FT232R_USB_UART_A7027NYX-if00-port0'
+    ):
         log.info('opening %s', dev)
         self._dev = serial.Serial(dev, baudrate=115200)
 
     def _8bitMessage(self, floatArray):
         px255 = (numpy.clip(floatArray, 0, 1) * 255).astype(numpy.uint8)
         return px255.reshape((-1,)).tostring()
-        
+
     def setStrip(self, which, pixels):
         """
         which: 0 or 1 to pick the strip
@@ -209,7 +219,8 @@
         level: 0 to 1
         """
         command = {0: '\x02', 1: '\x03'}[which]
-        self._dev.write('\x60' + command + chr(int(max(0, min(1, level)) * 255)))
+        self._dev.write('\x60' + command +
+                        chr(int(max(0, min(1, level)) * 255)))
         self._dev.flush()
 
     def setRgb(self, color):
@@ -221,17 +232,22 @@
         self._dev.write('\x60\x04' + self._8bitMessage(color))
         self._dev.flush()
 
-        
+
 class LedLoop(EffectLoop):
+
     def initOutput(self):
         self.board = ControlBoard()
-        self.lastSent = {} # what's in arduino's memory
-        
+        self.lastSent = {}  # what's in arduino's memory
+
     def combineOutputs(self, outputs):
-        combined = {'L': Z, 'R': Z,
-                    'blacklight0': 0, 'blacklight1': 0,
-                    'W': numpy.zeros((1, 3), dtype=numpy.float16)}
-        
+        combined = {
+            'L': Z,
+            'R': Z,
+            'blacklight0': 0,
+            'blacklight1': 0,
+            'W': numpy.zeros((1, 3), dtype=numpy.float16)
+        }
+
         for out in outputs:
             log.debug('combine output %r', out)
 
@@ -241,12 +257,17 @@
             if isinstance(out, Submaster.Submaster) and '*' in out.name:
                 level = float(out.name.split('*')[1])
                 n = out.name.split('*')[0]
-                if n == 'widered': out = Effects.Strip.solid('LRW', (1,0,0)) * level
-                if n == 'widegreen': out = Effects.Strip.solid('LRW', (0,1,0)) * level
-                if n == 'wideblue': out = Effects.Strip.solid('LRW', (0,0,1)) * level
-                if n == 'whiteled': out = Effects.Strip.solid('LRW', (1,.7,.7)) * level
-                if n == 'blacklight': out = Effects.Blacklight(level) # missing blues!
- 
+                if n == 'widered':
+                    out = Effects.Strip.solid('LRW', (1, 0, 0)) * level
+                if n == 'widegreen':
+                    out = Effects.Strip.solid('LRW', (0, 1, 0)) * level
+                if n == 'wideblue':
+                    out = Effects.Strip.solid('LRW', (0, 0, 1)) * level
+                if n == 'whiteled':
+                    out = Effects.Strip.solid('LRW', (1, .7, .7)) * level
+                if n == 'blacklight':
+                    out = Effects.Blacklight(level)  # missing blues!
+
             if isinstance(out, Effects.Blacklight):
                 # no picking yet
                 #key = 'blacklight%s' % out.which
@@ -256,34 +277,37 @@
                 pixels = numpy.array(out.pixels, dtype=numpy.float16)
                 for w in out.which:
                     combined[w] = numpy.maximum(
-                        combined[w], pixels[:1,:] if w == 'W' else pixels)
-                
+                        combined[w], pixels[:1, :] if w == 'W' else pixels)
+
         return combined
 
     @inlineCallbacks
     def sendOutput(self, combined):
         for meth, selectArgs, value in [
-                ('setStrip', (0,), combined['L']),
-                ('setStrip', (1,), combined['R']),
-                ('setUv', (0,), combined['blacklight0']),
-                ('setUv', (1,), combined['blacklight1']),
-                ('setRgb', (), combined['W']),
-            ]:
+            ('setStrip', (0,), combined['L']),
+            ('setStrip', (1,), combined['R']),
+            ('setUv', (0,), combined['blacklight0']),
+            ('setUv', (1,), combined['blacklight1']),
+            ('setRgb', (), combined['W']),
+        ]:
             key = (meth, selectArgs)
-            compValue = value.tolist() if isinstance(value, numpy.ndarray) else value
-            
+            compValue = value.tolist() if isinstance(value,
+                                                     numpy.ndarray) else value
+
             if self.lastSent.get(key) == compValue:
                 continue
 
             log.debug('value changed: %s(%s %s)', meth, selectArgs, value)
-            
+
             getattr(self.board, meth)(*(selectArgs + (value,)))
             self.lastSent[key] = compValue
-                
-        yield succeed(None) # there was an attempt at an async send
-        
+
+        yield succeed(None)  # there was an attempt at an async send
+
     def logMessage(self, out):
-        return str([(w, p.tolist() if isinstance(p, numpy.ndarray) else p) for w,p in out.items()])
+        return str([(w, p.tolist() if isinstance(p, numpy.ndarray) else p)
+                    for w, p in out.items()])
+
 
 def makeEffectLoop(graph, stats, outputWhere):
     if outputWhere == 'dmx':
@@ -292,5 +316,3 @@
         return LedLoop(graph, stats)
     else:
         raise NotImplementedError("unknown output system %r" % outputWhere)
-
-        
--- a/light9/effecteval/test_effect.py	Tue May 21 23:55:35 2019 +0000
+++ b/light9/effecteval/test_effect.py	Tue May 21 23:56:12 2019 +0000
@@ -1,21 +1,26 @@
 import unittest
 import mock
 import sys
-sys.path.insert(0, 'bin') # for run_local
+sys.path.insert(0, 'bin')  # for run_local
 
 from effect import CodeLine
 from rdflib import URIRef
 
+
 def isCurve(self, uri):
     return 'curve' in uri
+
+
 def isSub(self, uri):
     return 'sub' in uri
 
+
 @mock.patch('light9.effecteval.effect.CodeLine._uriIsCurve', new=isCurve)
 @mock.patch('light9.effecteval.effect.CodeLine._uriIsSub', new=isSub)
 @mock.patch('light9.effecteval.effect.CodeLine._resourcesAsPython',
             new=lambda self, r: self.expr)
 class TestAsPython(unittest.TestCase):
+
     def test_gets_lname(self):
         ec = CodeLine(graph=None, code='x = y+1')
         self.assertEqual('x', ec.outName)
@@ -24,7 +29,7 @@
         ec = CodeLine(graph=None, code='x = y+1')
         self.assertEqual('y+1', ec._asPython()[2])
         self.assertEqual({}, ec._asPython()[3])
-        
+
     def test_converts_uri_to_var(self):
         ec = CodeLine(graph=None, code='x = <http://example.com/>')
         _, inExpr, expr, uris = ec._asPython()
@@ -32,18 +37,22 @@
         self.assertEqual({'_res0': URIRef('http://example.com/')}, uris)
 
     def test_converts_multiple_uris(self):
-        ec = CodeLine(graph=None, code='x = <http://example.com/> + <http://other>')
+        ec = CodeLine(graph=None,
+                      code='x = <http://example.com/> + <http://other>')
         _, inExpr, expr, uris = ec._asPython()
         self.assertEqual('_res0 + _res1', expr)
-        self.assertEqual({'_res0': URIRef('http://example.com/'),
-                          '_res1': URIRef('http://other')}, uris)
-        
+        self.assertEqual(
+            {
+                '_res0': URIRef('http://example.com/'),
+                '_res1': URIRef('http://other')
+            }, uris)
+
     def test_doesnt_fall_for_brackets(self):
         ec = CodeLine(graph=None, code='x = 1<2>3< h')
         _, inExpr, expr, uris = ec._asPython()
         self.assertEqual('1<2>3< h', expr)
         self.assertEqual({}, uris)
-        
+
     def test_curve_uri_expands_to_curve_eval_func(self):
         ec = CodeLine(graph=None, code='x = <http://example/curve1>')
         _, inExpr, expr, uris = ec._asPython()
@@ -51,19 +60,24 @@
         self.assertEqual({'_res0': URIRef('http://example/curve1')}, uris)
 
     def test_curve_doesnt_double_wrap(self):
-        ec = CodeLine(graph=None, code='x = curve(<http://example/curve1>, t+.01)')
+        ec = CodeLine(graph=None,
+                      code='x = curve(<http://example/curve1>, t+.01)')
         _, inExpr, expr, uris = ec._asPython()
         self.assertEqual('curve(_res0, t+.01)', expr)
         self.assertEqual({'_res0': URIRef('http://example/curve1')}, uris)
-        
-        
+
+
 @mock.patch('light9.effecteval.effect.CodeLine._uriIsCurve', new=isCurve)
 @mock.patch('light9.effecteval.effect.CodeLine._resourcesAsPython',
             new=lambda self, r: self.expr)
 class TestPossibleVars(unittest.TestCase):
+
     def test1(self):
         self.assertEqual(set([]), CodeLine(None, 'a1 = 1').possibleVars)
+
     def test2(self):
         self.assertEqual(set(['a2']), CodeLine(None, 'a1 = a2').possibleVars)
+
     def test3(self):
-        self.assertEqual(set(['a2', 'a3']), CodeLine(None, 'a1 = a2 + a3').possibleVars)
+        self.assertEqual(set(['a2', 'a3']),
+                         CodeLine(None, 'a1 = a2 + a3').possibleVars)
--- a/light9/greplin_cyclone.py	Tue May 21 23:55:35 2019 +0000
+++ b/light9/greplin_cyclone.py	Tue May 21 23:56:12 2019 +0000
@@ -3,26 +3,34 @@
 import greplin.scales.twistedweb, greplin.scales.formats
 from greplin import scales
 
+
 # Like scales.twistedweb.StatsResource, but modified for cyclone. May
 # be missing features.
 class StatsForCyclone(cyclone.web.RequestHandler):
+
     def get(self):
         parts = []
-        statDict = greplin.scales.twistedweb.util.lookup(scales.getStats(), parts)
+        statDict = greplin.scales.twistedweb.util.lookup(
+            scales.getStats(), parts)
 
         if statDict is None:
-          self.set_status(404)
-          self.write("Path not found.")
-          return
+            self.set_status(404)
+            self.write("Path not found.")
+            return
 
         query = self.get_argument('query', default=None)
 
         if self.get_argument('format', default=None) == 'json':
-          self.set_header('content-type', 'text/javascript; charset=UTF-8')
-          greplin.scales.formats.jsonFormat(self, statDict, query)
+            self.set_header('content-type', 'text/javascript; charset=UTF-8')
+            greplin.scales.formats.jsonFormat(self, statDict, query)
         elif self.get_argument('format', default=None) == 'prettyjson':
-          self.set_header('content-type', 'text/javascript; charset=UTF-8')
-          greplin.scales.formats.jsonFormat(self, statDict, query, pretty=True)
+            self.set_header('content-type', 'text/javascript; charset=UTF-8')
+            greplin.scales.formats.jsonFormat(self,
+                                              statDict,
+                                              query,
+                                              pretty=True)
         else:
-          greplin.scales.formats.htmlHeader(self, '/' + '/'.join(parts), 'svr', query)
-          greplin.scales.formats.htmlFormat(self, tuple(parts), statDict, query)
+            greplin.scales.formats.htmlHeader(self, '/' + '/'.join(parts),
+                                              'svr', query)
+            greplin.scales.formats.htmlFormat(self, tuple(parts), statDict,
+                                              query)
--- a/light9/gtkpyconsole.py	Tue May 21 23:55:35 2019 +0000
+++ b/light9/gtkpyconsole.py	Tue May 21 23:56:12 2019 +0000
@@ -3,6 +3,7 @@
 from gi.repository import Gtk
 from gi.repository import Pango
 
+
 def togglePyConsole(self, item, user_ns):
     """
     toggles a toplevel window with an ipython console inside.
@@ -26,11 +27,12 @@
             self.pythonWindow.show_all()
             self.pythonWindow.set_size_request(750, 550)
             self.pythonWindow.set_resizable(True)
+
             def onDestroy(*args):
                 item.set_active(False)
                 del self.pythonWindow
+
             self.pythonWindow.connect("destroy", onDestroy)
     else:
         if hasattr(self, 'pythonWindow'):
             self.pythonWindow.destroy()
-
--- a/light9/io/__init__.py	Tue May 21 23:55:35 2019 +0000
+++ b/light9/io/__init__.py	Tue May 21 23:56:12 2019 +0000
@@ -1,25 +1,27 @@
 from __future__ import division
 import sys
 
+
 class BaseIO(object):
+
     def __init__(self):
-        self.dummy=1
+        self.dummy = 1
         self.__name__ = 'BaseIO'
         # please override and set __name__ to your class name
 
     def golive(self):
         """call this if you want to promote the dummy object becomes a live object"""
         print "IO: %s is going live" % self.__name__
-        self.dummy=0
+        self.dummy = 0
         # you'd override with additional startup stuff here,
         # perhaps even loading a module and saving it to a class
         # attr so the subclass-specific functions can use it
 
     def godummy(self):
         print "IO: %s is going dummy" % self.__name__
-        self.dummy=1
+        self.dummy = 1
         # you might override this to close ports, etc
-        
+
     def isdummy(self):
         return self.dummy
 
@@ -32,10 +34,12 @@
     # the derived class will have more methods to do whatever it does,
     # and they should return dummy values if self.dummy==1.
 
+
 class ParportDMX(BaseIO):
+
     def __init__(self, dimmers=68):
         BaseIO.__init__(self)
-        self.__name__='ParportDMX'
+        self.__name__ = 'ParportDMX'
         self.dimmers = dimmers
 
     def golive(self):
@@ -43,18 +47,20 @@
         import parport
         self.parport = parport
         self.parport.getparport()
-        
+
     def sendlevels(self, levels):
         if self.dummy:
             return
-        
+
         levels = list(levels) + [0]
         # if levels[14] > 0: levels[14] = 100 # non-dim
         self.parport.outstart()
         for p in range(1, self.dimmers + 2):
-            self.parport.outbyte(levels[p-1]*255 / 100)
+            self.parport.outbyte(levels[p - 1] * 255 / 100)
+
 
 class UsbDMX(BaseIO):
+
     def __init__(self, dimmers=72, port='/dev/dmx0'):
         BaseIO.__init__(self)
         self.__name__ = "UsbDMX"
@@ -79,7 +85,6 @@
             return
         # I was outputting on 76 and it was turning on the light at
         # dmx75. So I added the 0 byte.
-        packet = '\x00' + ''.join([chr(int(lev * 255 / 100)) 
-                                  for lev in levels]) + "\x55"
+        packet = '\x00' + ''.join([chr(int(lev * 255 / 100)) for lev in levels
+                                  ]) + "\x55"
         self._dmx().write(packet)
-        
--- a/light9/io/udmx.py	Tue May 21 23:55:35 2019 +0000
+++ b/light9/io/udmx.py	Tue May 21 23:56:12 2019 +0000
@@ -4,7 +4,6 @@
 from usb.util import CTRL_TYPE_VENDOR, CTRL_RECIPIENT_DEVICE, CTRL_OUT
 
 log = logging.getLogger('udmx')
-
 """
 Send dmx to one of these:
 http://www.amazon.com/Interface-Adapter-Controller-Lighting-Freestyler/dp/B00W52VIOS
@@ -23,24 +22,29 @@
 
 cmd_SetChannelRange = 0x0002
 
+
 class Udmx(object):
+
     def __init__(self, bus):
         self.dev = None
-        for dev in usb.core.find(idVendor=0x16c0, idProduct=0x05dc, find_all=True):
+        for dev in usb.core.find(idVendor=0x16c0,
+                                 idProduct=0x05dc,
+                                 find_all=True):
             print "udmx device at %r" % dev.bus
             if bus is None or bus == dev.bus:
                 self.dev = dev
         if not self.dev:
-            raise IOError('no matching udmx device found for requested bus %r' % bus)
+            raise IOError('no matching udmx device found for requested bus %r' %
+                          bus)
         log.info('found udmx at %r', self.dev)
-        
+
     def SendDMX(self, buf):
-        ret = self.dev.ctrl_transfer(
-           bmRequestType=CTRL_TYPE_VENDOR | CTRL_RECIPIENT_DEVICE | CTRL_OUT,
-           bRequest=cmd_SetChannelRange,
-           wValue=len(buf),
-           wIndex=0,
-           data_or_wLength=buf)
+        ret = self.dev.ctrl_transfer(bmRequestType=CTRL_TYPE_VENDOR |
+                                     CTRL_RECIPIENT_DEVICE | CTRL_OUT,
+                                     bRequest=cmd_SetChannelRange,
+                                     wValue=len(buf),
+                                     wIndex=0,
+                                     data_or_wLength=buf)
         if ret < 0:
             raise ValueError("ctrl_transfer returned %r" % ret)
 
@@ -52,9 +56,8 @@
         nsin = math.sin(time.time() * 6.28) / 2.0 + 0.5
         nsin8 = int(255 * nsin)
         try:
-            u.SendDMX('\x00' * (chan - 1) +
-                      chr(210) +
-                      chr(nsin8) + chr(nsin8) + chr(nsin8))
+            u.SendDMX('\x00' * (chan - 1) + chr(210) + chr(nsin8) + chr(nsin8) +
+                      chr(nsin8))
         except usb.core.USBError as e:
             print "err", time.time(), repr(e)
         time.sleep(1 / fps)
--- a/light9/namespaces.py	Tue May 21 23:55:35 2019 +0000
+++ b/light9/namespaces.py	Tue May 21 23:56:12 2019 +0000
@@ -3,15 +3,19 @@
 
 # Namespace was showing up in profiles
 class FastNs(object):
+
     def __init__(self, base):
         self.ns = Namespace(base)
         self.cache = {}
+
     def __getitem__(self, term):
         if term not in self.cache:
             self.cache[term] = self.ns[term]
         return self.cache[term]
+
     __getattr__ = __getitem__
 
+
 L9 = FastNs("http://light9.bigasterisk.com/")
 MUS = Namespace("http://light9.bigasterisk.com/music/")
 XSD = Namespace("http://www.w3.org/2001/XMLSchema#")
--- a/light9/networking.py	Tue May 21 23:55:35 2019 +0000
+++ b/light9/networking.py	Tue May 21 23:56:12 2019 +0000
@@ -3,7 +3,9 @@
 from showconfig import getGraph, showUri
 from namespaces import L9
 
+
 class ServiceAddress(object):
+
     def __init__(self, service):
         self.service = service
 
@@ -12,8 +14,8 @@
         net = graph.value(showUri(), L9['networking'])
         ret = graph.value(net, self.service)
         if ret is None:
-            raise ValueError("no url for %s -> %s -> %s" % (showUri(), L9['networking'],
-                                                            self.service))
+            raise ValueError("no url for %s -> %s -> %s" %
+                             (showUri(), L9['networking'], self.service))
         return str(ret)
 
     @property
@@ -31,11 +33,13 @@
     @property
     def url(self):
         return self._url()
+
     value = url
-    
+
     def path(self, more):
         return self.url + str(more)
 
+
 captureDevice = ServiceAddress(L9['captureDevice'])
 curveCalc = ServiceAddress(L9['curveCalc'])
 dmxServer = ServiceAddress(L9['dmxServer'])
--- a/light9/observable.py	Tue May 21 23:55:35 2019 +0000
+++ b/light9/observable.py	Tue May 21 23:56:12 2019 +0000
@@ -1,9 +1,11 @@
 import logging
 log = logging.getLogger('observable')
 
+
 class _NoNewVal(object):
     pass
 
+
 class Observable(object):
     """
     like knockout's observable. Hopefully this can be replaced by a
@@ -13,6 +15,7 @@
     http://knockoutjs.com/documentation/observables.html
     https://github.com/drpancake/python-observable/blob/master/observable/observable.py
     """
+
     def __init__(self, val):
         self.val = val
         self.subscribers = set()
--- a/light9/paint/capture.py	Tue May 21 23:55:35 2019 +0000
+++ b/light9/paint/capture.py	Tue May 21 23:56:12 2019 +0000
@@ -5,37 +5,46 @@
 from light9.namespaces import L9, RDF
 from light9.paint.solve import loadNumpy
 
+
 def writeCaptureDescription(graph, ctx, session, uri, dev, outPath,
                             settingsSubgraphCache, settings):
-    graph.patch(Patch(addQuads=settings.statements(
-        uri, ctx=ctx,
-        settingRoot=URIRef('/'.join([
-            showconfig.showUri(), 'capture', dev.rsplit('/')[1]])),
-        settingsSubgraphCache=settingsSubgraphCache)))
-    graph.patch(Patch(addQuads=[
-        (dev, L9['capture'], uri, ctx),
-        (session, L9['capture'], uri, ctx),
-        (uri, RDF.type, L9['LightSample'], ctx),
-        (uri, L9['imagePath'], URIRef('/'.join([
-            showconfig.showUri(), outPath])), ctx),
+    graph.patch(
+        Patch(addQuads=settings.statements(
+            uri,
+            ctx=ctx,
+            settingRoot=URIRef('/'.join(
+                [showconfig.showUri(), 'capture',
+                 dev.rsplit('/')[1]])),
+            settingsSubgraphCache=settingsSubgraphCache)))
+    graph.patch(
+        Patch(addQuads=[
+            (dev, L9['capture'], uri, ctx),
+            (session, L9['capture'], uri, ctx),
+            (uri, RDF.type, L9['LightSample'], ctx),
+            (uri, L9['imagePath'],
+             URIRef('/'.join([showconfig.showUri(), outPath])), ctx),
         ]))
-    graph.suggestPrefixes(ctx, {'cap': uri.rsplit('/', 1)[0] + '/',
-                                'showcap': showconfig.showUri() + '/capture/'})
-    
+    graph.suggestPrefixes(
+        ctx, {
+            'cap': uri.rsplit('/', 1)[0] + '/',
+            'showcap': showconfig.showUri() + '/capture/'
+        })
+
+
 class CaptureLoader(object):
+
     def __init__(self, graph):
         self.graph = graph
-        
+
     def loadImage(self, pic, thumb=(100, 100)):
         ip = self.graph.value(pic, L9['imagePath'])
         if not ip.startswith(showconfig.show()):
             raise ValueError(repr(ip))
         diskPath = os.path.join(showconfig.root(), ip[len(self.show):])
         return loadNumpy(diskPath, thumb)
-        
+
     def devices(self):
         """devices for which we have any captured data"""
 
     def capturedSettings(self, device):
         """list of (pic, settings) we know for this device"""
-        
--- a/light9/paint/solve.py	Tue May 21 23:55:35 2019 +0000
+++ b/light9/paint/solve.py	Tue May 21 23:56:12 2019 +0000
@@ -12,43 +12,54 @@
 
 # numpy images in this file are (x, y, c) layout.
 
+
 def numpyFromCairo(surface):
     w, h = surface.get_width(), surface.get_height()
     a = numpy.frombuffer(surface.get_data(), numpy.uint8)
     a.shape = h, w, 4
     a = a.transpose((1, 0, 2))
-    return a[:w,:h,:3]
+    return a[:w, :h, :3]
+
 
 def numpyFromPil(img):
     return scipy.misc.fromimage(img, mode='RGB').transpose((1, 0, 2))
 
+
 def loadNumpy(path, thumb=(100, 100)):
     img = Image.open(path)
     img.thumbnail(thumb)
     return numpyFromPil(img)
 
+
 def saveNumpy(path, img):
     # maybe this should only run if log level is debug?
     scipy.misc.imsave(path, img.transpose((1, 0, 2)))
 
+
 def scaledHex(h, scale):
     rgb = parseHex(h)
     rgb8 = (rgb * scale).astype(numpy.uint8)
     return '#%02x%02x%02x' % tuple(rgb8)
-    
+
+
 def colorRatio(col1, col2):
     rgb1 = parseHex(col1)
     rgb2 = parseHex(col2)
+
     def div(x, y):
         if y == 0:
             return 0
         return round(x / y, 3)
+
     return tuple([div(a, b) for a, b in zip(rgb1, rgb2)])
 
+
 def brightest(img):
     return numpy.amax(img, axis=(0, 1))
-    
+
+
 class ImageDist(object):
+
     def __init__(self, img1):
         self.a = img1.reshape((-1,))
         self.d = 255 * 255 * self.a.shape[0]
@@ -57,27 +68,31 @@
         b = img2.reshape((-1,))
         return 1 - numpy.dot(self.a, b) / self.d
 
+
 class ImageDistAbs(object):
+
     def __init__(self, img1):
         self.a = img1
         self.maxDist = img1.shape[0] * img1.shape[1] * img1.shape[2] * 255
 
     def distanceTo(self, img2):
-        return numpy.sum(numpy.absolute(self.a - img2), axis=None) / self.maxDist
+        return numpy.sum(numpy.absolute(self.a - img2),
+                         axis=None) / self.maxDist
 
-        
+
 class Solver(object):
+
     def __init__(self, graph, sessions=None, imgSize=(100, 53)):
         self.graph = graph
-        self.sessions = sessions # URIs of capture sessions to load
+        self.sessions = sessions  # URIs of capture sessions to load
         self.imgSize = imgSize
-        self.samples = {} # uri: Image array (float 0-255)
-        self.fromPath = {} # imagePath: image array
-        self.path = {} # sample: path
+        self.samples = {}  # uri: Image array (float 0-255)
+        self.fromPath = {}  # imagePath: image array
+        self.path = {}  # sample: path
         self.blurredSamples = {}
-        self.sampleSettings = {} # sample: DeviceSettings
-        self.samplesForDevice = {} # dev : [(sample, img)]
-        
+        self.sampleSettings = {}  # sample: DeviceSettings
+        self.samplesForDevice = {}  # dev : [(sample, img)]
+
     def loadSamples(self):
         """learn what lights do from images"""
 
@@ -93,7 +108,7 @@
         pathUri = g.value(samp, L9['imagePath'])
         img = loadNumpy(pathUri.replace(L9[''], '')).astype(float)
         settings = DeviceSettings.fromResource(self.graph, samp)
-        
+
         self.samples[samp] = img
         self.fromPath[pathUri] = img
         self.blurredSamples[samp] = self._blur(img)
@@ -104,30 +119,30 @@
         devs = settings.devices()
         if len(devs) == 1:
             self.samplesForDevice.setdefault(devs[0], []).append((samp, img))
-        
+
     def _blur(self, img):
         return scipy.ndimage.gaussian_filter(img, 10, 0, mode='nearest')
 
     def draw(self, painting):
         return self._draw(painting, self.imgSize[0], self.imgSize[1])
-        
+
     def _draw(self, painting, w, h):
         surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, w, h)
         ctx = cairo.Context(surface)
         ctx.rectangle(0, 0, w, h)
         ctx.fill()
-        
+
         ctx.set_line_cap(cairo.LINE_CAP_ROUND)
-        ctx.set_line_width(w / 15) # ?
+        ctx.set_line_width(w / 15)  # ?
         for stroke in painting['strokes']:
             for pt in stroke['pts']:
                 op = ctx.move_to if pt is stroke['pts'][0] else ctx.line_to
                 op(pt[0] * w, pt[1] * h)
 
-            r,g,b = parseHex(stroke['color'])
+            r, g, b = parseHex(stroke['color'])
             ctx.set_source_rgb(r / 255, g / 255, b / 255)
             ctx.stroke()
-        
+
         #surface.write_to_png('/tmp/surf.png')
         return numpyFromCairo(surface)
 
@@ -150,8 +165,7 @@
         print 'tops2'
         for row in results[:4]:
             print '%.5f' % row[0], row[1][-20:], self.sampleSettings[row[1]]
-        
-       
+
         #saveNumpy('/tmp/best_in.png', img)
         #saveNumpy('/tmp/best_out.png', topImg)
         #saveNumpy('/tmp/mult.png', topImg / 255 * img)
@@ -167,32 +181,31 @@
             for samp, img2 in self.samplesForDevice[dev]:
                 results.append((dist.distanceTo(img2), samp))
             results.sort()
-            
-            s = self.blendResults([(d, self.sampleSettings[samp])
-                                   for d, samp in results[:8]])
+
+            s = self.blendResults([
+                (d, self.sampleSettings[samp]) for d, samp in results[:8]
+            ])
             devSettings.append(s)
         return DeviceSettings.fromList(self.graph, devSettings)
 
     def blendResults(self, results):
         """list of (dist, settings)"""
-        
+
         dists = [d for d, sets in results]
         hi = max(dists)
         lo = min(dists)
         n = len(results)
-        remappedDists = [1 - (d - lo) / (hi - lo) * n / (n + 1)
-                         for d in dists]
+        remappedDists = [1 - (d - lo) / (hi - lo) * n / (n + 1) for d in dists]
         total = sum(remappedDists)
-        
+
         #print 'blend'
         #for o,n in zip(dists, remappedDists):
         #    print o,n, n / total
         blend = DeviceSettings.fromBlend(
             self.graph,
-            [(d / total, sets) for
-             d, (_, sets) in zip(remappedDists, results)])
+            [(d / total, sets) for d, (_, sets) in zip(remappedDists, results)])
         return blend
-        
+
     def solve(self, painting):
         """
         given strokes of colors on a photo of the stage, figure out the
@@ -216,7 +229,7 @@
         # this is wrong; some wrong-alignments ought to be dimmer than full
         brightest0 = brightest(pic0)
         brightestSample = brightest(self.samples[sample])
-        
+
         if max(brightest0) < 1 / 255:
             return DeviceSettings(self.graph, [])
 
@@ -234,25 +247,29 @@
 
         # use toVector then add ranges
         dims = [
-            (DEV['aura1'], L9['rx'], [slice(.2, .7+.1, .2)]),
-            (DEV['aura1'], L9['ry'], [slice(.573, .573+1, 1)]),
-            (DEV['aura1'], L9['color'], [slice(0, 1 + colorStep, colorStep),
-                                         slice(0, 1 + colorStep, colorStep),
-                                         slice(0, 1 + colorStep, colorStep)]),
+            (DEV['aura1'], L9['rx'], [slice(.2, .7 + .1, .2)]),
+            (DEV['aura1'], L9['ry'], [slice(.573, .573 + 1, 1)]),
+            (DEV['aura1'], L9['color'], [
+                slice(0, 1 + colorStep, colorStep),
+                slice(0, 1 + colorStep, colorStep),
+                slice(0, 1 + colorStep, colorStep)
+            ]),
         ]
-        deviceAttrFilter = [(d, a) for d,a,s in dims]
+        deviceAttrFilter = [(d, a) for d, a, s in dims]
 
         dist = ImageDist(pic0)
+
         def drawError(x):
-            settings = DeviceSettings.fromVector(self.graph, x, deviceAttrFilter=deviceAttrFilter)
+            settings = DeviceSettings.fromVector(
+                self.graph, x, deviceAttrFilter=deviceAttrFilter)
             preview = self.combineImages(self.simulationLayers(settings))
             #saveNumpy('/tmp/x_%s.png' % abs(hash(settings)), preview)
-            
+
             out = dist.distanceTo(preview)
-            
+
             #print 'measure at', x, 'drawError=', out
             return out
-            
+
         x0, fval, grid, Jout = scipy.optimize.brute(
             func=drawError,
             ranges=sum([s for dev, da, s in dims], []),
@@ -261,8 +278,10 @@
             full_output=True)
         if fval > 30000:
             raise ValueError('solution has error of %s' % fval)
-        return DeviceSettings.fromVector(self.graph, x0, deviceAttrFilter=deviceAttrFilter)
-        
+        return DeviceSettings.fromVector(self.graph,
+                                         x0,
+                                         deviceAttrFilter=deviceAttrFilter)
+
     def combineImages(self, layers):
         """make a result image from our self.samples images"""
         out = (self.fromPath.itervalues().next() * 0).astype(numpy.uint16)
@@ -271,7 +290,7 @@
             out += colorScaled.astype(numpy.uint16)
         numpy.clip(out, 0, 255, out)
         return out.astype(numpy.uint8)
-        
+
     def simulationLayers(self, settings):
         """
         how should a simulation preview approximate the light settings
@@ -282,7 +301,7 @@
 
         for dev, devSettings in settings.byDevice():
             requestedColor = devSettings.getValue(dev, L9['color'])
-            candidatePics = [] # (distance, path, picColor)
+            candidatePics = []  # (distance, path, picColor)
             for sample, s in self.sampleSettings.items():
                 path = self.path[sample]
                 otherDevSettings = s.ofDevice(dev)
@@ -295,9 +314,12 @@
             # we could even blend multiple top candidates, or omit all
             # of them if they're too far
             bestDist, bestPath, bestPicColor = candidatePics[0]
-            log.info('  device best d=%g path=%s color=%s', bestDist, bestPath, bestPicColor)
-            
-            layers.append({'path': bestPath,
-                           'color': colorRatio(requestedColor, bestPicColor)})
-        
+            log.info('  device best d=%g path=%s color=%s', bestDist, bestPath,
+                     bestPicColor)
+
+            layers.append({
+                'path': bestPath,
+                'color': colorRatio(requestedColor, bestPicColor)
+            })
+
         return layers
--- a/light9/paint/solve_test.py	Tue May 21 23:55:35 2019 +0000
+++ b/light9/paint/solve_test.py	Tue May 21 23:56:12 2019 +0000
@@ -6,11 +6,15 @@
 from rdfdb.localsyncedgraph import LocalSyncedGraph
 from light9.effect.settings import DeviceSettings
 
+
 class TestSolve(unittest.TestCase):
+
     def setUp(self):
-        self.graph = LocalSyncedGraph(files=['test/cam/lightConfig.n3',
-                                             'test/cam/bg.n3'])
-        self.solver = solve.Solver(self.graph, imgSize=(100, 48), sessions=[L9['session0']])
+        self.graph = LocalSyncedGraph(
+            files=['test/cam/lightConfig.n3', 'test/cam/bg.n3'])
+        self.solver = solve.Solver(self.graph,
+                                   imgSize=(100, 48),
+                                   sessions=[L9['session0']])
         self.solver.loadSamples()
         self.solveMethod = self.solver.solve
 
@@ -21,87 +25,132 @@
 
     @unittest.skip("unfinished")
     def testSingleLightCloseMatch(self):
-        devAttrs = self.solveMethod({'strokes': [{'pts': [[224, 141],
-                                                 [223, 159]],
-                                         'color': '#ffffff'}]})
-        self.assertEqual(DeviceSettings(self.graph, [
-            (DEV['aura1'], L9['color'], u"#ffffff"),
-            (DEV['aura1'], L9['rx'], 0.5 ),
-            (DEV['aura1'], L9['ry'], 0.573),
-        ]), devAttrs)
+        devAttrs = self.solveMethod({
+            'strokes': [{
+                'pts': [[224, 141], [223, 159]],
+                'color': '#ffffff'
+            }]
+        })
+        self.assertEqual(
+            DeviceSettings(self.graph, [
+                (DEV['aura1'], L9['color'], u"#ffffff"),
+                (DEV['aura1'], L9['rx'], 0.5),
+                (DEV['aura1'], L9['ry'], 0.573),
+            ]), devAttrs)
+
 
 class TestSolveBrute(TestSolve):
+
     def setUp(self):
         super(TestSolveBrute, self).setUp()
         self.solveMethod = self.solver.solveBrute
 
+
 CAM_TEST = Namespace('http://light9.bigasterisk.com/test/cam/')
-        
+
+
 class TestSimulationLayers(unittest.TestCase):
+
     def setUp(self):
-        self.graph = LocalSyncedGraph(files=['test/cam/lightConfig.n3',
-                                             'test/cam/bg.n3'])
-        self.solver = solve.Solver(self.graph, imgSize=(100, 48), sessions=[L9['session0']])
+        self.graph = LocalSyncedGraph(
+            files=['test/cam/lightConfig.n3', 'test/cam/bg.n3'])
+        self.solver = solve.Solver(self.graph,
+                                   imgSize=(100, 48),
+                                   sessions=[L9['session0']])
         self.solver.loadSamples()
-        
+
     def testBlack(self):
-        self.assertEqual(
-            [],
-            self.solver.simulationLayers(settings=DeviceSettings(self.graph, [])))
+        self.assertEqual([],
+                         self.solver.simulationLayers(
+                             settings=DeviceSettings(self.graph, [])))
 
     def testPerfect1Match(self):
-        layers = self.solver.simulationLayers(settings=DeviceSettings(self.graph, [
-            (DEV['aura1'], L9['color'], u"#ffffff"),
-            (DEV['aura1'], L9['rx'], 0.5 ),
-            (DEV['aura1'], L9['ry'], 0.573)]))
-        self.assertEqual([{'path': CAM_TEST['bg2-d.jpg'], 'color': (1., 1., 1.)}], layers)
+        layers = self.solver.simulationLayers(
+            settings=DeviceSettings(self.graph, [(
+                DEV['aura1'], L9['color'],
+                u"#ffffff"), (DEV['aura1'], L9['rx'],
+                              0.5), (DEV['aura1'], L9['ry'], 0.573)]))
+        self.assertEqual([{
+            'path': CAM_TEST['bg2-d.jpg'],
+            'color': (1., 1., 1.)
+        }], layers)
 
     def testPerfect1MatchTinted(self):
-        layers = self.solver.simulationLayers(settings=DeviceSettings(self.graph, [
-            (DEV['aura1'], L9['color'], u"#304050"),
-            (DEV['aura1'], L9['rx'], 0.5 ),
-            (DEV['aura1'], L9['ry'], 0.573)]))
-        self.assertEqual([{'path': CAM_TEST['bg2-d.jpg'], 'color': (.188, .251, .314)}], layers)
-        
+        layers = self.solver.simulationLayers(
+            settings=DeviceSettings(self.graph, [(
+                DEV['aura1'], L9['color'],
+                u"#304050"), (DEV['aura1'], L9['rx'],
+                              0.5), (DEV['aura1'], L9['ry'], 0.573)]))
+        self.assertEqual([{
+            'path': CAM_TEST['bg2-d.jpg'],
+            'color': (.188, .251, .314)
+        }], layers)
+
     def testPerfect2Matches(self):
-        layers = self.solver.simulationLayers(settings=DeviceSettings(self.graph, [
-            (DEV['aura1'], L9['color'], u"#ffffff"),
-            (DEV['aura1'], L9['rx'], 0.5 ),
-            (DEV['aura1'], L9['ry'], 0.573),
-            (DEV['aura2'], L9['color'], u"#ffffff"),
-            (DEV['aura2'], L9['rx'], 0.7 ),
-            (DEV['aura2'], L9['ry'], 0.573),
-        ]))
+        layers = self.solver.simulationLayers(
+            settings=DeviceSettings(self.graph, [
+                (DEV['aura1'], L9['color'], u"#ffffff"),
+                (DEV['aura1'], L9['rx'], 0.5),
+                (DEV['aura1'], L9['ry'], 0.573),
+                (DEV['aura2'], L9['color'], u"#ffffff"),
+                (DEV['aura2'], L9['rx'], 0.7),
+                (DEV['aura2'], L9['ry'], 0.573),
+            ]))
         self.assertItemsEqual([
-            {'path': CAM_TEST['bg2-d.jpg'], 'color': (1, 1, 1)},
-            {'path': CAM_TEST['bg2-f.jpg'], 'color': (1, 1, 1)},
-                      ], layers)
+            {
+                'path': CAM_TEST['bg2-d.jpg'],
+                'color': (1, 1, 1)
+            },
+            {
+                'path': CAM_TEST['bg2-f.jpg'],
+                'color': (1, 1, 1)
+            },
+        ], layers)
+
 
 class TestCombineImages(unittest.TestCase):
+
     def setUp(self):
-        graph = LocalSyncedGraph(files=['test/cam/lightConfig.n3',
-                                        'test/cam/bg.n3'])
-        self.solver = solve.Solver(graph, imgSize=(100, 48), sessions=[L9['session0']])
+        graph = LocalSyncedGraph(
+            files=['test/cam/lightConfig.n3', 'test/cam/bg.n3'])
+        self.solver = solve.Solver(graph,
+                                   imgSize=(100, 48),
+                                   sessions=[L9['session0']])
         self.solver.loadSamples()
+
     def test(self):
         out = self.solver.combineImages(layers=[
-            {'path': CAM_TEST['bg2-d.jpg'], 'color': (.2, .2, .3)},
-            {'path': CAM_TEST['bg2-a.jpg'], 'color': (.888, 0, .3)},
+            {
+                'path': CAM_TEST['bg2-d.jpg'],
+                'color': (.2, .2, .3)
+            },
+            {
+                'path': CAM_TEST['bg2-a.jpg'],
+                'color': (.888, 0, .3)
+            },
         ])
         solve.saveNumpy('/tmp/t.png', out)
         golden = solve.loadNumpy('test/cam/layers_out1.png')
         numpy.testing.assert_array_equal(golden, out)
 
+
 class TestBestMatch(unittest.TestCase):
+
     def setUp(self):
-        graph = LocalSyncedGraph(files=['test/cam/lightConfig.n3',
-                                        'test/cam/bg.n3'])
-        self.solver = solve.Solver(graph, imgSize=(100, 48), sessions=[L9['session0']])
+        graph = LocalSyncedGraph(
+            files=['test/cam/lightConfig.n3', 'test/cam/bg.n3'])
+        self.solver = solve.Solver(graph,
+                                   imgSize=(100, 48),
+                                   sessions=[L9['session0']])
         self.solver.loadSamples()
-        
+
     def testRightSide(self):
-        drawingOnRight = {"strokes":[{"pts":[[0.875,0.64],[0.854,0.644]],
-                                      "color":"#aaaaaa"}]}
+        drawingOnRight = {
+            "strokes": [{
+                "pts": [[0.875, 0.64], [0.854, 0.644]],
+                "color": "#aaaaaa"
+            }]
+        }
         drawImg = self.solver.draw(drawingOnRight)
         match, dist = self.solver.bestMatch(drawImg)
         self.assertEqual(L9['sample5'], match)
--- a/light9/prof.py	Tue May 21 23:55:35 2019 +0000
+++ b/light9/prof.py	Tue May 21 23:56:12 2019 +0000
@@ -1,6 +1,7 @@
 import sys, traceback, time, logging
 log = logging.getLogger()
 
+
 def run(main, profile=None):
     if not profile:
         main()
@@ -20,7 +21,8 @@
         finally:
             statprof.stop()
             statprof.display()
-    
+
+
 def watchPoint(filename, lineno, event="call"):
     """whenever we hit this line, print a stack trace. event='call'
     for lines that are function definitions, like what a profiler
@@ -28,7 +30,8 @@
 
     Switch to 'line' to match lines inside functions. Execution speed
     will be much slower."""
-    seenTraces = {} # trace contents : count
+    seenTraces = {}  # trace contents : count
+
     def trace(frame, ev, arg):
         if ev == event:
             if (frame.f_code.co_filename, frame.f_lineno) == (filename, lineno):
@@ -41,17 +44,21 @@
                     seenTraces[stack] += 1
 
         return trace
+
     sys.settrace(trace)
 
     # atexit, print the frequencies?
 
+
 def logTime(func):
+
     def inner(*args, **kw):
         t1 = time.time()
         try:
             ret = func(*args, **kw)
         finally:
-            log.info("Call to %s took %.1f ms" % (
-                func.__name__, 1000 * (time.time() - t1)))
+            log.info("Call to %s took %.1f ms" % (func.__name__, 1000 *
+                                                  (time.time() - t1)))
         return ret
+
     return inner
--- a/light9/subcomposer/subcomposerweb.py	Tue May 21 23:55:35 2019 +0000
+++ b/light9/subcomposer/subcomposerweb.py	Tue May 21 23:56:12 2019 +0000
@@ -6,21 +6,29 @@
 from twisted.internet import reactor
 log = logging.getLogger('web')
 
+
 def init(graph, session, currentSub):
     SFH = cyclone.web.StaticFileHandler
     app = cyclone.web.Application(handlers=[
-        (r'/()', SFH,
-         {'path': 'light9/subcomposer', 'default_filename': 'index.html'}),
+        (r'/()', SFH, {
+            'path': 'light9/subcomposer',
+            'default_filename': 'index.html'
+        }),
         (r'/toggle', Toggle),
-    ], debug=True, graph=graph, currentSub=currentSub)
+    ],
+                                  debug=True,
+                                  graph=graph,
+                                  currentSub=currentSub)
     reactor.listenTCP(networking.subComposer.port, app)
     log.info("listening on %s" % networking.subComposer.port)
 
+
 class Toggle(PrettyErrorHandler, cyclone.web.RequestHandler):
+
     def post(self):
         chan = URIRef(self.get_argument('chan'))
         sub = self.settings.currentSub()
-        
+
         chanKey = Literal(chan.rsplit('/', 1)[1])
         old = sub.get_levels().get(chanKey, 0)
 
--- a/light9/vidref/main.py	Tue May 21 23:55:35 2019 +0000
+++ b/light9/vidref/main.py	Tue May 21 23:56:12 2019 +0000
@@ -1,5 +1,4 @@
 #!/usr/bin/python
-
 """
 
 dvcam test
@@ -15,7 +14,9 @@
 from light9.vidref import remotepivideo
 log = logging.getLogger()
 
+
 class Gui(object):
+
     def __init__(self, graph):
         wtree = gtk.Builder()
         wtree.add_from_file(sibpath(__file__, "vidref.glade"))
@@ -28,7 +29,8 @@
         self.musicScale = wtree.get_object("musicScale")
         self.musicScale.connect("value-changed", self.onMusicScaleValue)
         # tiny race here if onMusicScaleValue tries to use musicTime right away
-        self.musicTime = MusicTime(onChange=self.onMusicTimeChange, pollCurvecalc=False)
+        self.musicTime = MusicTime(onChange=self.onMusicTimeChange,
+                                   pollCurvecalc=False)
         self.ignoreScaleChanges = False
         # self.attachLog(wtree.get_object("lastLog")) # disabled due to crashing
 
@@ -40,33 +42,31 @@
         vid3 = wtree.get_object("vid3")
 
         if 0:
-            self.pipeline = Pipeline(
-                liveVideoXid=vid3.window.xid,
-                musicTime=self.musicTime,
-                recordingTo=self.recordingTo)
+            self.pipeline = Pipeline(liveVideoXid=vid3.window.xid,
+                                     musicTime=self.musicTime,
+                                     recordingTo=self.recordingTo)
         else:
-            self.pipeline = remotepivideo.Pipeline(
-                liveVideo=vid3,
-                musicTime=self.musicTime,
-                recordingTo=self.recordingTo,
-                graph=graph)
+            self.pipeline = remotepivideo.Pipeline(liveVideo=vid3,
+                                                   musicTime=self.musicTime,
+                                                   recordingTo=self.recordingTo,
+                                                   graph=graph)
 
         vid3.props.width_request = 360
         vid3.props.height_request = 220
         wtree.get_object("frame1").props.height_request = 220
-        
 
-        self.pipeline.setInput('v4l') # auto seems to not search for dv
+        self.pipeline.setInput('v4l')  # auto seems to not search for dv
 
         gobject.timeout_add(1000 // framerate, self.updateLoop)
 
-
     def snapshot(self):
         return self.pipeline.snapshot()
-        
+
     def attachLog(self, textBuffer):
         """write log lines to this gtk buffer"""
+
         class ToBuffer(logging.Handler):
+
             def emit(self, record):
                 textBuffer.set_text(record.getMessage())
 
@@ -86,10 +86,9 @@
     def getInputs(self):
         return ['auto', 'dv', 'video0']
 
-
     def on_liveVideoEnabled_toggled(self, widget):
         self.pipeline.setLiveVideo(widget.get_active())
-                                                   
+
     def on_liveFrameRate_value_changed(self, widget):
         print widget.get_value()
 
--- a/light9/vidref/musictime.py	Tue May 21 23:55:35 2019 +0000
+++ b/light9/vidref/musictime.py	Tue May 21 23:56:12 2019 +0000
@@ -6,12 +6,17 @@
 import http_parser.http
 log = logging.getLogger()
 
+
 class MusicTime(object):
     """
     fetch times from ascoltami in a background thread; return times
     upon request, adjusted to be more precise with the system clock
     """
-    def __init__(self, period=.2, onChange=lambda position: None, pollCurvecalc=True):
+
+    def __init__(self,
+                 period=.2,
+                 onChange=lambda position: None,
+                 pollCurvecalc=True):
         """period is the seconds between http time requests.
 
         We call onChange with the time in seconds and the total time
@@ -28,7 +33,7 @@
 
         self.position = {}
         # driven by our pollCurvecalcTime and also by Gui.incomingTime
-        self.lastHoverTime = None # None means "no recent value"
+        self.lastHoverTime = None  # None means "no recent value"
         self.pollMusicTime()
         if pollCurvecalc:
             self.pollCurvecalcTime()
@@ -43,7 +48,7 @@
         Note that this may be called in a gst camera capture thread. Very often.
         """
         if not hasattr(self, 'position'):
-            return {'t' : 0, 'song' : None}
+            return {'t': 0, 'song': None}
         pos = self.position.copy()
         now = frameTime or time.time()
         if pos.get('playing'):
@@ -54,6 +59,7 @@
         return pos
 
     def pollMusicTime(self):
+
         def cb(response):
 
             if response.code != 200:
@@ -71,14 +77,14 @@
             self.onChange(position)
 
             reactor.callLater(self.period, self.pollMusicTime)
-            
+
         def eb(err):
             log.warn("talking to ascoltami: %s", err.getErrorMessage())
             reactor.callLater(2, self.pollMusicTime)
-            
+
         d = fetch(networking.musicPlayer.path("time"))
         d.addCallback(cb)
-        d.addErrback(eb) # note this includes errors in cb()
+        d.addErrback(eb)  # note this includes errors in cb()
 
     def pollCurvecalcTime(self):
         """
@@ -94,7 +100,7 @@
             self.lastHoverTime = None
             reactor.callLater(.2, self.pollCurvecalcTime)
             return
-            
+
         def cb(response):
             if response.code == 404:
                 # not hovering
@@ -115,9 +121,10 @@
 
         d = fetch(networking.curveCalc.path("hoverTime"))
         d.addCallback(cb)
-        d.addErrback(eb) # note this includes errors in cb()
-        
+        d.addErrback(eb)  # note this includes errors in cb()
+
     def sendTime(self, t):
         """request that the player go to this time"""
-        self.musicResource.post("time", payload=json.dumps({"t" : t}),
-                                headers={"content-type" : "application/json"})
+        self.musicResource.post("time",
+                                payload=json.dumps({"t": t}),
+                                headers={"content-type": "application/json"})
--- a/light9/vidref/qt_test.py	Tue May 21 23:55:35 2019 +0000
+++ b/light9/vidref/qt_test.py	Tue May 21 23:56:12 2019 +0000
@@ -4,7 +4,9 @@
 import gst
 import gobject
 
+
 class Vid(object):
+
     def __init__(self, windowId):
         self.player = gst.Pipeline("player")
         self.source = gst.element_factory_make("v4l2src", "vsource")
@@ -14,20 +16,24 @@
         self.window_id = None
         self.windowId = windowId
 
-        self.fvidscale_cap = gst.element_factory_make("capsfilter", "fvidscale_cap")
-        self.fvidscale_cap.set_property('caps', gst.caps_from_string('video/x-raw-yuv, width=320, height=240'))
-        
+        self.fvidscale_cap = gst.element_factory_make("capsfilter",
+                                                      "fvidscale_cap")
+        self.fvidscale_cap.set_property(
+            'caps',
+            gst.caps_from_string('video/x-raw-yuv, width=320, height=240'))
+
         self.player.add(self.source, self.scaler, self.fvidscale_cap, self.sink)
-        gst.element_link_many(self.source,self.scaler, self.fvidscale_cap, self.sink)
+        gst.element_link_many(self.source, self.scaler, self.fvidscale_cap,
+                              self.sink)
 
         self.s = MySink()
         self.player.add(self.s)
-#        self.scaler.link(self.s)
+        #        self.scaler.link(self.s)
 
         bus = self.player.get_bus()
         bus.add_signal_watch()
-#        bus.enable_sync_message_emission() # with this we segv
-#        bus.connect("message", self.on_message) # with this we segv
+        #        bus.enable_sync_message_emission() # with this we segv
+        #        bus.connect("message", self.on_message) # with this we segv
         bus.connect("sync-message::element", self.on_sync_message)
 
     def on_message(self, bus, message):
@@ -36,9 +42,9 @@
         if t == gst.MESSAGE_EOS:
             self.player.set_state(gst.STATE_NULL)
         elif t == gst.MESSAGE_ERROR:
-           err, debug = message.parse_error()
-           print "Error: %s" % err, debug
-           self.player.set_state(gst.STATE_NULL)
+            err, debug = message.parse_error()
+            print "Error: %s" % err, debug
+            self.player.set_state(gst.STATE_NULL)
 
     def on_sync_message(self, bus, message):
         print "syncmsg", bus, message
@@ -58,9 +64,10 @@
     def startPrev(self):
         self.player.set_state(gst.STATE_PLAYING)
         print "should be playing"
-        
+
 
 class MainWin(QtGui.QMainWindow):
+
     def __init__(self, *args):
         super(MainWin, self).__init__(*args)
 
@@ -71,5 +78,3 @@
     @QtCore.pyqtSlot()
     def startLiveView(self):
         print "slv"
-
-
--- a/light9/vidref/remotepivideo.py	Tue May 21 23:55:35 2019 +0000
+++ b/light9/vidref/remotepivideo.py	Tue May 21 23:56:12 2019 +0000
@@ -13,13 +13,15 @@
 from StringIO import StringIO
 log = logging.getLogger('remotepi')
 
+
 class Pipeline(object):
+
     def __init__(self, liveVideo, musicTime, recordingTo, graph):
         self.musicTime = musicTime
         self.recordingTo = recordingTo
 
         self.liveVideo = self._replaceLiveVideoWidget(liveVideo)
-        
+
         self._snapshotRequests = []
         self.graph = graph
         self.graph.addHandler(self.updateCamUrl)
@@ -30,10 +32,10 @@
         log.info("picsUrl now %r", self.picsUrl)
         if not self.picsUrl:
             return
-        
+
         # this cannot yet survive being called a second time
         self._startRequest(str(self.picsUrl))
-        
+
     def _replaceLiveVideoWidget(self, liveVideo):
         aspectFrame = liveVideo.get_parent()
         liveVideo.destroy()
@@ -42,7 +44,7 @@
         #img.set_size_request(320, 240)
         aspectFrame.add(img)
         return img
-        
+
     def _startRequest(self, url):
         self._buffer = ''
         log.info('start request to %r', url)
@@ -60,10 +62,10 @@
         size = int(size)
         if len(self._buffer) - i - 1 < size:
             return
-        jpg = self._buffer[i+1:i+1+size]
+        jpg = self._buffer[i + 1:i + 1 + size]
         self.onFrame(jpg, float(frameTime))
-        self._buffer = self._buffer[i+1+size:]
-        
+        self._buffer = self._buffer[i + 1 + size:]
+
     def snapshot(self):
         """
         returns deferred to the path (which is under snapshotDir()) where
@@ -93,14 +95,13 @@
                 out.write(jpg)
             d.callback(filename)
         self._snapshotRequests[:] = []
-            
-        
+
         if not position['song']:
             self.updateLiveFromTemp(jpg)
-            return 
+            return
         outDir = takeDir(songDir(position['song']), position['started'])
         outFilename = "%s/%08.03f.jpg" % (outDir, position['t'])
-        if os.path.exists(outFilename): # we're paused on one time
+        if os.path.exists(outFilename):  # we're paused on one time
             self.updateLiveFromTemp(jpg)
             return
         try:
@@ -111,14 +112,14 @@
             out.write(jpg)
 
         self.updateLiveFromFile(outFilename)
-            
+
         # if you're selecting the text while gtk is updating it,
         # you can get a crash in xcb_io
         if getattr(self, '_lastRecText', None) != outDir:
             with gtk.gdk.lock:
                 self.recordingTo.set_text(outDir)
             self._lastRecText = outDir
-            
+
     def updateLiveFromFile(self, outFilename):
         self.liveVideo.set_from_file(outFilename)
 
@@ -127,17 +128,14 @@
             img = Image.open(StringIO(jpg))
             if not hasattr(self, 'livePixBuf'):
                 self.livePixBuf = gtk.gdk.pixbuf_new_from_data(
-                    img.tobytes(),
-                    gtk.gdk.COLORSPACE_RGB,
-                    False, 8,
-                    img.size[0], img.size[1],
-                    img.size[0]*3)
+                    img.tobytes(), gtk.gdk.COLORSPACE_RGB, False, 8,
+                    img.size[0], img.size[1], img.size[0] * 3)
                 log.info("live images are %r", img.size)
             else:
                 # don't leak pixbufs; update the one we have
                 a = self.livePixBuf.pixel_array
                 newImg = numpy.fromstring(img.tobytes(), dtype=numpy.uint8)
-                a[:,:,:] = newImg.reshape(a.shape)
+                a[:, :, :] = newImg.reshape(a.shape)
             self.liveVideo.set_from_pixbuf(self.livePixBuf)
 
         except Exception:
--- a/light9/vidref/replay.py	Tue May 21 23:55:35 2019 +0000
+++ b/light9/vidref/replay.py	Tue May 21 23:56:12 2019 +0000
@@ -6,31 +6,36 @@
 
 framerate = 15
 
+
 def songDir(song):
-    safeUri = song.split('://')[-1].replace('/','_')
+    safeUri = song.split('://')[-1].replace('/', '_')
     return os.path.expanduser("~/light9-vidref/play-%s" % safeUri)
 
+
 def takeDir(songDir, startTime):
     """
     startTime: unix seconds (str ok)
     """
     return os.path.join(songDir, str(int(startTime)))
 
+
 def snapshotDir():
     return os.path.expanduser("~/light9-vidref/snapshot")
-    
+
+
 class ReplayViews(object):
     """
     the whole list of replay windows. parent is the scrolling area for
     these windows to be added
     """
+
     def __init__(self, parent):
         # today, parent is the vbox the replay windows should appear in
         self.parent = parent
         self.lastStart = None
 
         self.views = []
-     
+
     def update(self, position):
         """
         freshen all replay windows. We get called this about every
@@ -46,8 +51,8 @@
             self.lastStart = position['started']
         for v in self.views:
             v.updatePic(position)
-        log.debug("update %s views in %.2fms",
-                  len(self.views), (time.time() - t1) * 1000)
+        log.debug("update %s views in %.2fms", len(self.views),
+                  (time.time() - t1) * 1000)
 
     def loadViewsForSong(self, song):
         """
@@ -62,7 +67,7 @@
             takes = sorted(t for t in os.listdir(d) if t.isdigit())
         except OSError:
             return
-        
+
         for take in takes:
             td = takeDir(songDir(song), take)
             r = Replay(td)
@@ -76,10 +81,12 @@
             rv = ReplayView(self.parent, r)
             self.views.append(rv)
 
+
 class ReplayView(object):
     """
     one of the replay widgets
     """
+
     def __init__(self, parent, replay):
         self.replay = replay
         self.enabled = True
@@ -134,16 +141,20 @@
             if True:
                 en = withLabel(gtk.ToggleButton, "Enabled")
                 en.set_active(True)
+
                 def tog(w):
                     self.enabled = w.get_active()
+
                 en.connect("toggled", tog)
                 rows.append(en)
             if True:
                 d = withLabel(gtk.Button, "Delete")
                 d.props.image = delImage
+
                 def onClicked(w):
                     self.replay.deleteDir()
                     self.destroy()
+
                 d.connect("clicked", onClicked)
                 rows.append(d)
             if True:
@@ -156,7 +167,7 @@
             for r in rows:
                 stack.add(r)
                 stack.set_child_packing(r, False, False, 0, gtk.PACK_START)
-            
+
             replayPanel.pack_start(stack, False, False, 0)
 
         parent.pack_start(replayPanel, False, False)
@@ -166,7 +177,7 @@
     def destroy(self):
         self.replayPanel.destroy()
         self.enabled = False
-        
+
     def updatePic(self, position, lag=.2):
 
         # this should skip updating off-screen widgets! maybe that is
@@ -185,26 +196,30 @@
             self.picWidget.set_from_file(inPic)
             if 0:
                 # force redraw of that widget
-                self.picWidget.queue_draw_area(0,0,320,240)
+                self.picWidget.queue_draw_area(0, 0, 320, 240)
                 self.picWidget.get_window().process_updates(True)
         self.showingPic = inPic
 
+
 _existingFrames = {}  # takeDir : frames
-    
+
+
 class Replay(object):
     """
     model for one of the replay widgets
     """
+
     def __init__(self, takeDir):
         self.takeDir = takeDir
         try:
             self.existingFrames = _existingFrames[self.takeDir]
         except KeyError:
             log.info("scanning %s", self.takeDir)
-            self.existingFrames = sorted([Decimal(f.split('.jpg')[0])
-                                          for f in os.listdir(self.takeDir)])
+            self.existingFrames = sorted(
+                [Decimal(f.split('.jpg')[0]) for f in os.listdir(self.takeDir)])
             if not self.existingFrames:
-                raise NotImplementedError("suspiciously found no frames in dir %s" % self.takeDir)
+                raise NotImplementedError(
+                    "suspiciously found no frames in dir %s" % self.takeDir)
             _existingFrames[self.takeDir] = self.existingFrames
 
     def tooShort(self, minSeconds=5):
@@ -238,5 +253,5 @@
         i = bisect_left(self.existingFrames, Decimal(str(t)))
         if i >= len(self.existingFrames):
             i = len(self.existingFrames) - 1
-        return os.path.join(self.takeDir, "%08.03f.jpg" %
-                            self.existingFrames[i])
+        return os.path.join(self.takeDir,
+                            "%08.03f.jpg" % self.existingFrames[i])
--- a/light9/vidref/videorecorder.py	Tue May 21 23:55:35 2019 +0000
+++ b/light9/vidref/videorecorder.py	Tue May 21 23:56:12 2019 +0000
@@ -9,7 +9,9 @@
 from light9.vidref.replay import framerate, songDir, takeDir, snapshotDir
 log = logging.getLogger()
 
+
 class Pipeline(object):
+
     def __init__(self, liveVideoXid, musicTime, recordingTo):
         self.musicTime = musicTime
         self.liveVideoXid = liveVideoXid
@@ -28,28 +30,31 @@
         but I haven't noticed that being a problem yet.
         """
         d = defer.Deferred()
+
         def req(frame):
             filename = "%s/%s.jpg" % (snapshotDir(), time.time())
             log.debug("received snapshot; saving in %s", filename)
             frame.save(filename)
             d.callback(filename)
+
         log.debug("requesting snapshot")
         self.snapshotRequests.put(req)
         return d
-        
+
     def setInput(self, name):
         sourcePipe = {
             "auto": "autovideosrc name=src1",
-            "testpattern" : "videotestsrc name=src1",
+            "testpattern": "videotestsrc name=src1",
             "dv": "dv1394src name=src1 ! dvdemux ! dvdec",
-            "v4l": "v4l2src device=/dev/video0 name=src1" ,
-            }[name]
+            "v4l": "v4l2src device=/dev/video0 name=src1",
+        }[name]
 
-        cam = (sourcePipe + " ! "
-              "videorate ! video/x-raw-yuv,framerate=%s/1 ! "
-              "videoscale ! video/x-raw-yuv,width=640,height=480;video/x-raw-rgb,width=320,height=240 ! "
-              "videocrop left=160 top=180 right=120 bottom=80 ! "
-              "queue name=vid" % framerate)
+        cam = (
+            sourcePipe + " ! "
+            "videorate ! video/x-raw-yuv,framerate=%s/1 ! "
+            "videoscale ! video/x-raw-yuv,width=640,height=480;video/x-raw-rgb,width=320,height=240 ! "
+            "videocrop left=160 top=180 right=120 bottom=80 ! "
+            "queue name=vid" % framerate)
 
         print cam
         self.pipeline = gst.parse_launch(cam)
@@ -58,8 +63,9 @@
             e = gst.element_factory_make(t, n)
             self.pipeline.add(e)
             return e
-        
+
         sink = makeElem("xvimagesink")
+
         def setRec(t):
             # if you're selecting the text while gtk is updating it,
             # you can get a crash in xcb_io
@@ -68,21 +74,22 @@
             with gtk.gdk.lock:
                 self.recordingTo.set_text(t)
             self._lastRecText = t
+
         recSink = VideoRecordSink(self.musicTime, setRec, self.snapshotRequests)
         self.pipeline.add(recSink)
 
         tee = makeElem("tee")
-        
+
         caps = makeElem("capsfilter")
         caps.set_property('caps', gst.caps_from_string('video/x-raw-rgb'))
 
         gst.element_link_many(self.pipeline.get_by_name("vid"), tee, sink)
         gst.element_link_many(tee, makeElem("ffmpegcolorspace"), caps, recSink)
         sink.set_xwindow_id(self.liveVideoXid)
-        self.pipeline.set_state(gst.STATE_PLAYING)        
+        self.pipeline.set_state(gst.STATE_PLAYING)
 
     def setLiveVideo(self, on):
-        
+
         if on:
             self.pipeline.set_state(gst.STATE_PLAYING)
             # this is an attempt to bring the dv1394 source back, but
@@ -91,13 +98,11 @@
                 gst.FORMAT_TIME, gst.SEEK_FLAG_FLUSH, 0 * gst.SECOND)
         else:
             self.pipeline.set_state(gst.STATE_READY)
-                                                   
+
 
 class VideoRecordSink(gst.Element):
-    _sinkpadtemplate = gst.PadTemplate ("sinkpadtemplate",
-                                        gst.PAD_SINK,
-                                        gst.PAD_ALWAYS,
-                                        gst.caps_new_any())
+    _sinkpadtemplate = gst.PadTemplate("sinkpadtemplate", gst.PAD_SINK,
+                                       gst.PAD_ALWAYS, gst.caps_new_any())
 
     def __init__(self, musicTime, updateRecordingTo, snapshotRequests):
         gst.Element.__init__(self)
@@ -107,14 +112,15 @@
         self.add_pad(self.sinkpad)
         self.sinkpad.set_chain_function(self.chainfunc)
         self.lastTime = 0
-        
+
         self.musicTime = musicTime
 
         self.imagesToSave = Queue()
         self.startBackgroundImageSaver(self.imagesToSave)
-        
+
     def startBackgroundImageSaver(self, imagesToSave):
         """do image saves in another thread to not block gst"""
+
         def imageSaver():
             while True:
                 args = imagesToSave.get()
@@ -135,7 +141,7 @@
                 else:
                     req(args[1])
                     self.snapshotRequests.task_done()
-        
+
         t = Thread(target=imageSaver)
         t.setDaemon(True)
         t.start()
@@ -159,14 +165,14 @@
 
     def saveImg(self, position, img, bufferTimestamp):
         if not position['song']:
-            return 
-        
+            return
+
         t1 = time.time()
         outDir = takeDir(songDir(position['song']), position['started'])
         outFilename = "%s/%08.03f.jpg" % (outDir, position['t'])
-        if os.path.exists(outFilename): # we're paused on one time
+        if os.path.exists(outFilename):  # we're paused on one time
             return
-        
+
         try:
             os.makedirs(outDir)
         except OSError:
@@ -175,11 +181,10 @@
         img.save(outFilename)
 
         now = time.time()
-        log.info("wrote %s delay of %.2fms, took %.2fms",
-                  outFilename,
-                  (now - self.lastTime) * 1000,
-                  (now - t1) * 1000)
+        log.info("wrote %s delay of %.2fms, took %.2fms", outFilename,
+                 (now - self.lastTime) * 1000, (now - t1) * 1000)
         self.updateRecordingTo(outDir)
         self.lastTime = now
 
+
 gobject.type_register(VideoRecordSink)