Changeset - 7772cc48e016
bin/ascoltami2
Show inline comments
 
#!bin/python
 
from run_local import log
 
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')
 
gi.require_version('Gtk', '3.0')
 

	
 
from light9.ascoltami.player import Player
 
from light9.ascoltami.playlist import Playlist, NoSuchSong
 
from light9.ascoltami.webapp import makeWebApp, songUri, songLocation
 
from light9 import networking, showconfig
 

	
 
from gi.repository import GObject, Gst, Gtk
 

	
 

	
 
class App(object):
 

	
 
    def __init__(self, graph, show):
 
        self.graph = graph
 
        self.player = Player(onEOS=self.onEOS)
 
        self.show = show
 
        self.playlist = Playlist.fromShow(graph, show)
 

	
 
@@ -29,35 +31,41 @@ class App(object):
 
        self.player.seek(0)
 

	
 
        thisSongUri = songUri(graph, URIRef(song))
 

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

	
 
    log.setLevel(logging.DEBUG if options.verbose else logging.INFO)
 

	
 
    if not options.show:
 
        raise ValueError("missing --show http://...")
 
            
 

	
 
    graph = showconfig.getGraph()
 
    app = App(graph, URIRef(options.show))
 
    if options.twistedlog:
 
        from twisted.python import log as twlog
 
        twlog.startLogging(sys.stderr)
 
    reactor.listenTCP(networking.musicPlayer.port, makeWebApp(app))
bin/bcf_puppet_demo
Show inline comments
 
@@ -2,15 +2,16 @@
 
"""
 
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()
bin/bumppad
Show inline comments
 
#!bin/python
 
from __future__ import division,nested_scopes
 
from __future__ import division, nested_scopes
 
import sys, time, math
 
import Tkinter as tk
 

	
 
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("<KeyPress-%s>"%key,
 
                      lambda ev,sub=sub: self.bumpto(sub,1))
 
            root.bind("<KeyRelease-%s>"%key,
 
                      lambda ev,sub=sub: self.bumpto(sub,0))
 
    def bumpto(self,sub,lev):
 
        now=time.time()
 
        self.levs[sub]=lev*self.mag.get()
 
            self.levs[sub] = 0
 

	
 
            l = tk.Label(self,
 
                         font="arial 12 bold",
 
                         anchor='w',
 
                         height=2,
 
                         relief='groove',
 
                         bd=5,
 
                         text="%s\n%s" % (key.replace('KP_', ''), sub.name))
 
            l.grid(column=xy[0], row=xy[1], sticky='news')
 

	
 
            root.bind(
 
                "<KeyPress-%s>" % key, lambda ev, sub=sub: self.bumpto(sub, 1))
 
            root.bind("<KeyRelease-%s>" % key,
 
                      lambda ev, sub=sub: self.bumpto(sub, 0))
 

	
 
    def bumpto(self, sub, lev):
 
        now = time.time()
 
        self.levs[sub] = lev * self.mag.get()
 
        self.master.after_idle(self.output)
 

	
 
    def output(self):
 
        dmx = sub_maxes(*[s*l for s,l in self.levs.items()]).get_dmx_list()
 
        dmxclient.outputlevels(dmx,clientid="bumppad")
 
        
 
root=tk.Tk()
 
        dmx = sub_maxes(*[s * l for s, l in self.levs.items()]).get_dmx_list()
 
        dmxclient.outputlevels(dmx, clientid="bumppad")
 

	
 

	
 
root = tk.Tk()
 
root.tk_setPalette("maroon4")
 
root.wm_title("bumppad")
 
mag = tk.DoubleVar()
 

	
 
tk.Label(root,text="Keypad press/release activate sub; 1..5 set mag",
 
             font="Helvetica -12 italic",anchor='w').pack(side='bottom',fill='x')
 
                      
 
pad(root,root,mag).pack(side='left',fill='both',exp=1)
 
tk.Label(root,
 
         text="Keypad press/release activate sub; 1..5 set mag",
 
         font="Helvetica -12 italic",
 
         anchor='w').pack(side='bottom', fill='x')
 

	
 
pad(root, root, mag).pack(side='left', fill='both', exp=1)
 

	
 
magscl = tk.Scale(root,orient='vertical',from_=1,to=0,res=.01,
 
                   showval=1,variable=mag,label='mag',relief='raised',bd=1)
 
for i in range(1,6):
 
    root.bind("<Key-%s>"%i,lambda ev,i=i: mag.set(math.sqrt((i )/5)))
 
magscl.pack(side='left',fill='y')
 

	
 
magscl = tk.Scale(root,
 
                  orient='vertical',
 
                  from_=1,
 
                  to=0,
 
                  res=.01,
 
                  showval=1,
 
                  variable=mag,
 
                  label='mag',
 
                  relief='raised',
 
                  bd=1)
 
for i in range(1, 6):
 
    root.bind("<Key-%s>" % i, lambda ev, i=i: mag.set(math.sqrt((i) / 5)))
 
magscl.pack(side='left', fill='y')
 

	
 
root.mainloop()
bin/captureDevice
Show inline comments
 
@@ -23,44 +23,50 @@ from light9.greplin_cyclone import Stats
 
from light9.effect.settings import DeviceSettings
 
from light9.collector.collector_client import sendToCollector
 
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()
 
        try:
 
            os.makedirs(os.path.dirname(writePath))
 
        except OSError:
 
            pass
 
        with open(writePath, 'w') as out:
 
            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)]
 

	
 
        startTime = time.time()
 
        self.captureId = 'cap%s' % (int(startTime) - 1495170000)
 
        self.toGather = []
 
@@ -69,107 +75,124 @@ class Capture(object):
 
        rxSteps = steps(.06, .952, 10)
 
        rySteps = steps(0.1, .77, 5)
 
        zoomSteps = steps(.12, .85, 3)
 
        # 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[:]
 
            if row % 2:
 
                xSteps.reverse()
 
            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:
 
            yield self.off()
 
            yield deferSleep(1)
 
            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()
bin/clientdemo
Show inline comments
 
@@ -13,24 +13,27 @@ if __name__ == "__main__":
 
    logging.basicConfig(level=logging.DEBUG)
 
    log = logging.getLogger()
 

	
 
    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
 

	
 
    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()
bin/collector
Show inline comments
 
@@ -26,58 +26,62 @@ from light9.collector.output import Entt
 
from light9.collector.collector import Collector
 
from light9.namespaces import L9
 
from light9 import networking
 
from rdfdb.syncedgraph import SyncedGraph
 
from light9.greplin_cyclone import StatsForCyclone
 

	
 

	
 
def parseJsonMessage(msg):
 
    body = json.loads(msg)
 
    settings = []
 
    for device, attr, value in body['settings']:
 
        if isinstance(value, basestring) and value.startswith('http'):
 
            value = URIRef(value)
 
        else:
 
            value = Literal(value)
 
        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):
 
            with stats.setAttr.time():
 
                # 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"""
 

	
 
        self.pendingMessageForDev[dev] = (attrs, outputMap)
 
        try:
 
            self._flush()
 
@@ -91,14 +95,14 @@ class WebListeners(object):
 
            return
 
        self.lastFlush = now
 

	
 
        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.
 
            for client, seen in self.clients:
 
                if seen.get(dev) == attrs:
 
                    continue
 
@@ -109,99 +113,124 @@ class WebListeners(object):
 
                client.sendMessage(msg)
 

	
 
    def makeMsg(self, dev, attrs, outputMap):
 
        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):
 
        log.info('socket connect %s', self)
 
        self.settings.listeners.addClient(self)
 

	
 
    def connectionLost(self, reason):
 
        self.settings.listeners.delClient(self)
 

	
 
    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),
 
        ]
 
    except Exception:
 
        log.error("setting up outputs:")
 
        traceback.print_exc()
 
        raise
 
    listeners = WebListeners()
 
    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)
 
    if doLoadTest:
 
        # in a subprocess since we don't want this client to be
 
        # cooperating with the main event loop and only sending
 
        # 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()
bin/collector_loadtest.py
Show inline comments
 
@@ -4,43 +4,51 @@ from run_local import log
 
from light9.collector.collector_client import sendToCollector, sendToCollectorZmq
 
from light9.namespaces import L9, DEV
 
from twisted.internet import reactor
 
import time
 
import logging
 
log.setLevel(logging.DEBUG)
 

	
 

	
 
def loadTest():
 
    print "scheduling loadtest"
 
    n = 2500
 
    times = [None] * n
 
    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
 

	
 
    def done():
 
        print "loadtest done"
 
        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()
bin/curvecalc
Show inline comments
 
#!bin/python
 

	
 
"""
 
now launches like this:
 
% bin/curvecalc http://light9.bigasterisk.com/show/dance2007/song1
 

	
 

	
 

	
 
todo: curveview should preserve more objects, for speed maybe
 

	
 
"""
 
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
 

	
 
import time, textwrap, os, optparse, linecache, signal, traceback, json
 
import gi
 
from gi.repository import Gtk
 
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
 
from light9.curvecalc.output import Output
 
from light9.curvecalc.subterm import Subterm
 
from light9.curvecalc.subtermview import add_one_subterm
 
@@ -43,160 +42,176 @@ from light9.namespaces import L9
 
from light9.observable import Observable
 
from light9 import clientsession
 
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)
 
        self.registerCurrentPlayerSongToUi(wtree, graph, songChoice)
 

	
 
        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')
 
            log.debug('songChoiceToGraph is going to set to %r', newSong)
 

	
 
            # I get bogus newSong values in here sometimes. This
 
            # workaround may not even be helping.
 
            now = time.time()
 
            if now < self.muteSongChoiceUntil:
 
                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):
 
        """current_player_song 'song' param -> playerSong ui
 
        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:
 
                    # we're not making sure the expression/etc are
 
                    # 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)
 
        self.makeSubterm(Literal(flattened['subtermName']),
 
                         expr=flattened['subtermExpr'])
 

	
 
@@ -213,27 +228,27 @@ class Main(object):
 
        if dialog.run() == 1:
 
            self.curveset.new_curve(entry.get_text())
 
        dialog.hide()
 

	
 
    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)
 
        entry = self.wtree.get_object("newSubtermName").get_children()[0]
 
        entry.set_text("")
 
        entry.grab_focus()
 
@@ -242,15 +257,15 @@ class Main(object):
 
            wc = self.wtree.get_object("newSubtermMakeCurve").get_active()
 
            self.makeSubterm(newname, withCurve=wc)
 
        dialog.hide()
 

	
 
    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):
 
        return self.currentSong()
 

	
 
    def makeSubterm(self, newname, withCurve=False, expr=None, sub=None):
 
@@ -274,19 +289,19 @@ class Main(object):
 

	
 
        ctx = self.songSubtermsContext()
 
        quads = [
 
            (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):
 
        """
 
        Literal labels of subs in subterms. doesn't currently include labels of the
 
        subterm resources. I'm not sure what I'm going to do with
 
