diff --git a/bin/ascoltami2 b/bin/ascoltami2 --- a/bin/ascoltami2 +++ b/bin/ascoltami2 @@ -4,7 +4,7 @@ from twisted.internet import reactor 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 light9 import networking, showconfi 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 @@ class App(object): 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 __name__ == "__main__": if not options.show: raise ValueError("missing --show http://...") - + graph = showconfig.getGraph() app = App(graph, URIRef(options.show)) if options.twistedlog: diff --git a/bin/bcf_puppet_demo b/bin/bcf_puppet_demo --- a/bin/bcf_puppet_demo +++ b/bin/bcf_puppet_demo @@ -5,12 +5,13 @@ tiny bcf2000 controller demo 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() diff --git a/bin/bumppad b/bin/bumppad --- a/bin/bumppad +++ b/bin/bumppad @@ -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 run_local 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(""%key, - lambda ev,sub=sub: self.bumpto(sub,1)) - root.bind(""%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( + "" % key, lambda ev, sub=sub: self.bumpto(sub, 1)) + root.bind("" % 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(""%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("" % i, lambda ev, i=i: mag.set(math.sqrt((i) / 5))) +magscl.pack(side='left', fill='y') root.mainloop() diff --git a/bin/captureDevice b/bin/captureDevice --- a/bin/captureDevice +++ b/bin/captureDevice @@ -26,15 +26,18 @@ from rdfdb.patch import Patch 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 @@ class Camera(object): 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 @@ class Capture(object): # 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 @@ class Capture(object): 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 @@ class Capture(object): 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() diff --git a/bin/clientdemo b/bin/clientdemo --- a/bin/clientdemo +++ b/bin/clientdemo @@ -16,12 +16,13 @@ if __name__ == "__main__": 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 @@ if __name__ == "__main__": 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() diff --git a/bin/collector b/bin/collector --- a/bin/collector +++ b/bin/collector @@ -29,6 +29,7 @@ from light9 import networking from rdfdb.syncedgraph import SyncedGraph from light9.greplin_cyclone import StatsForCyclone + def parseJsonMessage(msg): body = json.loads(msg) settings = [] @@ -40,14 +41,15 @@ def parseJsonMessage(msg): 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 @@ def startZmq(port, collector): # 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 @@ class WebListeners(object): 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 @@ class WebListeners(object): 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 @@ class Updates(cyclone.websocket.WebSocke 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 @@ def launch(graph, doLoadTest=False): 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 @@ def launch(graph, doLoadTest=False): # 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() diff --git a/bin/collector_loadtest.py b/bin/collector_loadtest.py --- a/bin/collector_loadtest.py +++ b/bin/collector_loadtest.py @@ -7,6 +7,8 @@ from twisted.internet import reactor import time import logging log.setLevel(logging.DEBUG) + + def loadTest(): print "scheduling loadtest" n = 2500 @@ -14,23 +16,27 @@ def loadTest(): 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 @@ def loadTest(): 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() diff --git a/bin/curvecalc b/bin/curvecalc --- a/bin/curvecalc +++ b/bin/curvecalc @@ -1,5 +1,4 @@ #!bin/python - """ now launches like this: % bin/curvecalc http://light9.bigasterisk.com/show/dance2007/song1 @@ -12,7 +11,7 @@ todo: curveview should preserve more obj 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 GObject 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.patch import Patch 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 @@ class Main(object): 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 @@ class Main(object): 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 @@ class Main(object): 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 @@ class Main(object): # 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 @@ class Main(object): 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 @@ class Main(object): 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 @@ class Main(object): (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 @@ class Main(object): 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 @@ class Main(object): 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 @@ class Main(object): * { 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 @@ class Main(object): ns.update(globals()) ns.update(self.__dict__) togglePyConsole(self, item, ns) - + def onSeeCurrentTime(self, item): dispatcher.send("see time") @@ -395,15 +409,14 @@ class Main(object): 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 @@ class Main(object): 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 @@ class Main(object): 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 @@ class MaxTime(object): """ 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 @@ class MaxTime(object): def get(self): return self.maxtime + def launch(args, graph, session, opts, startTime, music): try: @@ -492,7 +510,7 @@ def launch(args, graph, session, opts, s pass curveset = Curveset(graph=graph, session=session) - + log.debug("startup: output %s", time.time() - startTime) mt = MaxTime(graph, session) @@ -501,9 +519,8 @@ def launch(args, graph, session, opts, s 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 @@ def launch(args, graph, session, opts, s 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 @@ def main(): 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() - diff --git a/bin/dmx_color_test.py b/bin/dmx_color_test.py --- a/bin/dmx_color_test.py +++ b/bin/dmx_color_test.py @@ -7,6 +7,7 @@ from twisted.internet import reactor, ta log.setLevel(logging.INFO) firstDmxChannel = 10 + def step(): hue = (time.time() * .2) % 1.0 r, g, b = colorsys.hsv_to_rgb(hue, 1, 1) diff --git a/bin/dmxserver b/bin/dmxserver --- a/bin/dmxserver +++ b/bin/dmxserver @@ -27,10 +27,10 @@ todo: 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 light9 import networking 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 @@ class ReceiverApplication(object): 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 @@ class ReceiverApplication(object): 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 @@ class XMLRPCServe(xmlrpc.XMLRPC): """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 @@ class XMLRPCServe(xmlrpc.XMLRPC): 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() - diff --git a/bin/effecteval b/bin/effecteval --- a/bin/effecteval +++ b/bin/effecteval @@ -8,8 +8,8 @@ import cyclone.web, cyclone.websocket, c 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 greplin import scales from lib.cycloneerr import PrettyErrorHandler + class EffectEdit(PrettyErrorHandler, cyclone.web.RequestHandler): + def get(self): - self.set_header('Content-Type', 'text/html') + self.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 @@ def currentSong(): 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 @@ class SongEffects(PrettyErrorHandler, cy 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 @@ class SongEffectsUpdates(cyclone.websock 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 @@ class EffectUpdates(cyclone.websocket.We 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 @@ class EffectUpdates(cyclone.websocket.We 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 @@ def replaceObjects(graph, c, s, p, newOb 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 @@ class Code(PrettyErrorHandler, cyclone.w 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 @@ class EffectEval(PrettyErrorHandler, cyc # 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 @@ class SongEffectsEval(PrettyErrorHandler 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 @@ class App(object): 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 __name__ == "__main__": 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 diff --git a/bin/effectsequencer b/bin/effectsequencer --- a/bin/effectsequencer +++ b/bin/effectsequencer @@ -17,7 +17,9 @@ from light9.collector.collector_client i from light9 import clientsession + class App(object): + def __init__(self, show, session): self.show = show self.session = session @@ -25,23 +27,25 @@ class App(object): 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 @@ class App(object): 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 __name__ == "__main__": if not options.show: raise ValueError("missing --show http://...") - + session = clientsession.getUri('effectSequencer', options) app = App(URIRef(options.show), session) diff --git a/bin/homepageConfig b/bin/homepageConfig --- a/bin/homepageConfig +++ b/bin/homepageConfig @@ -17,6 +17,7 @@ if not webServer: 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 @@ def location(path, server): 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 @@ for role, server in sorted(graph.predica 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)] +} diff --git a/bin/inputdemo b/bin/inputdemo --- a/bin/inputdemo +++ b/bin/inputdemo @@ -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 @@ from rdfdb.syncedgraph import SyncedGrap 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 @@ class App(object): 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 @@ class App(object): 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() diff --git a/bin/inputquneo b/bin/inputquneo --- a/bin/inputquneo +++ b/bin/inputquneo @@ -14,18 +14,20 @@ from rdfdb.syncedgraph import SyncedGrap 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 @@ class WatchMidi(object): 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 @@ class WatchMidi(object): 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 @@ class WatchMidi(object): 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 @@ class WatchMidi(object): 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 @@ class WatchMidi(object): 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 @@ class WatchMidi(object): 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() diff --git a/bin/kcclient b/bin/kcclient --- a/bin/kcclient +++ b/bin/kcclient @@ -1,5 +1,4 @@ #!/usr/bin/env python - """send KeyboardComposer a fade request, for use from the shell""" import sys @@ -10,10 +9,8 @@ from light9 import networking 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) - - diff --git a/bin/keyboardcomposer b/bin/keyboardcomposer --- a/bin/keyboardcomposer +++ b/bin/keyboardcomposer @@ -27,43 +27,58 @@ from light9.effect.simple_outputs import 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 @@ class SubmasterBox(tk.Frame): 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 @@ class SubmasterBox(tk.Frame): 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 @@ class SubmasterBox(tk.Frame): 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 @@ class SubmasterBox(tk.Frame): 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 @@ class KeyboardComposer(tk.Frame, SubClie 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 @@ class KeyboardComposer(tk.Frame, SubClie 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 @@ class KeyboardComposer(tk.Frame, SubClie 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 @@ class KeyboardComposer(tk.Frame, SubClie 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 @@ class KeyboardComposer(tk.Frame, SubClie 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 @@ class KeyboardComposer(tk.Frame, SubClie 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 @@ class KeyboardComposer(tk.Frame, SubClie 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 @@ class KeyboardComposer(tk.Frame, SubClie 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 @@ class KeyboardComposer(tk.Frame, SubClie if boxRow != self.current_row: return - + try: level = self.get_levels()[sub] except KeyError: @@ -421,13 +467,16 @@ class KeyboardComposer(tk.Frame, SubClie """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 @@ class KeyboardComposer(tk.Frame, SubClie """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 @@ class KeyboardComposer(tk.Frame, SubClie 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 @@ class KeyboardComposer(tk.Frame, SubClie (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 @@ class KeyboardComposer(tk.Frame, SubClie 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 @@ def postArgGetter(request): 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 @@ class LevelServerHttp(resource.Resource) 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 @@ class Sliders(BCF2000): 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 @@ class Sliders(BCF2000): 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 @@ if __name__ == "__main__": # 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) diff --git a/bin/lightsim b/bin/lightsim --- a/bin/lightsim +++ b/bin/lightsim @@ -21,26 +21,33 @@ from light9.namespaces import L9 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 @@ def poll(graph, serv, pollFreq, oglSurfa 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 @@ class StatusKeys(QWidget): 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 @@ class Window(QMainWindow): 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 @@ def requiredImages(graph): filenames.append(str(p)) return filenames + if __name__ == '__main__': app = reactor.qApp @@ -123,4 +137,3 @@ if __name__ == '__main__': LoopingCall(poll, graph, serv, pollFreq, window.glWidget).start(.05) reactor.run() - diff --git a/bin/listsongs b/bin/listsongs --- a/bin/listsongs +++ b/bin/listsongs @@ -1,5 +1,4 @@ #!bin/python - """for completion, print the available song uris on stdout in .zshrc: @@ -17,6 +16,7 @@ from rdfdb.syncedgraph import SyncedGrap graph = SyncedGraph(networking.rdfdb.url, "listsongs") + @graph.initiallySynced.addCallback def printSongs(result): with graph.currentState() as current: @@ -24,4 +24,5 @@ def printSongs(result): print song reactor.stop() + reactor.run() diff --git a/bin/mpd_timing_test b/bin/mpd_timing_test --- a/bin/mpd_timing_test +++ b/bin/mpd_timing_test @@ -1,5 +1,4 @@ #!/usr/bin/python - """ records times coming out of ascoltami @@ -17,5 +16,5 @@ import xmlrpclib, time 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) diff --git a/bin/musicPad b/bin/musicPad --- a/bin/musicPad +++ b/bin/musicPad @@ -11,7 +11,7 @@ logging.basicConfig(level=logging.INFO) 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 @@ for p in playlist.allSongPaths(): 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) - - diff --git a/bin/musictime b/bin/musictime --- a/bin/musictime +++ b/bin/musictime @@ -6,9 +6,12 @@ import Tkinter as tk 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 @@ class MusicTime: 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('', 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') diff --git a/bin/paintserver b/bin/paintserver --- a/bin/paintserver +++ b/bin/paintserver @@ -17,31 +17,38 @@ from lib.cycloneerr import PrettyErrorHa 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 @@ class BestMatches(PrettyErrorHandler, cy 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 @@ class App(object): 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 __name__ == "__main__": if not options.show: raise ValueError("missing --show http://...") - + session = clientsession.getUri('paint', options) app = App(URIRef(options.show), session) diff --git a/bin/picamserve b/bin/picamserve --- a/bin/picamserve +++ b/bin/picamserve @@ -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 @@ try: 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 @@ def setCameraParams(c, arg): 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 @@ def setupCrop(c, arg): # 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 @@ def getFrame(c, arg): 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 @@ class Pic(cyclone.web.RequestHandler): 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 @@ def captureContinuousAsync(c, resize, on # 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 @@ def captureContinuousAsync(c, resize, on 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 @@ class Pics(cyclone.web.RequestHandler): 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 @@ class Pics(cyclone.web.RequestHandler): 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() diff --git a/bin/rdfdb b/bin/rdfdb --- a/bin/rdfdb +++ b/bin/rdfdb @@ -5,8 +5,9 @@ from light9 import networking, showconfi 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 @@ rdfdb.service.main( 'dev': 'http://light9.bigasterisk.com/device/', }, port=networking.rdfdb.port, - ) - +) diff --git a/bin/run_local.py b/bin/run_local.py --- a/bin/run_local.py +++ b/bin/run_local.py @@ -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 @@ def fixSysPath(): root + 'env/lib/python2.7/site-packages/gtk-2.0', ] + fixSysPath() from twisted.python.failure import Failure @@ -55,12 +63,14 @@ try: 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 @@ except ImportError: 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 @@ class FractionTimeFilter(logging.Filter) # 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 @@ logging.getLogger().handlers[0].addFilte 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 diff --git a/bin/staticclient b/bin/staticclient --- a/bin/staticclient +++ b/bin/staticclient @@ -16,7 +16,8 @@ from light9 import dmxclient, showconfig 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 @@ if __name__ == "__main__": 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() diff --git a/bin/subcomposer b/bin/subcomposer --- a/bin/subcomposer +++ b/bin/subcomposer @@ -69,6 +69,7 @@ class Subcomposer(tk.Frame): 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 @@ class Subcomposer(tk.Frame): 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 @@ class Subcomposer(tk.Frame): e.pack() b = tk.Button(box, text="Make global") b.pack() + def clicked(*args): self.makeGlobal(newName=e.get()) box.destroy() - + b.bind("", 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 @@ class Subcomposer(tk.Frame): 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 @@ class Subcomposer(tk.Frame): # 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 @@ class Subcomposer(tk.Frame): 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 @@ class Subcomposer(tk.Frame): 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 @@ class Subcomposer(tk.Frame): 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 @@ class Subcomposer(tk.Frame): 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 @@ def launch(opts, args, root, graph, sess 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 @@ def launch(opts, args, root, graph, sess # 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 @@ def launch(opts, args, root, graph, sess 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 @@ if __name__ == "__main__": 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 @@ if __name__ == "__main__": 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) diff --git a/bin/subserver b/bin/subserver --- a/bin/subserver +++ b/bin/subserver @@ -16,17 +16,19 @@ from light9.namespaces import L9, DCTERM 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 @@ class Static(PrettyErrorHandler, cyclone 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 @@ class Snapshot(PrettyErrorHandler, cyclo # 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 @@ def newestImage(subject): 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 @@ if __name__ == "__main__": 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() diff --git a/bin/tkdnd_minimal_drop.py b/bin/tkdnd_minimal_drop.py --- a/bin/tkdnd_minimal_drop.py +++ b/bin/tkdnd_minimal_drop.py @@ -13,8 +13,7 @@ label.config(text="drop target %s" % lab 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 @@ tk.Label(frame1, text="not a target").pa 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() diff --git a/bin/tracker b/bin/tracker --- a/bin/tracker +++ b/bin/tracker @@ -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,"", - lambda ev: canvas.itemconfig(obj,**{attribute:highlightval})) - canvas.tag_bind(obj,"", - lambda ev: canvas.itemconfig(obj,**{attribute:normalval})) + canvas.tag_bind( + obj, "", lambda ev: canvas.itemconfig( + obj, **{attribute: highlightval})) + canvas.tag_bind( + obj, + "", 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,"",press) - c.tag_bind(self.txt,"",motion) - c.tag_bind(self.txt,"",release) + dispatcher.send("field coord changed") # updates bounds + + c.tag_bind(self.txt, "", press) + c.tag_bind(self.txt, "", motion) + c.tag_bind(self.txt, "", 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,"",motion) - c.tag_bind(outerring,"",release) # from above + + c.tag_bind(outerring, "", motion) + c.tag_bind(outerring, "", 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("",self.configcoords) - - c.bind("", - lambda ev: self._fieldset().report(*c.canvas2world(ev.x,ev.y))) + c.bind("", self.configcoords) + + c.bind("", lambda ev: self._fieldset().report(*c.canvas2world( + ev.x, ev.y))) + def save(ev): print "saving" self.fieldsetfile.save() - master.bind("",save) - dispatcher.connect(self.autobounds,"field coord changed") + + master.bind("", 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") diff --git a/bin/vidref b/bin/vidref --- a/bin/vidref +++ b/bin/vidref @@ -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.main import Gui 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 @@ class Snapshot(cyclone.web.RequestHandle 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 @@ class Snapshot(cyclone.web.RequestHandle 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 @@ graph = SyncedGraph(networking.rdfdb.url 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() - diff --git a/bin/vidrefsetup b/bin/vidrefsetup --- a/bin/vidrefsetup +++ b/bin/vidrefsetup @@ -16,17 +16,23 @@ from light9 import networking, showconfi 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 @@ class VidrefCamRequest(PrettyErrorHandle 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 @@ class VidrefCamRequest(PrettyErrorHandle 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 @@ def main(): 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() diff --git a/bin/wavecurve b/bin/wavecurve --- a/bin/wavecurve +++ b/bin/wavecurve @@ -3,24 +3,30 @@ import optparse 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 @@ if options.all: 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) diff --git a/bin/webcontrol b/bin/webcontrol --- a/bin/webcontrol +++ b/bin/webcontrol @@ -21,14 +21,14 @@ from light9 import showconfig, networkin 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 @@ class Commands(object): @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 @@ class Main(rend.Page): 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 @@ class Main(rend.Page): 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 @@ class Main(rend.Page): def child_icon(self, ctx): return static.File("/usr/share/pyshared/elisa/plugins/poblesec/tango") - + + graph = showconfig.getGraph() show = showconfig.showUri() diff --git a/light9/Effects.py b/light9/Effects.py --- a/light9/Effects.py +++ b/light9/Effects.py @@ -11,21 +11,24 @@ from light9.namespaces import L9 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 '' % (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 @@ class Strip(object): 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 @@ def chase(t, ontime=0.5, offset=0.2, onv 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 @@ def stack(t, names=None, fade=0): 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 @@ def configExprGlobals(): 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 @@ def configExprGlobals(): ret['noise2'] = smooth_random2 ret['notch2'] = notch_random2 - - - return ret diff --git a/light9/Fadable.py b/light9/Fadable.py --- a/light9/Fadable.py +++ b/light9/Fadable.py @@ -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 @@ class Fadable: 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("" % k, - lambda evt, k=k: self.fade(k / 10.0)) + self.bind("" % k, lambda evt, k=k: self.fade(k / 10.0)) self.bind("", lambda evt: self.fade(1.0)) self.bind("", lambda evt: self.fade(0)) @@ -61,7 +66,7 @@ class Fadable: self.bind('', lambda evt: self.increase(length=1)) self.bind('', 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 @@ class Fadable: # 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 @@ class Fadable: 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 @@ class Fadable: 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 @@ class Fadable: 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 @@ class Fadable: 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 @@ class Fadable: self.last_level = None self.set_var_rounded(newlevel) - diff --git a/light9/FlyingFader.py b/light9/FlyingFader.py --- a/light9/FlyingFader.py +++ b/light9/FlyingFader.py @@ -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)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 @@ class FlyingFader(Frame): self.label.pack(side=side2, expand=0, fill=X) for k in range(1, 10): - self.scale.bind("" % k, - lambda evt, k=k: self.newfade(k / 10.0, evt)) + self.scale.bind( + "" % k, lambda evt, k=k: self.newfade(k / 10.0, evt)) self.scale.bind("", lambda evt: self.newfade(1.0, evt)) self.scale.bind("", lambda evt: self.newfade(0, evt)) @@ -100,10 +127,10 @@ class FlyingFader(Frame): self.scale.bind("<3>", self.mousefade) self.trace_ret = self.variable.trace('w', self.updatelabel) - self.bind("",self.ondestroy) + self.bind("", 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 @@ class FlyingFader(Frame): 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 @@ class FlyingFader(Frame): 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 @@ class FlyingFader(Frame): 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 @@ class FlyingFader(Frame): 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() diff --git a/light9/Patch.py b/light9/Patch.py --- a/light9/Patch.py +++ b/light9/Patch.py @@ -8,12 +8,14 @@ def resolve_name(channelname): "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 @@ def get_dmx_channel(name): 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 @@ def get_channel_name(dmxnum): 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 @@ def reload_data(): else: reverse_patch[name] = norm_name + # importing patch will load initial data reload_data() - diff --git a/light9/ascoltami/player.py b/light9/ascoltami/player.py --- a/light9/ascoltami/player.py +++ b/light9/ascoltami/player.py @@ -1,5 +1,4 @@ #!/usr/bin/python - """ alternate to the mpd music player, for ascoltami """ @@ -8,10 +7,11 @@ import time, logging, traceback 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 @@ class Player(object): 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 @@ class Player(object): 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 @@ class Player(object): (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 @@ class Player(object): (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 @@ class Player(object): """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 @@ class Player(object): """ 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) diff --git a/light9/ascoltami/playlist.py b/light9/ascoltami/playlist.py --- a/light9/ascoltami/playlist.py +++ b/light9/ascoltami/playlist.py @@ -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 @@ class Playlist(object): 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") diff --git a/light9/ascoltami/webapp.py b/light9/ascoltami/webapp.py --- a/light9/ascoltami/webapp.py +++ b/light9/ascoltami/webapp.py @@ -9,32 +9,40 @@ render = render_genshi([sibpath(__file__ 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 @@ class timeResource(PrettyErrorHandler,cy 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 @@ class timeResource(PrettyErrorHandler,cy 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 @@ class seekPlayOrPause(PrettyErrorHandler 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 @@ class goButton(PrettyErrorHandler, cyclo 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 @@ def makeWebApp(app): (r"/seekPlayOrPause", seekPlayOrPause), (r"/output", output), (r"/go", goButton), - ], app=app) - + ], + app=app) diff --git a/light9/chase.py b/light9/chase.py --- a/light9/chase.py +++ b/light9/chase.py @@ -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 @@ def chase(t, ontime=0.5, offset=0.2, onv 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() diff --git a/light9/clientsession.py b/light9/clientsession.py --- a/light9/clientsession.py +++ b/light9/clientsession.py @@ -6,12 +6,15 @@ from rdflib import URIRef 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=''))) diff --git a/light9/collector/collector.py b/light9/collector/collector.py --- a/light9/collector/collector.py +++ b/light9/collector/collector.py @@ -16,6 +16,7 @@ ClientSessionType = TypeVar('ClientSessi 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 @@ def outputMap(graph, outputs): 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 @@ class Collector(Generic[ClientType, Clie 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 @@ class Collector(Generic[ClientType, Clie # 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 @@ class Collector(Generic[ClientType, Clie 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 @@ class Collector(Generic[ClientType, Clie 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 @@ class Collector(Generic[ClientType, Clie 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 @@ class Collector(Generic[ClientType, Clie 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 @@ class Collector(Generic[ClientType, Clie 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 @@ class Collector(Generic[ClientType, Clie 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)] diff --git a/light9/collector/collector_client.py b/light9/collector/collector_client.py --- a/light9/collector/collector_client.py +++ b/light9/collector/collector_client.py @@ -3,38 +3,43 @@ from light9 import networking 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 @@ def sendToCollector(client, session, set 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 diff --git a/light9/collector/collector_test.py b/light9/collector/collector_test.py --- a/light9/collector/collector_test.py +++ b/light9/collector/collector_test.py @@ -35,9 +35,11 @@ THEATER = ''' ''' -t0 = 0 # time +t0 = 0 # time + class MockOutput(object): + def __init__(self, uri, connections): self.connections = connections self.updates = [] @@ -53,31 +55,41 @@ class MockOutput(object): 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 TestOutputMap(unittest.TestCase): class TestCollector(unittest.TestCase): + def setUp(self): self.config = MockSyncedGraph(PREFIX + THEATER + ''' @@ -101,10 +114,8 @@ class TestCollector(unittest.TestCase): ''') 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 @@ class TestCollector(unittest.TestCase): 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 @@ class TestCollector(unittest.TestCase): 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 @@ class TestCollector(unittest.TestCase): 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 @@ class TestCollector(unittest.TestCase): 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 @@ class TestCollector(unittest.TestCase): (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) diff --git a/light9/collector/device.py b/light9/collector/device.py --- a/light9/collector/device.py +++ b/light9/collector/device.py @@ -19,7 +19,8 @@ class ChauvetColorStrip(Device): device attrs: color """ - + + class Mini15(Device): """ plan: @@ -31,14 +32,18 @@ class Mini15(Device): 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 @@ def resolve(deviceType, deviceAttr, valu 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 @@ def toOutputAttrs(deviceType, deviceAttr :outputAttrRange happens before we get here. """ + def floatAttr(attr, default=0): out = deviceAttrSettings.get(attr) if out is None: @@ -86,12 +93,10 @@ def toOutputAttrs(deviceType, deviceAttr 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 @@ def toOutputAttrs(deviceType, deviceAttr 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 @@ def toOutputAttrs(deviceType, deviceAttr 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 @@ def toOutputAttrs(deviceType, deviceAttr 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 @@ def toOutputAttrs(deviceType, deviceAttr 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 @@ def toOutputAttrs(deviceType, deviceAttr 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 @@ def toOutputAttrs(deviceType, deviceAttr 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) diff --git a/light9/collector/device_test.py b/light9/collector/device_test.py --- a/light9/collector/device_test.py +++ b/light9/collector/device_test.py @@ -4,50 +4,67 @@ from light9.namespaces import L9 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'])) diff --git a/light9/collector/output.py b/light9/collector/output.py --- a/light9/collector/output.py +++ b/light9/collector/output.py @@ -8,6 +8,7 @@ from twisted.internet import task, threa 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 @@ def setListElem(outList, index, value, f 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 @@ class Output(object): """ 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 @@ class Output(object): """ raise NotImplementedError - def update(self, values): """ output takes a flattened list of values, maybe dmx channels, or @@ -53,11 +55,13 @@ class Output(object): """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 @@ class DummyOutput(Output): def shortId(self): return 'null' - + class DmxOutput(Output): + def __init__(self, uri, numChannels): self.uri = uri self.numChannels = numChannels @@ -85,16 +90,14 @@ class DmxOutput(Output): 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 @@ class EnttecDmx(DmxOutput): 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 @@ class Udmx(DmxOutput): 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 - diff --git a/light9/collector/output_test.py b/light9/collector/output_test.py --- a/light9/collector/output_test.py +++ b/light9/collector/output_test.py @@ -2,38 +2,47 @@ import unittest 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() - diff --git a/light9/curvecalc/client.py b/light9/curvecalc/client.py --- a/light9/curvecalc/client.py +++ b/light9/curvecalc/client.py @@ -6,16 +6,19 @@ from light9 import networking 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 diff --git a/light9/curvecalc/cursors.py b/light9/curvecalc/cursors.py --- a/light9/curvecalc/cursors.py +++ b/light9/curvecalc/cursors.py @@ -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 @@ def pop(widget): log.debug("cursor pop from empty stack") return widget.config(cursor=c) - - diff --git a/light9/curvecalc/curve.py b/light9/curvecalc/curve.py --- a/light9/curvecalc/curve.py +++ b/light9/curvecalc/curve.py @@ -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 @@ log = logging.getLogger() 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 @@ class Curve(object): 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 @@ class Curve(object): 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 @@ class Curve(object): 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 @@ class Curve(object): 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 @@ class Curve(object): # 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 @@ class Curve(object): 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 @@ class Curve(object): 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 @@ class CurveResource(object): 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 @@ class CurveResource(object): 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 @@ class CurveResource(object): #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 @@ class CurveResource(object): # 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 @@ class Curveset(object): 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 @@ class Curveset(object): 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 @@ class Curveset(object): 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 @@ class Curveset(object): # 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 @@ class Curveset(object): 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() - diff --git a/light9/curvecalc/curveedit.py b/light9/curvecalc/curveedit.py --- a/light9/curvecalc/curveedit.py +++ b/light9/curvecalc/curveedit.py @@ -9,40 +9,46 @@ from lib.cycloneerr import PrettyErrorHa 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) - diff --git a/light9/curvecalc/curveview.py b/light9/curvecalc/curveview.py --- a/light9/curvecalc/curveview.py +++ b/light9/curvecalc/curveview.py @@ -13,44 +13,48 @@ from lib.goocanvas_compat import Points, 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 @@ class Sketch: 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 @@ class SelectManip(object): 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 @@ class SelectManip(object): 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 @@ class SelectManip(object): if param == 'centerScale': self.maxPointMove = min(moveLeft, moveRight) - + self.dragRange = (self.dragStartTime - moveLeft, self.dragStartTime + moveRight) return True @@ -165,51 +179,47 @@ class SelectManip(object): 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 @@ class SelectManip(object): 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 @@ class SelectManip(object): 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 @@ class SelectManip(object): 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 @@ class Curveview(object): 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 @@ class Curveview(object): 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("",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("",curs) # self.bind("",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 @@ class Curveview(object): 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 @@ class Curveview(object): # 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 @@ class Curveview(object): 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 @@ class Curveview(object): 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 @@ class Curveview(object): 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 @@ class Curveview(object): 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 @@ class Curveview(object): 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 @@ class Curveview(object): 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 @@ class Curveview(object): 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 @@ class Curveview(object): # 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 @@ class Curveview(object): 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 @@ class Curveview(object): 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 @@ class Curveview(object): 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 @@ class Curveview(object): 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 @@ class Curveview(object): 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 @@ class Curveview(object): 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,"", # lambda ev,i=i: self.dotpress(ev,i)) #self.tag_bind('handle%d' % i, "", # 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 @@ class Curveview(object): 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 @@ class Curveview(object): 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 @@ class Curveview(object): 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 @@ class Curveview(object): 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 @@ class Curveview(object): 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= 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 @@ class CurveRow(object): please pack self.box """ + def __init__(self, graph, name, curve, markers, zoomControl): self.graph = graph self.name = name @@ -1081,23 +1112,24 @@ class CurveRow(object): 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 @@ class CurveRow(object): 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 @@ class CurveRow(object): 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 @@ class Curvesetview(object): """ """ + def __init__(self, graph, curvesVBox, zoomControlBox, curveset): self.graph = graph self.live = True @@ -1184,13 +1221,13 @@ class Curvesetview(object): 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 @@ class Curvesetview(object): 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 @@ class Curvesetview(object): 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 @@ class Curvesetview(object): 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 @@ class Curvesetview(object): 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 @@ class Curvesetview(object): 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 @@ class Curvesetview(object): f.curveView.goLive() def watchCurveAreaHeight(self): + def sizeEvent(w, size): # this is firing really often if self.visibleHeight == size.height: @@ -1285,7 +1323,7 @@ class Curvesetview(object): 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 @@ class Curvesetview(object): 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 @@ class Curvesetview(object): 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() - - - diff --git a/light9/curvecalc/musicaccess.py b/light9/curvecalc/musicaccess.py --- a/light9/curvecalc/musicaccess.py +++ b/light9/curvecalc/musicaccess.py @@ -5,14 +5,16 @@ from light9 import networking 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 @@ class GatherJson(Protocol): 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 @@ class StringProducer(object): 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 @@ class Music: 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 @@ class Music: else: self.player.request("POST", networking.musicPlayer.path("seekPlayOrPause"), - bodyProducer=StringProducer(json.dumps({"t" : t}))) + bodyProducer=StringProducer(json.dumps({"t": + t}))) diff --git a/light9/curvecalc/output.py b/light9/curvecalc/output.py --- a/light9/curvecalc/output.py +++ b/light9/curvecalc/output.py @@ -7,33 +7,35 @@ from light9.curvecalc.subterm import Sub 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 @@ class Output(object): 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 diff --git a/light9/curvecalc/subterm.py b/light9/curvecalc/subterm.py --- a/light9/curvecalc/subterm.py +++ b/light9/curvecalc/subterm.py @@ -8,24 +8,26 @@ from rdfdb.patch import Patch 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 @@ class Expr(object): 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 @@ class Subterm(object): 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 @@ class Subterm(object): # 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 @@ class Subterm(object): 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 @@ class Subterm(object): 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: diff --git a/light9/curvecalc/subtermview.py b/light9/curvecalc/subtermview.py --- a/light9/curvecalc/subtermview.py +++ b/light9/curvecalc/subtermview.py @@ -9,7 +9,9 @@ log = logging.getLogger() # 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 @@ class Subexprview(object): 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 @@ class Subexprview(object): 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 @@ class Subtermview(object): 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 @@ class Subtermview(object): 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 @@ def scrollToRowUponAdd(widgetInRow): log.info("scroll %s", adj.props.value) adj.props.value = adj.props.upper widgetInRow.disconnect(handler) - + handler = widgetInRow.connect('expose-event', firstExpose, adj, widgetInRow) diff --git a/light9/curvecalc/zoomcontrol.py b/light9/curvecalc/zoomcontrol.py --- a/light9/curvecalc/zoomcontrol.py +++ b/light9/curvecalc/zoomcontrol.py @@ -3,10 +3,11 @@ from gi.repository import Gtk 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 @@ class ZoomControl(object): 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 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 @@ class RegionZoom: 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("" % 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("", 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 @@ class RegionZoom: # 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 @@ class RegionZoom: # 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 @@ class RegionZoom: 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 diff --git a/light9/dmxchanedit.py b/light9/dmxchanedit.py --- a/light9/dmxchanedit.py +++ b/light9/dmxchanedit.py @@ -16,7 +16,7 @@ proposal for new attribute system: - 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 @@ from light9.namespaces import L9 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 @@ class Onelevel(tk.Frame): 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 (('',b1down), - ('',b1motion), - ('',b1up), - ('', b2down), - ('', b3up), - ('', b3down)): + for e, func in (('', + b1down), ('', + b1motion), ('', b1up), + ('', + b2down), ('', + b3up), ('', 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 @@ class Onelevel(tk.Frame): """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 @@ class Levelbox(tk.Frame): """ 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 @@ class Levelbox(tk.Frame): 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 @@ class Levelbox(tk.Frame): if self.currentSub() is None: raise ValueError("no currentSub in Levelbox") self.currentSub().editLevel(chan, newLevel) - diff --git a/light9/dmxclient.py b/light9/dmxclient.py --- a/light9/dmxclient.py +++ b/light9/dmxclient.py @@ -10,25 +10,34 @@ from txzmq import ZmqEndpoint, ZmqFactor 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 @@ def outputlevels(levellist,twisted=0,cli 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 diff --git a/light9/editchoice.py b/light9/editchoice.py --- a/light9/editchoice.py +++ b/light9/editchoice.py @@ -2,11 +2,13 @@ import Tkinter as tk 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 @@ class EditChoice(object): - 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 @@ class EditChoice(object): 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 @@ class EditChoice(object): # 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 @@ class EditChoice(object): 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 @@ class EditChoice(object): def switchToLocalSub(self): self.resourceObservable(Local) - diff --git a/light9/editchoicegtk.py b/light9/editchoicegtk.py --- a/light9/editchoicegtk.py +++ b/light9/editchoicegtk.py @@ -4,15 +4,18 @@ from gi.repository import Gdk 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 @@ class EditChoice(Gtk.HBox): # 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 @@ class EditChoice(Gtk.HBox): 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 @@ class EditChoice(Gtk.HBox): 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 diff --git a/light9/effect/edit.py b/light9/effect/edit.py --- a/light9/effect/edit.py +++ b/light9/effect/edit.py @@ -8,22 +8,25 @@ from light9.namespaces import L9, RDF, R 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 @@ def songEffectPatch(graph, dropped, song (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 @@ def songEffectPatch(graph, dropped, song print qq returnValue(Patch(addQuads=quads)) + @inlineCallbacks def songNotePatch(graph, dropped, song, event, ctx, note=None): """ @@ -74,8 +78,7 @@ def songNotePatch(graph, dropped, song, 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 @@ def songNotePatch(graph, dropped, song, 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 songNotePatch(graph, dropped, song, 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 @@ def _finishCurve(graph, note, quads, ctx 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 @@ def _makeNote(graph, song, note, quads, (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 @@ def _newEffect(graph, song, ctx): ] 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 diff --git a/light9/effect/effecteval.py b/light9/effect/effecteval.py --- a/light9/effect/effecteval.py +++ b/light9/effect/effecteval.py @@ -17,28 +17,48 @@ print "reload effecteval" 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 @@ class EffectEval(object): 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 @@ class EffectEval(object): 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 @@ def effect_auraSparkles(effectSettings, 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 @@ def effect_qpan(effectSettings, strength (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 @@ def effect_qsweep(effectSettings, streng 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 @@ def effect_qsweepusa(effectSettings, str 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 @@ def effect_chase1(effectSettings, streng 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 @@ def effect_chase2(effectSettings, streng 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 @@ def effect_Strobe(effectSettings, streng 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): diff --git a/light9/effect/scale.py b/light9/effect/scale.py --- a/light9/effect/scale.py +++ b/light9/effect/scale.py @@ -10,16 +10,16 @@ def scale(value, strength): 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 @@ def scale(value, strength): return value * strength raise NotImplementedError("%r,%r" % (value, strength)) - diff --git a/light9/effect/sequencer.py b/light9/effect/sequencer.py --- a/light9/effect/sequencer.py +++ b/light9/effect/sequencer.py @@ -22,15 +22,17 @@ from light9.effect.simple_outputs import 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 @@ class Note(object): 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 @@ class Note(object): 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 @@ class Note(object): 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 Note(object): 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 @@ class Sequencer(object): 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 @@ class Sequencer(object): 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 @@ class Sequencer(object): # 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 @@ class Sequencer(object): 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 @@ class Updates(cyclone.sse.SSEHandler): return self.sendEvent(self.state) reactor.callLater(.1, self.loop) - + def unbind(self): self.numConnected -= 1 - - diff --git a/light9/effect/settings.py b/light9/effect/settings.py --- a/light9/effect/settings.py +++ b/light9/effect/settings.py @@ -13,15 +13,20 @@ import logging 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 @@ def getVal(graph, subj): 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 @@ class _Settings(object): 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 @@ class _Settings(object): 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 @@ class _Settings(object): 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 @@ class _Settings(object): 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 @@ class _Settings(object): 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 @@ class _Settings(object): def devices(self): return self._compiled.keys() - + def toVector(self, deviceAttrFilter=None): out = [] for dev, attr in self._vectorKeys(deviceAttrFilter): @@ -192,7 +204,7 @@ class _Settings(object): 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 @@ class _Settings(object): 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 @@ class DeviceSettings(_Settings): continue keys.append(key) return keys - diff --git a/light9/effect/settings_test.py b/light9/effect/settings_test.py --- a/light9/effect/settings_test.py +++ b/light9/effect/settings_test.py @@ -4,12 +4,13 @@ from rdfdb.patch import Patch 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 @@ class TestDeviceSettings(unittest.TestCa 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 @@ class TestDeviceSettings(unittest.TestCa 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 @@ class TestDeviceSettings(unittest.TestCa (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( diff --git a/light9/effect/simple_outputs.py b/light9/effect/simple_outputs.py --- a/light9/effect/simple_outputs.py +++ b/light9/effect/simple_outputs.py @@ -3,15 +3,17 @@ import traceback 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 @@ class SimpleOutputs(object): 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 @@ class SimpleOutputs(object): value = scale(value, colorScale) out[(dev, devAttr)] = value return out - diff --git a/light9/effecteval/effect.py b/light9/effecteval/effect.py --- a/light9/effecteval/effect.py +++ b/light9/effecteval/effect.py @@ -5,16 +5,19 @@ from light9.namespaces import L9, RDF 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 @@ class CodeLine(object): 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 @@ class CodeLine(object): 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 @@ class CodeLine(object): 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 @@ class CodeLine(object): 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 @@ class EffectNode(object): 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 @@ class EffectNode(object): 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 @@ class EffectNode(object): if 'out' not in ns: log.error("ran code for %s, didn't make an 'out' value", self.uri) return ns['out'] - diff --git a/light9/effecteval/effectloop.py b/light9/effecteval/effectloop.py --- a/light9/effecteval/effectloop.py +++ b/light9/effecteval/effectloop.py @@ -16,23 +16,26 @@ from light9 import dmxclient 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 @@ class EffectLoop(object): 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 @@ class EffectLoop(object): 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 @@ class EffectLoop(object): 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 @@ class EffectLoop(object): @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 @@ class EffectLoop(object): 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 @@ class EffectLoop(object): 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 @@ class EffectLoop(object): 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 @@ class ControlBoard(object): 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 @@ class ControlBoard(object): 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 @@ class LedLoop(EffectLoop): 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 @@ class LedLoop(EffectLoop): 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 @@ def makeEffectLoop(graph, stats, outputW return LedLoop(graph, stats) else: raise NotImplementedError("unknown output system %r" % outputWhere) - - diff --git a/light9/effecteval/test_effect.py b/light9/effecteval/test_effect.py --- a/light9/effecteval/test_effect.py +++ b/light9/effecteval/test_effect.py @@ -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 @@ class TestAsPython(unittest.TestCase): 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 = ') _, inExpr, expr, uris = ec._asPython() @@ -32,18 +37,22 @@ class TestAsPython(unittest.TestCase): self.assertEqual({'_res0': URIRef('http://example.com/')}, uris) def test_converts_multiple_uris(self): - ec = CodeLine(graph=None, code='x = + ') + ec = CodeLine(graph=None, + code='x = + ') _, 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 = ') _, inExpr, expr, uris = ec._asPython() @@ -51,19 +60,24 @@ class TestAsPython(unittest.TestCase): self.assertEqual({'_res0': URIRef('http://example/curve1')}, uris) def test_curve_doesnt_double_wrap(self): - ec = CodeLine(graph=None, code='x = curve(, t+.01)') + ec = CodeLine(graph=None, + code='x = curve(, 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) diff --git a/light9/greplin_cyclone.py b/light9/greplin_cyclone.py --- a/light9/greplin_cyclone.py +++ b/light9/greplin_cyclone.py @@ -3,26 +3,34 @@ import cyclone.web, cyclone.websocket, c 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) diff --git a/light9/gtkpyconsole.py b/light9/gtkpyconsole.py --- a/light9/gtkpyconsole.py +++ b/light9/gtkpyconsole.py @@ -3,6 +3,7 @@ import gi 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 @@ def togglePyConsole(self, item, user_ns) 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() - diff --git a/light9/io/__init__.py b/light9/io/__init__.py --- a/light9/io/__init__.py +++ b/light9/io/__init__.py @@ -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 @@ class BaseIO(object): # 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 @@ class ParportDMX(BaseIO): 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 @@ class UsbDMX(BaseIO): 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) - diff --git a/light9/io/udmx.py b/light9/io/udmx.py --- a/light9/io/udmx.py +++ b/light9/io/udmx.py @@ -4,7 +4,6 @@ import usb.core 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 @@ or https://github.com/markusb/uDMX-linux 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 @@ def demo(chan, fps=44): 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) diff --git a/light9/namespaces.py b/light9/namespaces.py --- a/light9/namespaces.py +++ b/light9/namespaces.py @@ -3,15 +3,19 @@ from rdflib import Namespace, RDF, RDFS # 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#") diff --git a/light9/networking.py b/light9/networking.py --- a/light9/networking.py +++ b/light9/networking.py @@ -3,7 +3,9 @@ from urllib import splitport from showconfig import getGraph, showUri from namespaces import L9 + class ServiceAddress(object): + def __init__(self, service): self.service = service @@ -12,8 +14,8 @@ class ServiceAddress(object): 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 @@ class ServiceAddress(object): @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']) diff --git a/light9/observable.py b/light9/observable.py --- a/light9/observable.py +++ b/light9/observable.py @@ -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 @@ class Observable(object): 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() diff --git a/light9/paint/capture.py b/light9/paint/capture.py --- a/light9/paint/capture.py +++ b/light9/paint/capture.py @@ -5,37 +5,46 @@ from rdfdb.patch import Patch 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""" - diff --git a/light9/paint/solve.py b/light9/paint/solve.py --- a/light9/paint/solve.py +++ b/light9/paint/solve.py @@ -12,43 +12,54 @@ log = logging.getLogger('solve') # 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 @@ class ImageDist(object): 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 @@ class Solver(object): 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 @@ class Solver(object): 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 @@ class Solver(object): 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 @@ class Solver(object): 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 @@ class Solver(object): # 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 @@ class Solver(object): # 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 @@ class Solver(object): 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 @@ class Solver(object): 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 @@ class Solver(object): 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 @@ class Solver(object): # 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 diff --git a/light9/paint/solve_test.py b/light9/paint/solve_test.py --- a/light9/paint/solve_test.py +++ b/light9/paint/solve_test.py @@ -6,11 +6,15 @@ from light9.namespaces import RDF, L9, D 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 @@ class TestSolve(unittest.TestCase): @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) diff --git a/light9/prof.py b/light9/prof.py --- a/light9/prof.py +++ b/light9/prof.py @@ -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 @@ def run(main, profile=None): 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 @@ def watchPoint(filename, lineno, event=" 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 @@ def watchPoint(filename, lineno, event=" 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 diff --git a/light9/subcomposer/subcomposerweb.py b/light9/subcomposer/subcomposerweb.py --- a/light9/subcomposer/subcomposerweb.py +++ b/light9/subcomposer/subcomposerweb.py @@ -6,21 +6,29 @@ from rdflib import URIRef, Literal 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) diff --git a/light9/vidref/main.py b/light9/vidref/main.py --- a/light9/vidref/main.py +++ b/light9/vidref/main.py @@ -1,5 +1,4 @@ #!/usr/bin/python - """ dvcam test @@ -15,7 +14,9 @@ from light9.vidref.videorecorder import 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 @@ class Gui(object): 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 @@ class Gui(object): 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 @@ class Gui(object): 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() diff --git a/light9/vidref/musictime.py b/light9/vidref/musictime.py --- a/light9/vidref/musictime.py +++ b/light9/vidref/musictime.py @@ -6,12 +6,17 @@ from restkit.errors import ResourceNotFo 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 @@ class MusicTime(object): 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 @@ class MusicTime(object): 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 @@ class MusicTime(object): return pos def pollMusicTime(self): + def cb(response): if response.code != 200: @@ -71,14 +77,14 @@ class MusicTime(object): 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 @@ class MusicTime(object): self.lastHoverTime = None reactor.callLater(.2, self.pollCurvecalcTime) return - + def cb(response): if response.code == 404: # not hovering @@ -115,9 +121,10 @@ class MusicTime(object): 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"}) diff --git a/light9/vidref/qt_test.py b/light9/vidref/qt_test.py --- a/light9/vidref/qt_test.py +++ b/light9/vidref/qt_test.py @@ -4,7 +4,9 @@ from PyQt4 import QtCore, QtGui, uic 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 @@ class Vid(object): 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 @@ class Vid(object): 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 @@ class Vid(object): 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 @@ class MainWin(QtGui.QMainWindow): @QtCore.pyqtSlot() def startLiveView(self): print "slv" - - diff --git a/light9/vidref/remotepivideo.py b/light9/vidref/remotepivideo.py --- a/light9/vidref/remotepivideo.py +++ b/light9/vidref/remotepivideo.py @@ -13,13 +13,15 @@ from PIL import Image 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 @@ class Pipeline(object): 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 @@ class Pipeline(object): #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 @@ class Pipeline(object): 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 @@ class Pipeline(object): 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 @@ class Pipeline(object): 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 @@ class Pipeline(object): 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: diff --git a/light9/vidref/replay.py b/light9/vidref/replay.py --- a/light9/vidref/replay.py +++ b/light9/vidref/replay.py @@ -6,31 +6,36 @@ log = logging.getLogger() 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 @@ class ReplayViews(object): 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 @@ class ReplayViews(object): 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 @@ class ReplayViews(object): 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 @@ class ReplayView(object): 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 @@ class ReplayView(object): 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 @@ class ReplayView(object): 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 @@ class ReplayView(object): 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 @@ class Replay(object): 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]) diff --git a/light9/vidref/videorecorder.py b/light9/vidref/videorecorder.py --- a/light9/vidref/videorecorder.py +++ b/light9/vidref/videorecorder.py @@ -9,7 +9,9 @@ from Queue import Queue, Empty 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 @@ class Pipeline(object): 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 @@ class Pipeline(object): 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 @@ class Pipeline(object): 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 @@ class Pipeline(object): 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 @@ class VideoRecordSink(gst.Element): 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 @@ class VideoRecordSink(gst.Element): else: req(args[1]) self.snapshotRequests.task_done() - + t = Thread(target=imageSaver) t.setDaemon(True) t.start() @@ -159,14 +165,14 @@ class VideoRecordSink(gst.Element): 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 @@ class VideoRecordSink(gst.Element): 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)