@@ -298,23 +313,23 @@ class Main(object):
 
                    current.value(self.session, L9['currentSong']),
 
                    L9['subterm']):
 
                sub = current.value(st, L9['sub'])
 
                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"""
 
        song = self.graph.value(self.session, L9['currentSong'])
 

	
 
        newList = []
 
        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
 

	
 
        master = self.wtree.get_object("subterms")
 
        log.debug("removing subterm widgets")
 
        [master.remove(c) for c in master.get_children()]
 
@@ -332,19 +347,18 @@ class Main(object):
 
        providers[-1].load_from_path("theme/Just-Dark/gtk-3.0/gtk.css")
 
        providers.append(Gtk.CssProvider())
 
        providers[-1].load_from_data('''
 
          * { 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")
 
        v = subtermsTable.get_parent().props.vadjustment
 
        v.props.value = v.props.upper
 

	
 
@@ -366,13 +380,13 @@ class Main(object):
 

	
 
    def onPythonConsole(self, item):
 
        ns = dict()
 
        ns.update(globals())
 
        ns.update(self.__dict__)
 
        togglePyConsole(self, item, ns)
 
        
 

	
 
    def onSeeCurrentTime(self, item):
 
        dispatcher.send("see time")
 

	
 
    def onSeeTimeUntilEnd(self, item):
 
        dispatcher.send("see time until end")
 

	
 
@@ -392,62 +406,64 @@ class Main(object):
 
        self.curveset.save()
 
        log.info("saved")
 

	
 
    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)
 
            master.attach(key, 0, 1, row, row + 1)
 
            master.attach(value, 1, 2, row, row + 1)
 
            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)
 

	
 
                # old ones are not getting deleted right
 
                if hasattr(self, 'curvesetView'):
 
                    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
 
                # try to minimize the number of times we redraw
 
                # the curve at startup. If tk is very slow, it's
 
                # ok. You'll just get some wasted redraws.
 
@@ -460,12 +476,13 @@ class Main(object):
 

	
 

	
 
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)
 

	
 
    def update(self):
 
        song = self.graph.value(self.session, L9['currentSong'])
 
@@ -477,79 +494,80 @@ class MaxTime(object):
 
        log.info("new max time %r", self.maxtime)
 
        dispatcher.send("max time", maxtime=self.maxtime)
 

	
 
    def get(self):
 
        return self.maxtime
 

	
 

	
 
def launch(args, graph, session, opts, startTime, music):
 

	
 
    try:
 
        song = URIRef(args[0])
 
        graph.patchObject(context=session,
 
                          subject=session,
 
                          predicate=L9['currentSong'],
 
                          newObject=song)
 
    except IndexError:
 
        pass
 

	
 
    curveset = Curveset(graph=graph, session=session)
 
        
 

	
 
    log.debug("startup: output %s", time.time() - startTime)
 

	
 
    mt = MaxTime(graph, session)
 
    dispatcher.connect(lambda: mt.get(), "get max time", weak=False)
 

	
 
    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
 

	
 
    def hoverTimeResponse(requestHandler):
 
        results = dispatcher.send("onPlayPause")
 
        times = [t for listener, t in results if t is not None]
 
        if not times:
 
            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)
 
    opts, args = parser.parse_args()
 

	
 
    log.setLevel(logging.DEBUG if opts.debug else logging.INFO)
 

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

	
bin/dmx_color_test.py
Show inline comments
 
@@ -4,12 +4,13 @@ import colorsys, time, logging
 
from light9 import dmxclient
 
from twisted.internet import reactor, task
 

	
 
log.setLevel(logging.INFO)
 
firstDmxChannel = 10
 

	
 

	
 
def step():
 
    hue = (time.time() * .2) % 1.0
 
    r, g, b = colorsys.hsv_to_rgb(hue, 1, 1)
 
    chans = [r, g, b]
 
    log.info(chans)
 
    dmxclient.outputlevels([0] * (firstDmxChannel - 1) + chans, twisted=True)
bin/dmxserver
Show inline comments
 
@@ -24,195 +24,200 @@ todo:
 
  if parport fails, run in dummy mode (and make an option for that too)
 
"""
 

	
 
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
 
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
 
        self.receiver = txosc.dispatch.Receiver()
 
        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)
 

	
 
    def pixel_handler(self, message, address):
 
        # this is already 1-based though I don't know why
 
        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()
 

	
 
    def sendlevels_dmx(self):
 
        """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"
 

	
 
    def xmlrpc_currentlevels(self, cid):
 
        """get a list of levels we're currently sending out. All
 
        channels beyond the list you get back, they're at zero."""
 
@@ -224,46 +229,53 @@ class XMLRPCServe(xmlrpc.XMLRPC):
 
        if i < 0:
 
            return []
 
        while trunc[i] == 0 and i >= 0:
 
            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()
 

	
bin/effecteval
Show inline comments
 
@@ -5,100 +5,120 @@ from run_local import log
 
from twisted.internet import reactor
 
from twisted.internet.defer import inlineCallbacks, returnValue
 
import cyclone.web, cyclone.websocket, cyclone.httpclient
 
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
 
from light9.effecteval.effectloop import makeEffectLoop
 
from light9.greplin_cyclone import StatsForCyclone
 
from light9.namespaces import L9
 
from rdfdb.patch import Patch
 
from rdfdb.syncedgraph import SyncedGraph
 
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']
 
    if s is None:
 
        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('')
 

	
 
    @inlineCallbacks
 
    def post(self):
 
        self.wideOpenCors()
 
        dropped = URIRef(self.get_argument('drop'))
 

	
 
        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'])
 
        songs = list(self.graph.items(playlist))
 
        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'))
 
        self.sendMessage({'hello': repr(self)})
 

	
 
        self.graph = self.settings.graph
 
@@ -110,36 +130,37 @@ class EffectUpdates(cyclone.websocket.We
 

	
 
        # EffectNode knows how to put them in order. Somehow this is
 
        # not triggering an update when the order changes.
 
        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")
 

	
 
    def messageReceived(self, message):
 
        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:]:
 
        moreAdds.append((s, p, line, c))
 
    fullPatch = Patch(delQuads=patch.delQuads,
 
                      addQuads=patch.addQuads + moreAdds)
 
    graph.patch(fullPatch)
 

	
 
        
 

	
 
class Code(PrettyErrorHandler, cyclone.web.RequestHandler):
 

	
 
    def put(self):
 
        effect = URIRef(self.get_argument('uri'))
 
        codeLines = []
 
        for i in itertools.count(0):
 
            k = 'codeLines[%s][text]' % i
 
            v = self.get_argument(k, None)
 
@@ -147,22 +168,24 @@ class Code(PrettyErrorHandler, cyclone.w
 
                codeLines.append(Literal(v))
 
            else:
 
                break
 
        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
 
        uri = URIRef(self.get_argument('uri'))
 
        response = yield cyclone.httpclient.fetch(
 
            networking.musicPlayer.path('time'))
 
@@ -173,47 +196,58 @@ class EffectEval(PrettyErrorHandler, cyc
 
        self.write(json.dumps(outSub.get_dmx_list()))
 

	
 

	
 
# Completely not sure where the effect background loop should
 
# go. Another process could own it, and get this request repeatedly:
 
class SongEffectsEval(PrettyErrorHandler, cyclone.web.RequestHandler):
 

	
 
    def get(self):
 
        song = URIRef(self.get_argument('song'))
 
        effects = effectsForSong(self.settings.graph, song)
 
        raise NotImplementedError
 
        self.write(maxDict(effectDmxDict(e) for e in effects))
 
        # return dmx dict for all effects in the song, already combined
 

	
 

	
 
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),
 
            (r'/effect/eval', EffectEval),
 
            (r'/songEffects', SongEffects),
 
            (r'/songEffects/eval', SongEffectsEval),
 
@@ -221,38 +255,46 @@ class App(object):
 
        ],
 
                                                  debug=True,
 
                                                  graph=self.graph,
 
                                                  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()
 
    log.setLevel(logging.DEBUG if options.verbose else logging.INFO)
 

	
 
    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
 
        twlog.startLogging(sys.stderr)
 
    reactor.run()
bin/effectsequencer
Show inline comments
 
@@ -14,37 +14,41 @@ import cyclone.web
 
from rdflib import URIRef
 
from light9.effect.sequencer import Sequencer, Updates
 
from light9.collector.collector_client import sendToCollector
 

	
 
from light9 import clientsession
 

	
 

	
 
class App(object):
 

	
 
    def __init__(self, show, session):
 
        self.show = show
 
        self.session = session
 

	
 
        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),
 
        ],
 
                                                  debug=True,
 
                                                  seq=self.seq,
 
                                                  graph=self.graph,
 
@@ -52,26 +56,30 @@ class App(object):
 
        reactor.listenTCP(networking.effectSequencer.port, self.cycloneApp)
 
        log.info("listening on %s" % networking.effectSequencer.port)
 

	
 

	
 
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()
 
    log.setLevel(logging.DEBUG if options.verbose else logging.INFO)
 

	
 
    if not options.show:
 
        raise ValueError("missing --show http://...")
 
        
 

	
 
    session = clientsession.getUri('effectSequencer', options)
 

	
 
    app = App(URIRef(options.show), session)
 
    if options.twistedlog:
 
        from twisted.python import log as twlog
 
        twlog.startLogging(sys.stderr)
bin/homepageConfig
Show inline comments
 
@@ -14,12 +14,13 @@ graph = showconfig.getGraph()
 
netHome = graph.value(showconfig.showUri(), L9['networking'])
 
webServer = graph.value(netHome, L9['webServer'])
 
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/ {
 

	
 
      # for websocket
 
      proxy_http_version 1.1;
 
@@ -29,23 +30,24 @@ def location(path, server):
 

	
 
      proxy_pass %(server)s;
 
      proxy_buffering off;
 
      rewrite /[^/]+/(.*) /$1 break;
 
    }""" % vars()
 

	
 

	
 
for role, server in sorted(graph.predicate_objects(netHome)):
 
    if not server.startswith('http') or role == L9['webServer']:
 
        continue
 
    path = graph.value(role, L9['urlPath'])
 
    if not path:
 
        continue
 
    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)]
 
}
bin/inputdemo
Show inline comments
 
#!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
 
from rdflib import URIRef
 
import optparse, logging, urllib, time
 
from gi.repository import Gtk
 
@@ -11,49 +11,56 @@ from run_local import log
 
from light9 import showconfig, networking
 
from light9 import clientsession
 
from rdfdb.syncedgraph import SyncedGraph
 
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()
 

	
 
        log.setLevel(logging.DEBUG if opts.debug else logging.INFO)
 

	
 
        self.session = clientsession.getUri('inputdemo', opts)
 
        self.graph = SyncedGraph(networking.rdfdb.url, "inputdemo")
 

	
 
        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)
 

	
 
        win.add(slider)
 
        win.parse_geometry('50x250')
 
        win.connect("delete-event", lambda *a: reactor.crash())
 
        win.connect("destroy", lambda *a: reactor.crash())
 
        win.show_all()
 

	
 
    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()
bin/inputquneo
Show inline comments
 
@@ -11,98 +11,104 @@ from twisted.internet import reactor, ta
 
from light9.curvecalc.client import sendLiveInputPoint
 
from light9.namespaces import L9, RDF, RDFS
 
from rdfdb.syncedgraph import SyncedGraph
 
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()
 

	
 
        dev = self.findQuneo()
 
        self.inp = pygame.midi.Input(dev)
 
        task.LoopingCall(self.step).start(.05)
 

	
 
        self.noteIsOn = {}
 

	
 
        self.effectMap = {} # note: effect class uri
 
        self.effectMap = {}  # note: effect class uri
 
        self.graph.addHandler(self.setupNotes)
 

	
 
    def setupNotes(self):
 
        for e in self.graph.subjects(RDF.type, L9['EffectClass']):
 
            qn = self.graph.value(e, L9['quneoNote'])
 
            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
 
        NOTEON, NOTEOFF = 144, 128
 
        for ev in self.inp.read(999):
 
            (status, d1, d2, _), _ = ev
 
            if status in [NOTEON, NOTEOFF]:
 
                print status, d1, d2
 

	
 

	
 
            if status == NOTEON:
 
                if not self.noteIsOn.get(d1):
 
                    self.noteIsOn[d1] = True
 
                    try:
 
                        e = self.effectMap[d1]
 
                        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:
 
                        pass
 

	
 
            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"
 

	
 
                            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()
bin/kcclient
Show inline comments
 
#!/usr/bin/env python
 

	
 
"""send KeyboardComposer a fade request, for use from the shell"""
 

	
 
import sys
 
import run_local
 
from restclient import Resource
 
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)
 

	
 

	
bin/keyboardcomposer
Show inline comments
 
@@ -24,72 +24,96 @@ import light9.effect.effecteval
 
from light9.effect.settings import DeviceSettings
 
from rdfdb.patch import Patch
 
from light9.effect.simple_outputs import SimpleOutputs
 

	
 
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
 
        self.session = session
 
        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)
 

	
 
        for w in [self, self.namelabel, levellabel]:
 
            dragSourceRegister(w, 'copy', 'text/uri-list', sub)
 

	
 
@@ -108,13 +132,15 @@ class SubmasterBox(tk.Frame):
 

	
 
        if self.pauseTrace:
 
            return
 
        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):
 
        """in our per-session graph, we maintain SubSetting objects like this:
 

	
 
           ?session :subSetting [a :SubSetting; :sub ?s; :level ?l]
 
@@ -122,80 +148,95 @@ class SubmasterBox(tk.Frame):
 
        # move to syncedgraph patchMapping
 

	
 
        self.graph.patchMapping(context=self.session,
 
                                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"""
 
        # move this to syncedgraph readMapping
 
        graph = self.graph
 

	
 
        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)
 

	
 
        self.make_key_hints()
 
        self.make_buttons()
 

	
 
        self.graph.addHandler(self.redraw_sliders)
 

	
 
        self.codeWatcher = CodeWatcher(
 
            onChange=lambda: self.graph.addHandler(self.redraw_sliders))
 
        
 

	
 
        self.send_levels_loop(delay=.05)
 
        self.graph.addHandler(self.rowFromGraph)
 

	
 
    def make_buttons(self):
 
        self.buttonframe = tk.Frame(self, bg='black')
 
        self.buttonframe.pack(side=tk.BOTTOM)
 

	
 
        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)
 

	
 
    def redraw_sliders(self):
 
        self.draw_sliders()
 
@@ -219,17 +260,15 @@ class KeyboardComposer(tk.Frame, SubClie
 
        rowcount = -1
 
        col = 0
 
        last_group = None
 

	
 
        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)
 

	
 
        self.effectEval = {}
 
        reload(light9.effect.effecteval)
 
@@ -237,19 +276,21 @@ class KeyboardComposer(tk.Frame, SubClie
 
        for group, order, sortLabel, effect in withgroups:
 
            if col == 0 or group != last_group:
 
                row = self.make_row(group)
 
                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
 

	
 
    def toggle_slider_connectedness(self):
 
        self.use_hw_sliders = not self.use_hw_sliders
 
@@ -274,21 +315,25 @@ class KeyboardComposer(tk.Frame, SubClie
 
            self.sliders = DummySliders()
 

	
 
    def make_key_hints(self):
 
        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
 

	
 
        keyhintrow.pack(fill=tk.X, expand=0)
 
        self.keyhints = keyhintrow
 

	
 
@@ -319,13 +364,15 @@ class KeyboardComposer(tk.Frame, SubClie
 
        diff = 1
 
        if event.keysym in ('Prior', 'p', 'bracketright'):
 
            diff = -1
 
        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
 
        self.current_row = row
 
        self.current_row = max(0, self.current_row)
 
        self.current_row = min(len(self.rows) - 1, self.current_row)
 
@@ -354,14 +401,13 @@ class KeyboardComposer(tk.Frame, SubClie
 
            except KeyError:
 
                # unfilled bottom row has holes (plus rows with incomplete
 
                # groups
 
                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:
 
            subbox = self.slider_table[(self.current_row, number)]
 
        except KeyError:
 
            return
 
@@ -379,13 +425,13 @@ class KeyboardComposer(tk.Frame, SubClie
 

	
 
    def hw_slider_moved(self, col, value):
 
        value = int(value * 100) / 100
 
        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
 
            try:
 
                self.pendingHwSet.cancel()
 
            except twisted.internet.error.AlreadyCalled:
 
@@ -397,13 +443,13 @@ class KeyboardComposer(tk.Frame, SubClie
 
            return
 

	
 
        assert isinstance(sub, URIRef), repr(sub)
 

	
 
        if boxRow != self.current_row:
 
            return
 
        
 

	
 
        try:
 
            level = self.get_levels()[sub]
 
        except KeyError:
 
            log.warn("%r not in %r", sub, self.get_levels())
 
            raise
 
        v = round(127 * level)
 
@@ -418,43 +464,48 @@ class KeyboardComposer(tk.Frame, SubClie
 
        self.sliders.valueOut(chan, v)
 

	
 
    def make_row(self, group):
 
        """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)
 
        return row
 

	
 
    def change_group(self, sub, row):
 
        """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]
 
        row['bg'] = 'red'
 

	
 
    def unhighlight_row(self, row):
 
        row = self.rows[row]
 
        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
 
        outputSettings = []
 
        for setting in _graph.objects(self.session, L9['subSetting']):
 
            effect = _graph.value(setting, L9['sub'])
 
@@ -478,72 +529,81 @@ class KeyboardComposer(tk.Frame, SubClie
 
        ctx = URIRef(showconfig.showUri() + '/effect/' + subname)
 
        stmts = ds.statements(effect, ctx, effect + '/', set())
 
        stmts.extend([
 
            (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):
 
        for uri, subbox in self.subbox.items():
 
            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
 
    values. Supports args encoded in the url or in postdata. No
 
    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):
 
        arg = postArgGetter(request)
 

	
 
        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)
 
            except IOError:
 
                if dev is devices[-1]:
 
                    raise
 
            else:
 
                break
 

	
 
        self.kc = kc
 
        log.info('found sliders on %s', dev)
 

	
 
    def valueIn(self, name, value):
 
        kc = self.kc
 
        if name.startswith("slider"):
 
            kc.hw_slider_moved(int(name[6:]) - 1, value / 127)
 
        elif name.startswith("button-upper"):
 
            kc.change_row(kc.current_row)
 
@@ -569,45 +629,49 @@ class Sliders(BCF2000):
 
            else:
 
                return
 

	
 
            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")
 
    opts, args = parser.parse_args()
 

	
 
    log.setLevel(logging.INFO if opts.v else logging.WARN)
 

	
 
    graph = SyncedGraph(networking.rdfdb.url, "keyboardcomposer")
 

	
 
    # 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)
bin/lightsim
Show inline comments
 
@@ -18,32 +18,39 @@ from OpenGL.GLU import *
 

	
 
from light9 import networking, Patch, showconfig, dmxclient, updatefreq, prof
 
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
 

	
 
            try:
 
                chan = Patch.get_channel_uri(Patch.get_channel_name(i + 1))
 
@@ -51,24 +58,27 @@ def poll(graph, serv, pollFreq, oglSurfa
 
                continue
 

	
 
            for imgPath in filenamesForChan(graph, chan):
 
                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()
 
            self.layout.addWidget(row)
 
            cols = QHBoxLayout()
 
            row.setLayout(cols)
 
@@ -78,28 +88,31 @@ class StatusKeys(QWidget):
 
            cols.addWidget(lab2)
 
            self.row[key] = lab2
 
        else:
 
            lab = self.row[key]
 
            lab.setText(value)
 

	
 

	
 
class Window(QMainWindow):
 

	
 
    def __init__(self, filenames):
 
        QMainWindow.__init__(self, None)
 
        self.setWindowTitle(dmxclient._id)
 

	
 
        w = QWidget()
 
        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
 
    like this:
 
      ch:frontLeft a :Channel;
 
         :previewLayer [ :path "lightsim/skyline/front-left.png" ] .
 
@@ -107,12 +120,13 @@ def requiredImages(graph):
 
    filenames = []
 
    for lyr in graph.objects(None, L9['previewLayer']):
 
        for p in graph.objects(lyr, L9['path']):
 
            filenames.append(str(p))
 
    return filenames
 

	
 

	
 
if __name__ == '__main__':
 
    app = reactor.qApp
 

	
 
    graph = showconfig.getGraph()
 

	
 
    window = Window(requiredImages(graph))
 
@@ -120,7 +134,6 @@ if __name__ == '__main__':
 

	
 
    serv = Proxy(networking.dmxServer.url)
 
    pollFreq = updatefreq.Updatefreq()
 
    LoopingCall(poll, graph, serv, pollFreq, window.glWidget).start(.05)
 

	
 
    reactor.run()
 

	
bin/listsongs
Show inline comments
 
#!bin/python
 

	
 
"""for completion, print the available song uris on stdout
 

	
 
in .zshrc:
 

	
 
function _songs { local expl;  _description files expl 'songs';  compadd "$expl[@]" - `${LIGHT9_SHOW}/../../bin/listsongs` }
 
compdef _songs curvecalc
 
@@ -14,14 +13,16 @@ from rdflib import RDF
 
from light9 import networking
 
from light9.namespaces import L9
 
from rdfdb.syncedgraph import SyncedGraph
 

	
 
graph = SyncedGraph(networking.rdfdb.url, "listsongs")
 

	
 

	
 
@graph.initiallySynced.addCallback
 
def printSongs(result):
 
    with graph.currentState() as current:
 
        for song in current.subjects(RDF.type, L9['Song']):
 
            print song
 
    reactor.stop()
 

	
 

	
 
reactor.run()
bin/mpd_timing_test
Show inline comments
 
#!/usr/bin/python
 

	
 
"""
 
records times coming out of ascoltami
 

	
 
for example:
 

	
 
 % mpd_timing_test > timing
 
@@ -14,8 +13,8 @@ for example:
 

	
 
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)
bin/musicPad
Show inline comments
 
@@ -8,32 +8,30 @@ from light9 import showconfig
 
from light9.namespaces import L9
 
from light9.ascoltami.playlist import Playlist
 
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():
 
    log.info("read %s", p)
 
    inputWave = wave.open(p, 'r')
 

	
 
    outputDir = os.path.join(os.path.dirname(p), "pad")
 
    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)
 

	
 
    
bin/musictime
Show inline comments
 
@@ -3,51 +3,66 @@ import run_local
 
import light9.networking
 

	
 
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:
 
            try:
 
                playtime = jsonlib.read(self.player.get("time").body_string(),
 
                                        use_float=True)['t']
 
            except restkit.RequestError, e:
 
                print "Server error %s, waiting" % e
 
                time.sleep(2)
 
        return playtime
 

	
 

	
 
class MusicTimeTk(tk.Frame, MusicTime):
 

	
 
    def __init__(self, master, url):
 
        tk.Frame.__init__(self)
 
        MusicTime.__init__(self, url)
 
        self.timevar = tk.DoubleVar()
 
        self.timelabel = tk.Label(self, textvariable=self.timevar, bd=2,
 
            relief='raised', width=10, padx=2, pady=2, anchor='w')
 
        self.timelabel = tk.Label(self,
 
                                  textvariable=self.timevar,
 
                                  bd=2,
 
                                  relief='raised',
 
                                  width=10,
 
                                  padx=2,
 
                                  pady=2,
 
                                  anchor='w')
 
        self.timelabel.pack(expand=1, fill='both')
 

	
 
        def print_time(evt, *args):
 
            self.timevar.set(self.get_music_time())
 
            print self.timevar.get(), evt.keysym
 

	
 
        self.timelabel.bind('<KeyPress>', print_time)
 
        self.timelabel.bind('<1>', print_time)
 
        self.timelabel.focus()
 
        self.update_time()
 

	
 
    def update_time(self):
 
        self.timevar.set(self.get_music_time())
 
        self.after(100, self.update_time)
 

	
 

	
 
if __name__ == "__main__":
 
    from optparse import OptionParser
 
    parser = OptionParser()
 
    parser.add_option("-u", "--url", default=light9.networking.musicPlayer.url)
 
    options, args = parser.parse_args()
 
    
 

	
 
    root = tk.Tk()
 
    root.title("Time")
 
    MusicTimeTk(root, options.url).pack(expand=1, fill='both')
 
    try:
 
        tk.mainloop()
 
    except KeyboardInterrupt:
bin/paintserver
Show inline comments
 
@@ -14,73 +14,85 @@ from rdflib import URIRef
 
from light9 import clientsession
 
import light9.paint.solve
 
from lib.cycloneerr import PrettyErrorHandler
 
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']
 
        devs = [URIRef(d) for d in body['devices']]
 
        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),
 
            (r'/bestMatches', BestMatches),
 
        ],
 
                                                  debug=True,
 
@@ -90,26 +102,30 @@ class App(object):
 
        reactor.listenTCP(networking.paintServer.port, self.cycloneApp)
 
        log.info("listening on %s" % networking.paintServer.port)
 

	
 

	
 
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()
 
    log.setLevel(logging.DEBUG if options.verbose else logging.INFO)
 

	
 
    if not options.show:
 
        raise ValueError("missing --show http://...")
 
        
 

	
 
    session = clientsession.getUri('paint', options)
 

	
 
    app = App(URIRef(options.show), session)
 
    if options.twistedlog:
 
        from twisted.python import log as twlog
 
        twlog.startLogging(sys.stderr)
bin/picamserve
Show inline comments
 
#!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
 
from twisted.internet.defer import inlineCallbacks
 
from light9 import prof
 

	
 
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 = {
 
        480: (640, 480),
 
        1080: (1920, 1080),
 
        1944: (2592, 1944),
 
    }[res]
 
    c.shutter_speed = int(arg('shutter', 50000))
 
    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 ]
 
    scl1 = rw / (c.crop[2] * c.resolution[0])
 
    scl2 = rh / (c.crop[3] * c.resolution[1])
 
    if scl1 < scl2:
 
        # width is the constraint; reduce height to the same scale
 
        rh = int(scl1 * c.crop[3] * c.resolution[1])
 
    else:
 
        # 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)
 
    resize = setupCrop(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')
 
            self.write(getFrame(self.settings.camera, self.get_argument))
 
        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
 
                # pic during this time instead of pausing.
 
                # 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()
 
            log.debug(" - sending to onFrame took %.1fms", 1000 * (t3 - t2))
 
            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:
 
            self.set_header('Content-Type', 'x-application/length-time-jpeg')
 
            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
 
            # coming in
 
            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()
bin/rdfdb
Show inline comments
 
@@ -2,20 +2,20 @@
 
import run_local
 
import os
 
from light9 import networking, showconfig
 
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/',
 
        'rdf': 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
 
        'rdfs': 'http://www.w3.org/2000/01/rdf-schema#',
 
        'xsd': 'http://www.w3.org/2001/XMLSchema#',
 
        'effect': 'http://light9.bigasterisk.com/effect/',
 
        'dev': 'http://light9.bigasterisk.com/device/',
 
    },
 
    port=networking.rdfdb.port,
 
    )
 

	
 
)
bin/run_local.py
Show inline comments
 
# allows bin/* to work without installation
 

	
 
# this should be turned off when the programs are installed
 

	
 
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 = [
 
        root,
 
        root + 'env/lib/python2.7',
 
        root + 'env/lib/python2.7/plat-x86_64-linux-gnu',
 
@@ -43,57 +50,66 @@ def fixSysPath():
 
        root + 'env/local/lib/python2.7/site-packages',
 
        root + 'env/local/lib/python2.7/site-packages/gtk-2.0',
 
        root + 'env/lib/python2.7/site-packages',
 
        root + 'env/lib/python2.7/site-packages/gtk-2.0',
 
    ]
 

	
 

	
 
fixSysPath()
 

	
 
from twisted.python.failure import Failure
 

	
 
try:
 
    import Tkinter
 
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
 
try:
 
    import faulthandler
 
    faulthandler.enable()
 
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)) +
 
            ("%.3f" % (record.created % 1)).lstrip('0'))
 
        # Don't filter the record.
 
        return 1
 

	
 

	
 
coloredlogs.install(
 
    level='DEBUG',
 
    fmt='%(fractionTime)s %(name)s[%(process)d] %(levelname)s %(message)s')
 
logging.getLogger().handlers[0].addFilter(FractionTimeFilter())
 

	
 

	
 
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
bin/staticclient
Show inline comments
 
@@ -13,22 +13,25 @@ from run_local import log
 
log.setLevel(logging.DEBUG)
 

	
 
from light9 import dmxclient, showconfig, networking
 

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

	
 
    opts, args = parser.parse_args()
 

	
 
    log.setLevel(logging.DEBUG if opts.v else logging.INFO)
 

	
 
    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()
bin/subcomposer
Show inline comments
 
@@ -66,12 +66,13 @@ class Subcomposer(tk.Frame):
 
      graph -> Submaster levels (handled in Submaster)
 
      Submaster levels -> OneLevel widget
 
      OneLevel widget -> Submaster.editLevel
 
      Submaster.editLevel -> graph (handled in Submaster)
 

	
 
    """
 

	
 
    def __init__(self, master, graph, session):
 
        tk.Frame.__init__(self, master, bg='black')
 
        self.graph = graph
 
        self.session = session
 
        self.launchTime = time.time()
 
        self.localSerial = 0
 
@@ -80,17 +81,18 @@ class Subcomposer(tk.Frame):
 
        # has a uri, which you can get from
 
        # self.currentSub.uri. Probably that should be on the Local
 
        # object too, or maybe Local should be a subclass of URIRef
 
        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)
 
        ec.frame.pack(side='top')
 

	
 
        ec.subIcon.bind("<ButtonPress-1>", self.clickSubIcon)
 
@@ -102,89 +104,92 @@ class Subcomposer(tk.Frame):
 
        box.wm_transient(self.editChoice.frame)
 
        tk.Label(box, text="Name this sub:").pack()
 
        e = tk.Entry(box)
 
        e.pack()
 
        b = tk.Button(box, text="Make global")
 
        b.pack()
 

	
 
        def clicked(*args):
 
            self.makeGlobal(newName=e.get())
 
            box.destroy()
 
            
 

	
 
        b.bind("<Button-1>", clicked)
 
        e.focus()
 
        
 

	
 
    def makeGlobal(self, newName):
 
        """promote our local submaster into a non-local, named one"""
 
        uri = self.currentSub().uri
 
        newUri = showconfig.showUri() + ("/sub/%s" %
 
                                         urllib.quote(newName, safe=''))
 
        with self.graph.currentState(
 
                tripleFilter=(uri, None, None)) as current:
 
        with self.graph.currentState(tripleFilter=(uri, None, None)) as current:
 
            if (uri, RDF.type, L9['LocalSubmaster']) not in current:
 
                raise ValueError("%s is not a local submaster" % uri)
 
            if (newUri, None, None) in current:
 
                raise ValueError("new uri %s is in use" % newUri)
 
                
 

	
 
        # the local submaster was storing in ctx=self.session, but now
 
        # we want it to be in ctx=uri
 

	
 
        self.relocateSub(newUri, newName)
 

	
 
        # these are in separate patches for clarity as i'm debugging this
 
        self.graph.patch(Patch(addQuads=[
 
            (newUri, RDFS.label, Literal(newName), newUri),
 
        ], delQuads=[
 
            (newUri, RDF.type, L9['LocalSubmaster'], newUri),
 
                           ]))
 
        self.graph.patchObject(self.session,
 
                               self.session, L9['currentSub'], newUri)
 
        
 
        self.graph.patch(
 
            Patch(addQuads=[
 
                (newUri, RDFS.label, Literal(newName), newUri),
 
            ],
 
                  delQuads=[
 
                      (newUri, RDF.type, L9['LocalSubmaster'], newUri),
 
                  ]))
 
        self.graph.patchObject(self.session, self.session, L9['currentSub'],
 
                               newUri)
 

	
 
    def relocateSub(self, newUri, newName):
 
        # maybe this goes in Submaster
 
        uri = self.currentSub().uri
 

	
 
        def repl(u):
 
            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())
 

	
 
        @graph.addHandler
 
        def graphChanged():
 
            # some bug where SC is making tons of graph edits and many
 
            # 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:
 
                s = self.switchToLocal()
 
            self.currentSub(Submaster.PersistentSubmaster(graph, s))
 

	
 
        @self.currentSub.subscribe
 
        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:
 
                if newSub and localStmt in current:
 
                    log.debug('  HANDLER set _currentChoice to Local')
 
                    self._currentChoice(Local)
 
@@ -192,113 +197,119 @@ class Subcomposer(tk.Frame):
 
                    # i think right here is the point that the last local
 
                    # becomes garbage, and we could clean it up.
 
                    log.debug('  HANDLER set _currentChoice to newSub.uri')
 
                    self._currentChoice(newSub.uri)
 

	
 
        dispatcher.connect(self.levelsChanged, "sub levels changed")
 
            
 

	
 
        @self._currentChoice.subscribe
 
        def choiceChanged(newChoice):
 
            log.debug('HANDLER choiceChanged to %s', newChoice)
 
            if newChoice is Local:
 
                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
 
        """
 
        # todo: where will these get stored, or are they local to this
 
        # subcomposer process and don't use PersistentSubmaster at all?
 
        localId = "%s-%s" % (self.launchTime, self.localSerial)
 
        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):
 
        d = self.currentSub().get_dmx_list()
 
        dmxclient.outputlevels(d, twisted=True)
 

	
 

	
 
def launch(opts, args, root, graph, session):
 
    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
 
        # process changes anything else in the same session. But also
 
        # I'm not sure who would ever make 2 subcomposers of the same
 
        # session (except when quitting and restarting, to get the
 
        # 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)
 

	
 

	
 
#############################
 

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

	
 
    clientsession.add_option(parser)
 
    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")
 

	
 
    initTkdnd(root.tk, 'tkdnd/trunk/')
 

	
 
    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)
bin/subserver
Show inline comments
 
@@ -13,77 +13,92 @@ import pyjade.utils
 
from rdfdb.syncedgraph import SyncedGraph
 
from rdfdb.patch import Patch
 
from light9.namespaces import L9, DCTERMS
 
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)
 

	
 
    def respondStaticJade(self, src):
 
        html = pyjade.utils.process(open(src).read())
 
        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
 
        # better for us to tell vidref where to attach the result in
 
        # 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']):
 
        created = graph.value(img, DCTERMS['created'])
 
        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()
 

	
 
    log.setLevel(logging.DEBUG if options.verbose else logging.INFO)
 

	
 
    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()
bin/tkdnd_minimal_drop.py
Show inline comments
 
@@ -10,33 +10,47 @@ label = tk.Label(root, borderwidth=2, re
 
label.pack()
 
label.config(text="drop target %s" % label._w)
 

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

	
 

	
 
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()
bin/tracker
Show inline comments
 
#!/usr/bin/python
 
from __future__ import division,nested_scopes
 
from __future__ import division, nested_scopes
 

	
 
import sys
 
sys.path.append("../../editor/pour")
 
sys.path.append("../light8")
 

	
 
from Submaster import Submaster
 
from skim.zooming import Zooming,Pair
 
from math import sqrt,sin,cos
 
from skim.zooming import Zooming, Pair
 
from math import sqrt, sin, cos
 
from pygame.rect import Rect
 
from xmlnodebase import xmlnodeclass,collectiveelement,xmldocfile
 
from xmlnodebase import xmlnodeclass, collectiveelement, xmldocfile
 
from dispatch import dispatcher
 

	
 
import dmxclient
 

	
 
import Tkinter as tk
 

	
 
defaultfont="arial 8"
 
defaultfont = "arial 8"
 

	
 
def pairdist(pair1,pair2):
 

	
 
def pairdist(pair1, pair2):
 
    return pair1.dist(pair2)
 

	
 
def canvashighlighter(canvas,obj,attribute,normalval,highlightval):
 

	
 
def canvashighlighter(canvas, obj, attribute, normalval, highlightval):
 
    """creates bindings on a canvas obj that make attribute go
 
    from normal to highlight when the mouse is over the obj"""
 
    canvas.tag_bind(obj,"<Enter>",
 
                    lambda ev: canvas.itemconfig(obj,**{attribute:highlightval}))
 
    canvas.tag_bind(obj,"<Leave>",
 
                    lambda ev: canvas.itemconfig(obj,**{attribute:normalval}))
 
    canvas.tag_bind(
 
        obj, "<Enter>", lambda ev: canvas.itemconfig(
 
            obj, **{attribute: highlightval}))
 
    canvas.tag_bind(
 
        obj,
 
        "<Leave>", lambda ev: canvas.itemconfig(obj, **{attribute: normalval}))
 

	
 

	
 
class Field(xmlnodeclass):
 
    
 
    """one light has a field of influence. for any point on the
 
    canvas, you can ask this field how strong it is. """
 

	
 
    def name(self,newval=None):
 
    def name(self, newval=None):
 
        """light/sub name"""
 
        return self._getorsetattr("name",newval)
 
    def center(self,x=None,y=None):
 
        return self._getorsetattr("name", newval)
 

	
 
    def center(self, x=None, y=None):
 
        """x,y float coords for the center of this light in the field. returns
 
        a Pair, although it accepts x,y"""
 
        return Pair(self._getorsettypedattr("x",float,x),
 
                    self._getorsettypedattr("y",float,y))
 
        return Pair(self._getorsettypedattr("x", float, x),
 
                    self._getorsettypedattr("y", float, y))
 

	
 
    def falloff(self,dist=None):
 
        
 
    def falloff(self, dist=None):
 
        """linear falloff from 1 at center, to 0 at dist pixels away
 
        from center"""
 
        return self._getorsettypedattr("falloff",float,dist)
 
        return self._getorsettypedattr("falloff", float, dist)
 

	
 
    def getdistforintensity(self,intens):
 
    def getdistforintensity(self, intens):
 
        """returns the distance you'd have to be for the given intensity (0..1)"""
 
        return (1-intens)*self.falloff()
 
        return (1 - intens) * self.falloff()
 

	
 
    def calc(self,x,y):
 
    def calc(self, x, y):
 
        """returns field strength at point x,y"""
 
        dist=pairdist(Pair(x,y),self.center())
 
        return max(0,(self.falloff()-dist)/self.falloff())
 
        dist = pairdist(Pair(x, y), self.center())
 
        return max(0, (self.falloff() - dist) / self.falloff())
 

	
 

	
 
class Fieldset(collectiveelement):
 
    """group of fields. persistent."""
 
    def childtype(self): return Field
 

	
 
    def childtype(self):
 
        return Field
 

	
 
    def version(self):
 
        """read-only version attribute on fieldset tag"""
 
        return self._getorsetattr("version",None)
 
        return self._getorsetattr("version", None)
 

	
 
    def report(self,x,y):
 
    def report(self, x, y):
 
        """reports active fields and their intensities"""
 
        active=0
 
        active = 0
 
        for f in self.getall():
 
            name=f.name()
 
            intens=f.calc(x,y)
 
            if intens>0:
 
                print name,intens,
 
                active+=1
 
        if active>0:
 
            name = f.name()
 
            intens = f.calc(x, y)
 
            if intens > 0:
 
                print name, intens,
 
                active += 1
 
        if active > 0:
 
            print
 
        self.dmxsend(x,y)
 
    def dmxsend(self,x,y):
 
        self.dmxsend(x, y)
 

	
 
    def dmxsend(self, x, y):
 
        """output lights to dmx"""
 
        levels=dict([(f.name(),f.calc(x,y)) for f in self.getall()])
 
        dmxlist=Submaster(None,levels).get_dmx_list()
 
        levels = dict([(f.name(), f.calc(x, y)) for f in self.getall()])
 
        dmxlist = Submaster(None, levels).get_dmx_list()
 
        dmxclient.outputlevels(dmxlist)
 
        
 
        
 

	
 
    def getbounds(self):
 
        """returns xmin,xmax,ymin,ymax for the non-zero areas of this field"""
 
        r=None
 
        r = None
 
        for f in self.getall():
 
            rad=f.getdistforintensity(0)
 
            fx,fy=f.center()
 
            fieldrect=Rect(fx-rad,fy-rad,rad*2,rad*2)
 
            rad = f.getdistforintensity(0)
 
            fx, fy = f.center()
 
            fieldrect = Rect(fx - rad, fy - rad, rad * 2, rad * 2)
 
            if r is None:
 
                r=fieldrect
 
                r = fieldrect
 
            else:
 
                r=r.union(fieldrect)
 
        return r.left,r.right,r.top,r.bottom
 
                r = r.union(fieldrect)
 
        return r.left, r.right, r.top, r.bottom
 

	
 

	
 
class Fieldsetfile(xmldocfile):
 
    def __init__(self,filename):
 
        self._openornew(filename,topleveltype=Fieldset)
 

	
 
    def __init__(self, filename):
 
        self._openornew(filename, topleveltype=Fieldset)
 

	
 
    def fieldset(self):
 
        return self._gettoplevel()
 

	
 

	
 
########################################################################
 
########################################################################
 

	
 

	
 
class FieldDisplay:
 
    """the view for a Field."""
 
    def __init__(self,canvas,field):
 
        self.canvas=canvas
 
        self.field=field
 
        self.tags=[str(id(self))] # canvas tag to id our objects
 
        
 

	
 
    def __init__(self, canvas, field):
 
        self.canvas = canvas
 
        self.field = field
 
        self.tags = [str(id(self))]  # canvas tag to id our objects
 

	
 
    def setcoords(self):
 
        """adjust canvas obj coords to match the field"""
 
        # this uses the canvas object ids saved by makeobjs
 
        f=self.field
 
        c=self.canvas
 
        w2c=self.canvas.world2canvas
 
        f = self.field
 
        c = self.canvas
 
        w2c = self.canvas.world2canvas
 

	
 
        # rings
 
        for intens,ring in self.rings.items():
 
            rad=f.getdistforintensity(intens)
 
            p1=w2c(*(f.center()-Pair(rad,rad)))
 
            p2=w2c(*(f.center()+Pair(rad,rad)))
 
            c.coords(ring,p1[0],p1[1],p2[0],p2[1])
 
        for intens, ring in self.rings.items():
 
            rad = f.getdistforintensity(intens)
 
            p1 = w2c(*(f.center() - Pair(rad, rad)))
 
            p2 = w2c(*(f.center() + Pair(rad, rad)))
 
            c.coords(ring, p1[0], p1[1], p2[0], p2[1])
 

	
 
        # text
 
        p1=w2c(*f.center())
 
        c.coords(self.txt,*p1)
 
        p1 = w2c(*f.center())
 
        c.coords(self.txt, *p1)
 

	
 
    def makeobjs(self):
 
        """(re)create the canvas objs (null coords) and make their bindings"""
 
        c=self.canvas
 
        f=self.field
 
        c = self.canvas
 
        f = self.field
 
        c.delete(self.tags)
 

	
 
        w2c=self.canvas.world2canvas
 
        w2c = self.canvas.world2canvas
 

	
 
        # make rings
 
        self.rings={} # rad,canvasobj
 
        for intens,color in (#(1,'white'),
 
                             (.8,'gray90'),(.6,'gray80'),(.4,'gray60'),(.2,'gray50'),
 
                             (0,'#000080')):
 
            self.rings[intens]=c.create_oval(0,0,0,0,
 
                                          outline=color,width=2,tags=self.tags,
 
                                          outlinestipple='gray50')
 
        self.rings = {}  # rad,canvasobj
 
        for intens, color in (  #(1,'white'),
 
            (.8, 'gray90'), (.6, 'gray80'), (.4, 'gray60'), (.2, 'gray50'),
 
            (0, '#000080')):
 
            self.rings[intens] = c.create_oval(0,
 
                                               0,
 
                                               0,
 
                                               0,
 
                                               outline=color,
 
                                               width=2,
 
                                               tags=self.tags,
 
                                               outlinestipple='gray50')
 

	
 
        # make text
 
        self.txt=c.create_text(0,0,text=f.name(),font=defaultfont+" bold",
 
                               fill='white',anchor='c',
 
                               tags=self.tags)
 
        self.txt = c.create_text(0,
 
                                 0,
 
                                 text=f.name(),
 
                                 font=defaultfont + " bold",
 
                                 fill='white',
 
                                 anchor='c',
 
                                 tags=self.tags)
 

	
 
        # highlight text bindings
 
        canvashighlighter(c,self.txt,'fill',normalval='white',highlightval='red')
 
        canvashighlighter(c,
 
                          self.txt,
 
                          'fill',
 
                          normalval='white',
 
                          highlightval='red')
 

	
 
        # position drag bindings
 
        def press(ev):
 
            self._lastmouse=ev.x,ev.y
 
            self._lastmouse = ev.x, ev.y
 

	
 
        def motion(ev):
 
            dcan=Pair(*[a-b for a,b in zip((ev.x,ev.y),self._lastmouse)])
 
            dworld=c.canvas2world_vector(*dcan)
 
            self.field.center(*(self.field.center()+dworld))
 
            self._lastmouse=ev.x,ev.y
 
            self.setcoords() # redraw
 
            dcan = Pair(*[a - b for a, b in zip((ev.x, ev.y), self._lastmouse)])
 
            dworld = c.canvas2world_vector(*dcan)
 
            self.field.center(*(self.field.center() + dworld))
 
            self._lastmouse = ev.x, ev.y
 
            self.setcoords()  # redraw
 

	
 
        def release(ev):
 
            if hasattr(self,'_lastmouse'):
 
            if hasattr(self, '_lastmouse'):
 
                del self._lastmouse
 
            dispatcher.send("field coord changed") # updates bounds
 
            
 
        c.tag_bind(self.txt,"<ButtonPress-1>",press)
 
        c.tag_bind(self.txt,"<B1-Motion>",motion)
 
        c.tag_bind(self.txt,"<B1-ButtonRelease>",release)
 
            dispatcher.send("field coord changed")  # updates bounds
 

	
 
        c.tag_bind(self.txt, "<ButtonPress-1>", press)
 
        c.tag_bind(self.txt, "<B1-Motion>", motion)
 
        c.tag_bind(self.txt, "<B1-ButtonRelease>", release)
 

	
 
        # radius drag bindings
 
        outerring=self.rings[0]
 
        canvashighlighter(c,outerring,
 
                          'outline',normalval='#000080',highlightval='#4040ff')
 
        outerring = self.rings[0]
 
        canvashighlighter(c,
 
                          outerring,
 
                          'outline',
 
                          normalval='#000080',
 
                          highlightval='#4040ff')
 

	
 
        def motion(ev):
 
            worldmouse=self.canvas.canvas2world(ev.x,ev.y)
 
            currentdist=pairdist(worldmouse,self.field.center())
 
            worldmouse = self.canvas.canvas2world(ev.x, ev.y)
 
            currentdist = pairdist(worldmouse, self.field.center())
 
            self.field.falloff(currentdist)
 
            self.setcoords()
 
        c.tag_bind(outerring,"<B1-Motion>",motion)
 
        c.tag_bind(outerring,"<B1-ButtonRelease>",release) # from above
 

	
 
        c.tag_bind(outerring, "<B1-Motion>", motion)
 
        c.tag_bind(outerring, "<B1-ButtonRelease>", release)  # from above
 

	
 
        self.setcoords()
 
    
 

	
 

	
 
class Tracker(tk.Frame):
 

	
 
    """whole tracker widget, which is mostly a view for a
 
    Fieldset. tracker makes its own fieldset"""
 

	
 
    # world coords of the visible canvas (preserved even in window resizes)
 
    xmin=0
 
    xmax=100
 
    ymin=0
 
    ymax=100
 
    xmin = 0
 
    xmax = 100
 
    ymin = 0
 
    ymax = 100
 

	
 
    fieldsetfile = None
 
    displays = None  # Field : FieldDisplay. we keep these in sync with the fieldset
 

	
 
    fieldsetfile=None
 
    displays=None # Field : FieldDisplay. we keep these in sync with the fieldset
 
    
 
    def __init__(self,master):
 
        tk.Frame.__init__(self,master)
 
    def __init__(self, master):
 
        tk.Frame.__init__(self, master)
 

	
 
        self.displays={}
 
        
 
        c=self.canvas=Zooming(self,bg='black',closeenough=5)
 
        c.pack(fill='both',exp=1)
 
        self.displays = {}
 

	
 
        c = self.canvas = Zooming(self, bg='black', closeenough=5)
 
        c.pack(fill='both', exp=1)
 

	
 
        # preserve edge coords over window resize
 
        c.bind("<Configure>",self.configcoords)
 
        
 
        c.bind("<Motion>",
 
               lambda ev: self._fieldset().report(*c.canvas2world(ev.x,ev.y)))
 
        c.bind("<Configure>", self.configcoords)
 

	
 
        c.bind("<Motion>", lambda ev: self._fieldset().report(*c.canvas2world(
 
            ev.x, ev.y)))
 

	
 
        def save(ev):
 
            print "saving"
 
            self.fieldsetfile.save()
 
        master.bind("<Key-s>",save)
 
        dispatcher.connect(self.autobounds,"field coord changed")
 

	
 
        master.bind("<Key-s>", save)
 
        dispatcher.connect(self.autobounds, "field coord changed")
 

	
 
    def _fieldset(self):
 
        return self.fieldsetfile.fieldset()
 

	
 
    def load(self,filename):
 
        self.fieldsetfile=Fieldsetfile(filename)
 
    def load(self, filename):
 
        self.fieldsetfile = Fieldsetfile(filename)
 
        self.displays.clear()
 
        for f in self.fieldsetfile.fieldset().getall():
 
            self.displays[f]=FieldDisplay(self.canvas,f)
 
            self.displays[f] = FieldDisplay(self.canvas, f)
 
            self.displays[f].makeobjs()
 
        self.autobounds()
 

	
 
    def configcoords(self,*args):
 
    def configcoords(self, *args):
 
        # force our canvas coords to stay at the edges of the window
 
        c=self.canvas
 
        cornerx,cornery=c.canvas2world(0,0)
 
        c.move(cornerx-self.xmin,
 
               cornery-self.ymin)
 
        c.setscale(0,0,
 
                   c.winfo_width()/(self.xmax-self.xmin),
 
                   c.winfo_height()/(self.ymax-self.ymin))
 
        c = self.canvas
 
        cornerx, cornery = c.canvas2world(0, 0)
 
        c.move(cornerx - self.xmin, cornery - self.ymin)
 
        c.setscale(0, 0,
 
                   c.winfo_width() / (self.xmax - self.xmin),
 
                   c.winfo_height() / (self.ymax - self.ymin))
 

	
 
    def autobounds(self):
 
        """figure out our bounds from the fieldset, and adjust the display zooms.
 
        writes the corner coords onto the canvas."""
 
        self.xmin,self.xmax,self.ymin,self.ymax=self._fieldset().getbounds()
 
        self.xmin, self.xmax, self.ymin, self.ymax = self._fieldset().getbounds(
 
        )
 

	
 
        self.configcoords()
 
        
 
        c=self.canvas
 

	
 
        c = self.canvas
 
        c.delete('cornercoords')
 
        for x,anc2 in ((self.xmin,'w'),(self.xmax,'e')):
 
            for y,anc1 in ((self.ymin,'n'),(self.ymax,'s')):
 
                pos=c.world2canvas(x,y)
 
                c.create_text(pos[0],pos[1],text="%s,%s"%(x,y),
 
                              fill='white',anchor=anc1+anc2,
 
        for x, anc2 in ((self.xmin, 'w'), (self.xmax, 'e')):
 
            for y, anc1 in ((self.ymin, 'n'), (self.ymax, 's')):
 
                pos = c.world2canvas(x, y)
 
                c.create_text(pos[0],
 
                              pos[1],
 
                              text="%s,%s" % (x, y),
 
                              fill='white',
 
                              anchor=anc1 + anc2,
 
                              tags='cornercoords')
 
        [d.setcoords() for d in self.displays.values()]
 

	
 

	
 
########################################################################
 
########################################################################
 
                
 
root=tk.Tk()
 

	
 
root = tk.Tk()
 
root.wm_geometry('700x350')
 
tra=Tracker(root)
 
tra.pack(fill='both',exp=1)
 
tra = Tracker(root)
 
tra.pack(fill='both', exp=1)
 

	
 
tra.load("fieldsets/demo")
 

	
 
root.mainloop()
bin/vidref
Show inline comments
 
#!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
 
import gobject
 
gobject.threads_init()
 
import gtk
 
@@ -12,49 +12,51 @@ import sys, logging, optparse, json
 
import cyclone.web, cyclone.httpclient, cyclone.websocket
 
from light9 import networking, showconfig
 
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
 
        # return /snapshot/path
 
        try:
 
            outputFilename = yield self.settings.gui.snapshot()
 

	
 
            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)
 
        except Exception as e:
 
            import traceback
 
            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']
 
        source = body['source']
 
        self.settings.gui.incomingTime(t, source)
 
        self.set_status(202)
 
@@ -62,17 +64,24 @@ class Time(cyclone.web.RequestHandler):
 

	
 
graph = SyncedGraph(networking.rdfdb.url, "vidref")
 

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

	
bin/vidrefsetup
Show inline comments
 
@@ -13,57 +13,74 @@ from rdfdb.syncedgraph import SyncedGrap
 
from rdfdb.patch import Patch
 
from light9.namespaces import L9, DCTERMS
 
from light9 import networking, showconfig
 

	
 
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()
 
        with graph.currentState(tripleFilter=(show, None, None)) as g:
 
            ret = g.value(show, L9['vidrefCamRequest'])
 
            if ret is None:
 
                self.send_error(404)
 
            self.redirect(ret)
 
            
 

	
 
    def put(self):
 
        graph = self.settings.graph
 
        show = showconfig.showUri()
 
        graph.patchObject(context=URIRef(show + '/vidrefConfig'),
 
                          subject=show,
 
                          predicate=L9['vidrefCamRequest'],
 
                          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()
 

	
 
    log.setLevel(logging.DEBUG if options.verbose else logging.INFO)
 
    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()
bin/wavecurve
Show inline comments
 
#!bin/python
 
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
 
    from light9.namespaces import L9
 
    from rdflib import RDF
 
    from light9.ascoltami.playlist import Playlist
 
    graph = showconfig.getGraph()
 

	
 
    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)
 
else:
 
    inpath, outpath = args
 
    createCurve(inpath, outpath, options.t)
bin/webcontrol
Show inline comments
 
@@ -18,20 +18,20 @@ from rdflib import URIRef
 
from louie.robustapply import robust_apply
 
sys.path.append(".")
 
from light9 import showconfig, networking
 
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)
 
        songPath = graph.value(URIRef(songUri), L9.showPath)
 
        if songPath is None:
 
            raise ValueError("unknown song %s" % songUri)
 
@@ -42,23 +42,28 @@ class Commands(object):
 
        s = xmlrpclib.ServerProxy(networking.musicPlayer.url)
 
        return s.stop()
 

	
 
    @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"))
 

	
 
    def __init__(self, graph):
 
        self.graph = graph
 
        rend.Page.__init__(self)
 
@@ -72,43 +77,45 @@ class Main(rend.Page):
 
    def render_songButtons(self, ctx, data):
 
        playList = graph.value(show, L9['playList'])
 
        songs = list(graph.items(playList))
 
        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
 
    def locateChild(self, ctx, segments):
 
        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:
 
            pass
 
        returnValue(rend.Page.locateChild(self, ctx, segments))
 

	
 
    def child_icon(self, ctx):
 
        return static.File("/usr/share/pyshared/elisa/plugins/poblesec/tango")
 
            
 

	
 

	
 
graph = showconfig.getGraph()
 
show = showconfig.showUri()
 

	
 
log.startLogging(sys.stdout)
 

	
 
reactor.listenTCP(9000, NevowSite(Main(graph)))
light9/Effects.py
Show inline comments
 
@@ -8,56 +8,69 @@ import showconfig
 
from rdflib import RDF
 
from light9 import Patch
 
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 '<Strip which=%r px0=%r>' % (self.which, self.pixels[0])
 
    
 

	
 
    @classmethod
 
    def solid(cls, which='L', color=(1,1,1), hsv=None):
 
    def solid(cls, which='L', color=(1, 1, 1), hsv=None):
 
        """hsv overrides color"""
 
        if hsv is not None:
 
            color = colorsys.hsv_to_rgb(hsv[0] % 1.0, hsv[1], hsv[2])
 
        x = cls()
 
        x.which = which
 
        x.pixels = [tuple(color)] * 50
 
        return x
 

	
 
    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:
 
        r = random_mod.Random(random)
 
        names = names[:]
 
        r.shuffle(names)
 
@@ -66,30 +79,33 @@ def chase(t, ontime=0.5, offset=0.2, onv
 
    lev = {}
 
    for uri, value in chase_vals.items():
 
        try:
 
            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
 

	
 
    fade=0 makes steps, fade=1 means each one gets its full fraction
 
    of the time to fade in. Fades never...
 
@@ -99,25 +115,28 @@ def stack(t, names=None, fade=0):
 
    lev = {}
 
    for i, uri in enumerate(names):
 
        if t >= (i + 1) * frac:
 
            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 = {}
 

	
 
    for chaseUri in graph.subjects(RDF.type, L9['Chase']):
 
        shortName = chaseUri.rsplit('/')[-1]
 
@@ -127,14 +146,16 @@ def configExprGlobals():
 

	
 
    for f in registered:
 
        ret[f.__name__] = f
 

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

	
 
    # suffix '2' to keep backcompat with the versions that magically knew time
 
    def smooth_random2(t, speed=1):
 
@@ -153,10 +174,7 @@ def configExprGlobals():
 
        y1 = _smooth_random_items[x1]
 
        return y1
 

	
 
    ret['noise2'] = smooth_random2
 
    ret['notch2'] = notch_random2
 

	
 

	
 

	
 
    
 
    return ret
light9/Fadable.py
Show inline comments
 
# taken from SnackMix -- now that's reusable code
 
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.
 
    It's probably too specialized to be used elsewhere, but could possibly
 
    work with an Entry or a Meter, I guess.  (Actually, this is used by
 
    KeyboardComposer and KeyboardRecorder now too.)
 
@@ -18,30 +19,34 @@ class Fadable:
 

	
 
    If mouse_bindings is true, the following mouse bindings will be
 
    installed: Right clicking toggles muting.  The mouse wheel will
 
    raise or lower the volume.  Shift-mouse wheeling will cause a more
 
    precise volume adjustment.  Control-mouse wheeling will cause a
 
    longer fade."""
 
    def __init__(self, var, wheel_step=5, use_fades=1, key_bindings=1,
 

	
 
    def __init__(self,
 
                 var,
 
                 wheel_step=5,
 
                 use_fades=1,
 
                 key_bindings=1,
 
                 mouse_bindings=1):
 
        self.use_fades = use_fades # whether increase and decrease should fade
 
        self.wheel_step = wheel_step # amount that increase and descrease should
 
                                     # change volume (by default)
 
        
 
        self.use_fades = use_fades  # whether increase and decrease should fade
 
        self.wheel_step = wheel_step  # amount that increase and descrease should
 
        # change volume (by default)
 

	
 
        self.fade_start_level = 0
 
        self.fade_end_level = 0
 
        self.fade_start_time = 0
 
        self.fade_length = 1
 
        self.fade_step_time = 10
 
        self.fade_var = var
 
        self.fading = 0 # whether a fade is in progress
 
        self.fading = 0  # whether a fade is in progress
 

	
 
        if key_bindings:
 
            for k in range(1, 10):
 
                self.bind("<Key-%d>" % k,
 
                    lambda evt, k=k: self.fade(k / 10.0))
 
                self.bind("<Key-%d>" % k, lambda evt, k=k: self.fade(k / 10.0))
 
            self.bind("<Key-0>", lambda evt: self.fade(1.0))
 
            self.bind("<grave>", lambda evt: self.fade(0))
 

	
 
            # up / down arrows
 
            self.bind("<Key-Up>", lambda evt: self.increase())
 
            self.bind("<Key-Down>", lambda evt: self.decrease())
 
@@ -58,43 +63,44 @@ class Fadable:
 
            # modified mouse wheel
 
            self.bind('<Shift-4>', lambda evt: self.increase(multiplier=0.2))
 
            self.bind('<Shift-5>', lambda evt: self.decrease(multiplier=0.2))
 
            self.bind('<Control-4>', lambda evt: self.increase(length=1))
 
            self.bind('<Control-5>', lambda evt: self.decrease(length=1))
 

	
 
        self.last_level = None # used for muting
 
        self.last_level = None  # used for muting
 

	
 
    def set_var_rounded(self, value):
 
        """use this instead of just self.fade_var.set(value) so we can
 
        control the precision"""
 
        # this was just to make the display not look so weird, but it
 
        # could actually affect the speed of really slow fades. If
 
        # that's a problem, do a real trace_write hook for the
 
        # 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."""
 
        now = time.time()
 
        elapsed = now - self.fade_start_time
 
        complete = elapsed / self.fade_length
 
@@ -103,49 +109,52 @@ class Fadable:
 
        newlevel = (complete * diff) + self.fade_start_level
 
        self.set_var_rounded(newlevel)
 
        if complete < 1:
 
            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."""
 
        amount = self.wheel_step * multiplier
 
        if self.fading:
 
            newlevel = self.fade_end_level + amount
 
        else:
 
            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."""
 
        amount = self.wheel_step * multiplier
 
        if self.fading:
 
            newlevel = self.fade_end_level - amount
 
        else:
 
            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."""
 
        if self.use_fades:
 
            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
 
        else:
 
            newlevel = self.last_level
 
            self.last_level = None
 

	
 
        self.set_var_rounded(newlevel)
 

	
light9/FlyingFader.py
Show inline comments
 
from Tix import *
 
from time import time,sleep
 
from time import time, sleep
 
from __future__ import division
 

	
 

	
 
class Mass:
 

	
 
    def __init__(self):
 
        self.x=0 # position
 
        self.xgoal=0 # position goal
 
        
 
        self.v=0 # velocity
 
        self.maxspeed = .8 # maximum speed, in position/second
 
        self.maxaccel = 3 # maximum acceleration, in position/second^2
 
        self.eps = .03 # epsilon - numbers within this much are considered the same
 
        self.x = 0  # position
 
        self.xgoal = 0  # position goal
 

	
 
        self._lastupdate=time()
 
        self._stopped=1
 
        self.v = 0  # velocity
 
        self.maxspeed = .8  # maximum speed, in position/second
 
        self.maxaccel = 3  # maximum acceleration, in position/second^2
 
        self.eps = .03  # epsilon - numbers within this much are considered the same
 

	
 
    def equal(self,a,b):
 
        return abs(a-b)<self.eps
 
        self._lastupdate = time()
 
        self._stopped = 1
 

	
 
    def equal(self, a, b):
 
        return abs(a - b) < self.eps
 

	
 
    def stop(self):
 
        self.v=0
 
        self.xgoal=self.x
 
        self._stopped=1
 
        
 
        self.v = 0
 
        self.xgoal = self.x
 
        self._stopped = 1
 

	
 
    def update(self):
 
        t0 = self._lastupdate
 
        tnow = time()
 
        self._lastupdate = tnow
 

	
 
        dt = tnow-t0
 
        dt = tnow - t0
 

	
 
        self.x += self.v*dt
 
        self.x += self.v * dt
 
        # hitting the ends stops the slider
 
        if self.x>1: self.v=max(self.v,0); self.x=1
 
        if self.x<0: self.v=min(self.v,0); self.x=0
 
            
 
        if self.equal(self.x,self.xgoal):
 
            self.x=self.xgoal # clean up value
 
        if self.x > 1:
 
            self.v = max(self.v, 0)
 
            self.x = 1
 
        if self.x < 0:
 
            self.v = min(self.v, 0)
 
            self.x = 0
 

	
 
        if self.equal(self.x, self.xgoal):
 
            self.x = self.xgoal  # clean up value
 
            self.stop()
 
            return
 
        
 
        self._stopped=0
 
        dir = (-1.0,1,0)[self.xgoal>self.x]
 

	
 
        if abs(self.xgoal-self.x) < abs(self.v*5*dt):
 
        self._stopped = 0
 
        dir = (-1.0, 1, 0)[self.xgoal > self.x]
 

	
 
        if abs(self.xgoal - self.x) < abs(self.v * 5 * dt):
 
            # apply the brakes on the last 5 steps
 
            dir *= -.5
 

	
 
        self.v += dir*self.maxaccel*dt # velocity changes with acceleration in the right direction
 
        self.v = min(max(self.v,-self.maxspeed),self.maxspeed) # clamp velocity
 
        self.v += dir * self.maxaccel * dt  # velocity changes with acceleration in the right direction
 
        self.v = min(max(self.v, -self.maxspeed),
 
                     self.maxspeed)  # clamp velocity
 

	
 
        #print "x=%+.03f v=%+.03f a=%+.03f %f" % (self.x,self.v,self.maxaccel,self.xgoal)
 

	
 
    def goto(self,newx):
 
        self.xgoal=newx
 
    def goto(self, newx):
 
        self.xgoal = newx
 

	
 
    def ismoving(self):
 
        return not self._stopped
 

	
 

	
 
class FlyingFader(Frame):
 
    def __init__(self, master, variable, label, fadedur=1.5, font=('Arial', 8), labelwidth=12,
 

	
 
    def __init__(self,
 
                 master,
 
                 variable,
 
                 label,
 
                 fadedur=1.5,
 
                 font=('Arial', 8),
 
                 labelwidth=12,
 
                 **kw):
 
        Frame.__init__(self, master)
 
        self.name = label
 
        self.variable = variable
 

	
 
        self.mass = Mass()
 
        
 
        self.config({'bd':1, 'relief':'raised'})
 
        scaleopts = {'variable' : variable, 'showvalue' : 0, 'from' : 1.0,
 
                     'to' : 0, 'res' : 0.001, 'width' : 20, 'length' : 200, 'orient':'vert'}
 

	
 
        self.config({'bd': 1, 'relief': 'raised'})
 
        scaleopts = {
 
            'variable': variable,
 
            'showvalue': 0,
 
            'from': 1.0,
 
            'to': 0,
 
            'res': 0.001,
 
            'width': 20,
 
            'length': 200,
 
            'orient': 'vert'
 
        }
 
        scaleopts.update(kw)
 
        if scaleopts['orient']=='vert':
 
            side1=TOP
 
            side2=BOTTOM
 
        if scaleopts['orient'] == 'vert':
 
            side1 = TOP
 
            side2 = BOTTOM
 
        else:
 
            side1=RIGHT
 
            side2=LEFT
 
        
 
            side1 = RIGHT
 
            side2 = LEFT
 

	
 
        self.scale = Scale(self, **scaleopts)
 
        self.vlabel = Label(self, text="0.0", width=6, font=font)
 
        self.label = Label(self, text=label, font=font, anchor='w',width=labelwidth) #wraplength=40, )
 
        self.label = Label(self,
 
                           text=label,
 
                           font=font,
 
                           anchor='w',
 
                           width=labelwidth)  #wraplength=40, )
 

	
 
        self.oldtrough = self.scale['troughcolor']
 

	
 
        self.scale.pack(side=side2, expand=1, fill=BOTH, anchor='c')
 
        self.vlabel.pack(side=side2, expand=0, fill=X)
 
        self.label.pack(side=side2, expand=0, fill=X)
 

	
 
        for k in range(1, 10):
 
            self.scale.bind("<Key-%d>" % k, 
 
                lambda evt, k=k: self.newfade(k / 10.0, evt))
 
            self.scale.bind(
 
                "<Key-%d>" % k, lambda evt, k=k: self.newfade(k / 10.0, evt))
 

	
 
        self.scale.bind("<Key-0>", lambda evt: self.newfade(1.0, evt))
 
        self.scale.bind("<grave>", lambda evt: self.newfade(0, evt))
 

	
 
        self.scale.bind("<1>", self.cancelfade)
 
        self.scale.bind("<2>", self.cancelfade)
 
        self.scale.bind("<3>", self.mousefade)
 

	
 
        self.trace_ret = self.variable.trace('w', self.updatelabel)
 
        self.bind("<Destroy>",self.ondestroy)
 
        self.bind("<Destroy>", self.ondestroy)
 

	
 
    def ondestroy(self,*ev):
 
        self.variable.trace_vdelete('w',self.trace_ret)
 
    def ondestroy(self, *ev):
 
        self.variable.trace_vdelete('w', self.trace_ret)
 

	
 
    def cancelfade(self, evt):
 
        self.fadegoal = self.variable.get()
 
        self.fadevel = self.fadeacc = 0
 

	
 
        self.scale['troughcolor'] = self.oldtrough
 

	
 
    def mousefade(self, evt):
 
        target = float(self.tk.call(self.scale, 'get', evt.x, evt.y))
 
        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)
 

	
 
        self.gofade()
 

	
 
@@ -136,27 +162,29 @@ class FlyingFader(Frame):
 
        self.mass.update()
 
        self.variable.set(self.mass.x)
 

	
 
        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:
 
            # self.scale.config(troughcolor='white')
 
            self.scale.config(troughcolor='yellow')
 

	
 
#        colorfade(self.scale, percent)
 
        self.after(30, self.gofade)
 

	
 
    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]:
 
#            self.vlabel['fg'] = 'red'
 
#        else:
 
#            self.vlabel['fg'] = 'blue'
 
@@ -164,28 +192,35 @@ class FlyingFader(Frame):
 
    def get(self):
 
        return self.scale.get()
 

	
 
    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()
light9/Patch.py
Show inline comments
 
@@ -5,41 +5,47 @@ from light9 import showconfig
 

	
 

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

	
 
    try:
 
        i = int(name)
 
        return i
 
    except ValueError:
 
        raise ValueError("Invalid channel name: %r" % name)
 

	
 

	
 
def get_channel_name(dmxnum):
 
    """if you pass a name, it will get normalized"""
 
    try:
 
        return reverse_patch[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 = {}
 
    reverse_patch = {}
 
    uri_map = {}
 
    uri_patch = {}
 
@@ -64,9 +70,9 @@ def reload_data():
 
                        reverse_patch[addrInt] = name
 
                        reverse_patch[addr] = name
 
                        norm_name = name
 
                    else:
 
                        reverse_patch[name] = norm_name
 

	
 

	
 
# importing patch will load initial data
 
reload_data()
 

	
light9/ascoltami/player.py
Show inline comments
 
#!/usr/bin/python
 

	
 
"""
 
alternate to the mpd music player, for ascoltami
 
"""
 
from __future__ import division
 
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).
 
        onEOS is an optional function to be called when we reach the
 
        end of a stream (for example, can be used to advance the song).
 
        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()
 

	
 
            self.lastWatchTime = t
 
        except:
 
@@ -55,27 +55,30 @@ class Player(object):
 
        bus.add_signal_watch()
 

	
 
        def onEos(*args):
 
            print "onEos", args
 
            if self.onEOS is not None:
 
                self.onEOS(self.getSong())
 

	
 
        bus.connect('message::eos', onEos)
 

	
 
        def onStreamStatus(bus, message):
 
            print "streamstatus", bus, message
 
            (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
 
            # output has an error, since that's otherwise kind of
 
            # mysterious to diagnose. I don't think this is exactly
 
            # working.
 
@@ -85,13 +88,13 @@ class Player(object):
 
                if self.onEOS is not None:
 
                    self.onEOS(self.getSong())
 
            if msg.type == mt.STREAM_STATUS:
 
                (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,
 
            Gst.SeekFlags.FLUSH | Gst.SeekFlags.ACCURATE | Gst.SeekFlags.SKIP,
 
            t * Gst.SECOND)
 
        if not isSeekable:
 
@@ -146,25 +149,32 @@ class Player(object):
 
        return dur / Gst.SECOND
 

	
 
    def states(self):
 
        """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)
 

	
 
    def isAutostopped(self):
 
        """
 
        are we stopped at the autostop time?
 
        """
 
        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)
 

	
 
    def setupAutostop(self):
 
        dur = self.duration()
light9/ascoltami/playlist.py
Show inline comments
 
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."""
 
        try:
 
            currentIndex = self.songs.index(currentSong)
 
        except IndexError:
 
@@ -27,20 +30,20 @@ class Playlist(object):
 

	
 
        return nextSong
 

	
 
    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")
 
        # maybe that function should be moved to this method
 

	
 
    @classmethod
light9/ascoltami/webapp.py
Show inline comments
 
@@ -6,58 +6,67 @@ from light9.showconfig import getSongsFr
 
from rdflib import URIRef
 
from web.contrib.template import render_genshi
 
render = render_genshi([sibpath(__file__, ".")], auto_reload=True)
 

	
 
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
 
        self.set_header("Content-Type", "application/json")
 

	
 
        if player.isAutostopped():
 
            nextAction = 'finish'
 
        elif player.isPlaying():
 
            nextAction = 'disabled'
 
        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):
 
        """
 
        post a json object with {pause: true} or {resume: true} if you
 
        want those actions. Use {t: <seconds>} to seek, optionally
 
@@ -71,71 +80,87 @@ class timeResource(PrettyErrorHandler,cy
 
            player.resume()
 
        if 't' in params:
 
            player.seek(params['t'])
 
        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
 

	
 
        data = json.loads(self.request.body)
 
        if player.isPlaying():
 
            player.pause()
 
        else:
 
            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.
 
        """
 
        graph, player = self.settings.app.graph, self.settings.app.player
 

	
 
        if player.isAutostopped():
 
            player.resume()
 
        elif player.isPlaying():
 
            pass
 
        else:
 
            player.resume()
 
            
 

	
 
        self.set_header("Content-Type", "text/plain")
 
        self.write("ok")
 

	
 

	
 
def makeWebApp(app):
 
    return cyclone.web.Application(handlers=[
 
        (r"/", root),
 
        (r"/time", timeResource),
 
        (r"/song", songResource),
 
        (r"/songs", songs),
 
        (r"/seekPlayOrPause", seekPlayOrPause),
 
        (r"/output", output),
 
        (r"/go", goButton),
 
        ], app=app)
 

	
 
    ],
 
                                   app=app)
light9/chase.py
Show inline comments
 
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))
 
    period = (offset + ontime) * len(names)
 
    outputs = {}
 
    for index, name in enumerate(names):
 
@@ -23,15 +29,20 @@ def chase(t, ontime=0.5, offset=0.2, onv
 
        if name in outputs:
 
            outputs[name] = combiner(value, outputs[name])
 
        else:
 
            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()
 
        print "%.2f\t%s" % (x, ' '.join([str(x) for x in output]))
light9/clientsession.py
Show inline comments
 
@@ -3,15 +3,18 @@ some clients will support the concept of
 
multiple instances of that client separate
 
"""
 
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='')))
light9/collector/collector.py
Show inline comments
 
@@ -13,12 +13,13 @@ from light9.collector.output import Outp
 

	
 
ClientType = TypeVar('ClientType')
 
ClientSessionType = TypeVar('ClientSessionType')
 

	
 
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
 
       (device, outputattr) : (output, index)
 
    that explains which output index to set for any device update.
 
    """
 
@@ -43,35 +44,39 @@ def outputMap(graph, outputs):
 
                outputAttr = graph.value(row, L9['outputAttr'])
 
                offset = int(graph.value(row, L9['dmxOffset']).toPython())
 
                index = dmxBase + offset - 1
 
                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
 
        self.outputs = outputs
 
        self.listeners = listeners
 
        self.clientTimeoutSec = clientTimeoutSec
 
        self.initTime = time.time()
 
        self.allDevices = set()
 

	
 
        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)
 
                self.deviceType[dev] = dc
 

	
 
                for remap in self.graph.objects(dev, L9['outputAttrRange']):
 
@@ -90,51 +95,54 @@ class Collector(Generic[ClientType, Clie
 
            log.info('forgetting stale client %r', c)
 
            del self.lastRequest[c]
 

	
 
    # 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])
 
            else:
 
                out[(d, da)] = v
 
        return out
 

	
 
    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:
 
                    start, end = self.remapOut[(device, deviceAttr)]
 
                    value = Literal(start + float(value) * (end - start))
 

	
 
                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,
 
                # not going to 0.
 
                if deviceAttr in [L9['rx'], L9['ry'], L9['zoom'], L9['focus']]:
 
                    self.stickyAttrs[(device, deviceAttr)] = value
 

	
 
        # e.g. don't let an unspecified rotation go to 0
 
        for (d, da), v in self.stickyAttrs.iteritems():
 
            daDict = deviceAttrs.setdefault(d, {})
 
            if da not in daDict:
 
                daDict[da] = v
 
                    
 

	
 
        return deviceAttrs
 

	
 
    def setAttrs(self, client, clientSession, settings, sendTime):
 
        """
 
        settings is a list of (device, attr, value). These attrs are
 
        device attrs. We resolve conflicting values, process them into
 
@@ -153,42 +161,44 @@ class Collector(Generic[ClientType, Clie
 
        self._forgetStaleClients(now)
 

	
 
        uniqueSettings = self.resolvedSettingsDict(settings)
 
        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]
 
            except KeyError:
 
                log.warn("request for output to unconfigured device %s" % d)
 
                continue
 
            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
 

	
 
        for device, attrs in outputAttrs.iteritems():
 
            for outputAttr, value in attrs.iteritems():
 
                self.setAttr(device, outputAttr, value, pendingOut)
 

	
 
        dt1 = 1000 * (time.time() - now)
 
        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)]
 
        outList = pendingOut[output]
 
        setListElem(outList, index, value, combine=max)
 

	
light9/collector/collector_client.py
Show inline comments
 
from __future__ import division
 
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()
 
    msg = toCollectorJson(client, session, settings)
 

	
 
    if useZmq:
 
        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
light9/collector/collector_test.py
Show inline comments
 
@@ -32,15 +32,17 @@ THEATER = '''
 
            [ :outputAttr :red;   :dmxOffset 1 ],
 
            [ :outputAttr :green; :dmxOffset 2 ],
 
            [ :outputAttr :blue;  :dmxOffset 3 ] .
 

	
 
'''
 

	
 
t0 = 0 # time
 
t0 = 0  # time
 

	
 

	
 
class MockOutput(object):
 

	
 
    def __init__(self, uri, connections):
 
        self.connections = connections
 
        self.updates = []
 
        self.uri = uri
 
        self.numChannels = 4
 

	
 
@@ -50,44 +52,55 @@ class MockOutput(object):
 
    def update(self, values):
 
        self.updates.append(values)
 

	
 
    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 .
 
        '''), [out0])
 

	
 

	
 
class TestCollector(unittest.TestCase):
 

	
 
    def setUp(self):
 
        self.config = MockSyncedGraph(PREFIX + THEATER + '''
 

	
 
        dev:colorStrip a :Device, :ChauvetColorStrip;
 
          :dmxUniverse udmx:; :dmxBase 1;
 
          :red dev:colorStripRed;
 
@@ -98,16 +111,14 @@ class TestCollector(unittest.TestCase):
 
        dev:inst1 a :Device, :SimpleDimmer;
 
          :dmxUniverse dmx0:; :dmxBase 1;
 
          :level dev:inst1Brightness .
 
        ''')
 

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

	
 
        c.setAttrs('client', 'sess1',
 
                   [(DEV['colorStrip'], L9['color'], '#00ff00')], t0)
 
@@ -120,15 +131,15 @@ class TestCollector(unittest.TestCase):
 

	
 
        c.setAttrs('client1', 'sess1',
 
                   [(DEV['colorStrip'], L9['color'], '#ff0000')], t0)
 
        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)
 

	
 
    def testClientOnSameOutputIsRememberedOverCalls(self):
 
        c = Collector(self.config, outputs=[self.dmx0, self.udmx])
 

	
 
@@ -136,42 +147,42 @@ class TestCollector(unittest.TestCase):
 
                   [(DEV['colorStrip'], L9['color'], '#080000')], t0)
 
        c.setAttrs('client2', 'sess1',
 
                   [(DEV['colorStrip'], L9['color'], '#060000')], t0)
 
        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 + '''
 

	
 
        dev:colorStrip a :Device, :ChauvetColorStrip;
 
          :dmxUniverse udmx:; :dmxBase 1;
 
@@ -180,67 +191,64 @@ class TestCollector(unittest.TestCase):
 
          :blue dev:colorStripBlue;
 
          :mode dev:colorStripMode .
 

	
 
        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:
 
            c = Collector(self.config, outputs=[self.dmx0, self.udmx])
 
            c.setAttrs('cli1', 'sess1', [(DEV['inst1'], L9['brightness'], .5)],
 
                       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),
 
        ], t0)
 
        self.assertEqual([[127, 0, 0, 0], 'flush'], self.dmx0.updates)
 

	
 
        c.setAttrs('client1', 'sess1', [
 
            (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)

Changeset was too big and was cut off... Show full diff anyway

0 comments (0 inline, 0 general)