Changeset - 5bcb950024af
bin/bumppad
Show inline comments
 
@@ -10,79 +10,80 @@ from light9.TLUtility import make_attrib
 
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'),
 
        ]:
 

	
 
            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.master.after_idle(self.output)
 

	
 
    def output(self):
 
        dmx = sub_maxes(*[s * l for s, l in list(self.levs.items())]).get_dmx_list()
 
        dmx = sub_maxes(*[s * l
 
                          for s, l in list(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)
 

	
 
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/collector
Show inline comments
 
#!bin/python
 
"""
 
Collector receives device attrs from multiple senders, combines
 
them, and sends output attrs to hardware. The combining part has
 
custom code for some attributes.
 

	
 
Input can be over http or zmq.
 
"""
 

	
 

	
 
from run_local import log
 

	
 
from rdflib import URIRef, Literal
 
from twisted.internet import reactor, utils
 
from txzmq import ZmqEndpoint, ZmqFactory, ZmqPullConnection
 
import json
 
import logging
 
import optparse
 
import time
 
import traceback
 
import cyclone.web, cyclone.websocket
 
from greplin import scales
 

	
 
from lib.cycloneerr import PrettyErrorHandler
 
from light9.collector.output import EnttecDmx, Udmx, DummyOutput
 
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, str) 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'))
 

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

	
 
"""
 

	
 

	
 
import sys
 
import imp
 
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 urllib.parse import parse_qsl
 
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.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
 
from light9.editchoicegtk import EditChoice, Local
 
from light9.gtkpyconsole import togglePyConsole
 
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 = [
 
@@ -368,97 +367,98 @@ class Main(object):
 
        # there's a hang after this, maybe in sem_wait in two
 
        # threads. I don't know whose they are.
 
        # This fix affects profilers who want to write output at the end.
 
        os.kill(os.getpid(), signal.SIGKILL)
 

	
 
    def onCollapseAll(self, *args):
 
        self.curvesetView.collapseAll()
 

	
 
    def onCollapseNone(self, *args):
 
        self.curvesetView.collapseNone()
 

	
 
    def onDelete(self, *args):
 
        self.curvesetView.onDelete()
 

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

	
 
    def onZoomAll(self, item):
 
        dispatcher.send("show all")
 

	
 
    def onPlayPause(self, item):
 
        # since the X coord in a curveview affects the handling, one
 
        # of them may be able to pick this up
 
        results = dispatcher.send("onPlayPause")
 
        times = [t for listener, t in results if t is not None]
 
        self.music.playOrPause(t=times[0] if times else None)
 

	
 
    def onSave(self, *args):
 
        # only doing curves still. I hope to eliminate all this.
 
        log.info("saving curves")
 
        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 list(levels.items())[:2] if v > 0
 
                    "%s:%.2f" % (n, v) for n, v in list(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: setattr(self, 'lastSeenInputTime', val),
 
                           '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',
 
            ]
 
        ]
 

	
 
        if (not hasattr(self, 'curvesetView') or
 
                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()]
 
            try:
 
                linecache.clearcache()
 
                imp.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)
bin/dmxserver
Show inline comments
 
#!bin/python
 
"""
 
Replaced by bin/collector
 

	
 

	
 
this is the only process to talk to the dmx hardware. other clients
 
can connect to this server and present dmx output, and this server
 
will max ('pile-on') all the client requests.
 

	
 
this server has a level display which is the final set of values that
 
goes to the hardware.
 

	
 
clients shall connect to the xmlrpc server and send:
 

	
 
  their PID (or some other cookie)
 

	
 
  a length-n list of 0..1 levels which will represent the channel
 
    values for the n first dmx channels.
 

	
 
server is port 8030; xmlrpc method is called outputlevels(pid,levellist).
 

	
 
todo:
 
  save dmx on quit and restore on restart
 
  if parport fails, run in dummy mode (and make an option for that too)
 
"""
 

	
 

	
 
from twisted.internet import reactor
 
from twisted.web import xmlrpc, server
 
import sys, time, os
 
from optparse import OptionParser
 
import run_local
 
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),
 
            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])
 
@@ -82,155 +81,155 @@ class XMLRPCServe(xmlrpc.XMLRPC):
 

	
 
    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
 

	
 
        # desired seconds between sendlevels() calls
 
        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):
 
            self.parportdmx.godummy()
 
        else:
 
            self.parportdmx.golive()
 

	
 
        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)
 

	
 
        now = time.time()
 
        cids = list(self.lastseen.keys())
 
        for cid in cids:
 
            lastseen = self.lastseen[cid]
 
            if lastseen < now - purge_age:
 
                print(("forgetting client %s (no activity for %s sec)" %
 
                      (cid, purge_age)))
 
                       (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)
 

	
 
        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
 
                self.printlevels()
 
                self.lastshownlevels = self.combinedlevels[:]
 
            else:
 
                self.num_unshown_updates += 1
 

	
 
        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()
 
            self.sendlevels_dmx()
 

	
 
        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
 
            for clientlist in list(self.clientlevels.values()):
 
                if len(clientlist) > chan:
 
                    # clamp client levels to 0..1
 
                    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 list(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.updatefreq.update()
 

	
 
    def xmlrpc_echo(self, x):
 
        return x
 

	
 
    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
 
        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."""
 
        # if this is still too slow, it might help to return a single
 
        # pickled string
 
        self.trackClientFreq(cid)
 
        trunc = self.combinedlevels[:]
 
        i = len(trunc) - 1
 
        if i < 0:
 
            return []
 
        while trunc[i] == 0 and i >= 0:
 
            i -= 1
 
        if i < 0:
 
            return []
 
        trunc = trunc[:i + 1]
 
        return trunc
bin/effecteval
Show inline comments
 
#!bin/python
 

	
 

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

	
 

	
 
@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):
bin/keyboardcomposer
Show inline comments
 
#!bin/python
 

	
 

	
 
from run_local import log
 
import cgi, time, logging
 
from optparse import OptionParser
 
import webcolors, colorsys
 
from louie import dispatcher
 
from twisted.internet import reactor, tksupport
 
from twisted.web import resource
 
from rdflib import URIRef, Literal
 
import tkinter.tix as tk
 

	
 
from light9.Fadable import Fadable
 
from light9.subclient import SubClient
 
from light9 import showconfig, networking, prof
 
from light9.uihelpers import toplevelat
 
from light9.namespaces import L9, RDF, RDFS
 
from light9.tkdnd import initTkdnd, dragSourceRegister, dropTargetRegister
 
from light9 import clientsession
 
from rdfdb.syncedgraph import SyncedGraph
 
from light9.effect.sequencer import CodeWatcher
 
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
 
import imp
 

	
 
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,
 
@@ -456,99 +455,98 @@ class KeyboardComposer(tk.Frame, SubClie
 
        v = round(127 * level)
 
        chan = "slider%s" % hwCol
 

	
 
        # workaround for some rounding issue, where we receive one
 
        # value and then decide to send back a value that's one step
 
        # lower.  -5 is a fallback for having no last value.  hopefully
 
        # we won't really see it
 
        if abs(v - self.sliders.lastValue.get(chan, -5)) <= 1:
 
            return
 
        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=['*'],
 
                           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)
 

	
 
    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 list(self.subbox.items())
 
        ])
 
        return dict([(uri, box.slider_var.get())
 
                     for uri, box in list(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'])
 
            strength = _graph.value(setting, L9['level'])
 
            if strength:
 
                now = time.time()
 
                out, report = self.effectEval[effect].outputFromEffect(
 
                    [(L9['strength'], strength)],
 
                    songTime=now,
 
                    # should be counting from when you bumped up from 0
 
                    noteTime=now)
 
                outputSettings.append(out)
 

	
 
        return DeviceSettings.fromList(_graph, outputSettings)
 

	
 
    def save_current_stage(self, subname):
 
        log.info("saving current levels as %s", subname)
 
        with self.graph.currentState() as g:
 
            ds = self.get_output_settings(_graph=g)
 
        effect = L9['effect/%s' % subname]
 
        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 list(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
bin/lightsim
Show inline comments
 
#!bin/python
 

	
 

	
 
import run_local
 
import sys, logging
 

	
 
sys.path.append("lib")
 
import qt4reactor
 
qt4reactor.install()
 

	
 
from twisted.internet import reactor
 
from twisted.internet.task import LoopingCall
 
from twisted.web.xmlrpc import Proxy
 
from louie import dispatcher
 
from PyQt4.QtGui import QWidget, QLabel, QVBoxLayout, QHBoxLayout, QMainWindow
 
from OpenGL.GL import *
 
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")
 
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
 
        for i, lev in enumerate(dmxLevels):
bin/paintserver
Show inline comments
 
#!bin/python
 

	
 

	
 
from run_local import log
 
import json
 
from twisted.internet import reactor
 
from light9.greplin_cyclone import StatsForCyclone
 
from rdfdb.syncedgraph import SyncedGraph
 
from light9 import networking, showconfig
 
from greplin import scales
 
import optparse, sys, logging
 
import cyclone.web
 
from rdflib import URIRef
 
from light9 import clientsession
 
import light9.paint.solve
 
from lib.cycloneerr import PrettyErrorHandler
 
from light9.namespaces import RDF, L9, DEV
 
import imp
 

	
 

	
 
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'])
 
            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,
 
            }))
 

	
 
    def reloadSolver(self):
 
        imp.reload(light9.paint.solve)
 
        self.settings.solver = light9.paint.solve.Solver(self.settings.graph)
 
        self.settings.solver.loadSamples()
 

	
 

	
 
class BestMatches(PrettyErrorHandler, cyclone.web.RequestHandler):
bin/subcomposer
Show inline comments
 
#!bin/python
 
"""
 
subcomposer
 
  session
 
  observable(currentSub), a Submaster which tracks the graph
 

	
 
    EditChoice widget
 
      can change currentSub to another sub
 

	
 
    Levelbox widget
 
      watch observable(currentSub) for a new sub, and also watch currentSub for edits to push to the OneLevel widgets
 

	
 
        OneLevel widget
 
          UI edits are caught here and go all the way back to currentSub
 

	
 

	
 
"""
 

	
 

	
 
from run_local import log
 
import time, logging
 

	
 
log.setLevel(logging.DEBUG)
 

	
 
from optparse import OptionParser
 
import logging, urllib.request, urllib.parse, urllib.error
 
import tkinter as tk
 
import louie as dispatcher
 
from twisted.internet import reactor, tksupport, task
 
from rdflib import URIRef, RDF, RDFS, Literal
 

	
 
from light9.dmxchanedit import Levelbox
 
from light9 import dmxclient, Submaster, prof, showconfig, networking
 
from light9.Patch import get_channel_name
 
from light9.uihelpers import toplevelat
 
from rdfdb.syncedgraph import SyncedGraph
 
from light9 import clientsession
 
from light9.tkdnd import initTkdnd
 
from light9.namespaces import L9
 
from rdfdb.patch import Patch
 
from light9.observable import Observable
 
from light9.editchoice import EditChoice, Local
 
from light9.subcomposer import subcomposerweb
 

	
 

	
 
class Subcomposer(tk.Frame):
 
    """
 
    <session> l9:currentSub ?sub is the URI of the sub we're tied to for displaying and
 
    editing. If we don't have a currentSub, then we're actually
 
    editing a session-local sub called <session> l9:currentSub <sessionLocalSub>
 

	
 
    I'm not sure that Locals should even be PersistentSubmaster with
 
    uri and graph storage, but I think that way is making fewer
 
    special cases.
 

	
 
    Contains an EditChoice widget
 

	
 
    Dependencies:
 

	
 
      graph (?session :currentSub ?s) -> self.currentSub
 
      self.currentSub -> graph
 
      self.currentSub -> self._currentChoice (which might be Local)
 
      self._currentChoice (which might be Local) -> self.currentSub
 

	
 
      inside the current sub:
 
      graph -> Submaster levels (handled in Submaster)
 
      Submaster levels -> OneLevel widget
 
@@ -116,98 +115,98 @@ class Subcomposer(tk.Frame):
 
        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.parse.quote(newName, safe=''))
 
        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)
 

	
 
    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]
 
        # 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()))
 
            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)
 
                return
 
            self.sendupdate()
 
            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)
 
                else:
 
                    # 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)
 
                self.currentSub(newSub)
bin/tracker
Show inline comments
 
#!/usr/bin/python
 

	
 

	
 
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 pygame.rect import Rect
 
from xmlnodebase import xmlnodeclass, collectiveelement, xmldocfile
 
from dispatch import dispatcher
 

	
 
import dmxclient
 

	
 
import tkinter as tk
 

	
 
defaultfont = "arial 8"
 

	
 

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

	
 

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

	
 

	
 
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):
 
        """light/sub name"""
 
        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))
 

	
 
    def falloff(self, dist=None):
light9/collector/collector.py
Show inline comments
 

	
 
import time
 
import logging
 
from rdflib import Literal
 
from light9.namespaces import L9, RDF
 
from light9.collector.output import setListElem
 
from light9.collector.device import toOutputAttrs, resolve
 

	
 
# types only
 
from rdflib import Graph, URIRef
 
from typing import List, Dict, Tuple, Any, TypeVar, Generic
 
from light9.collector.output import Output
 

	
 
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.
 
    """
 
    ret = {}
 

	
 
    outputByUri = {}  # universeUri : output
 
    for out in outputs:
 
        outputByUri[out.uri] = out
 

	
 
    for dc in graph.subjects(RDF.type, L9['DeviceClass']):
 
        log.info('mapping DeviceClass %s', dc)
 
        for dev in graph.subjects(RDF.type, dc):
 
            log.info('  mapping device %s', dev)
 
            universe = graph.value(dev, L9['dmxUniverse'])
 
            try:
 
                output = outputByUri[universe]
 
            except Exception:
 
                log.warn('dev %r :dmxUniverse %r', dev, universe)
 
                raise
 
            dmxBase = int(graph.value(dev, L9['dmxBase']).toPython())
 
            for row in graph.objects(dc, L9['attr']):
 
                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
light9/collector/collector_client.py
Show inline comments
 

	
 
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 treq
 

	
 
log = logging.getLogger('coll_client')
 

	
 
_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(),
 
    })
 

	
 

	
 
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)
light9/collector/device.py
Show inline comments
 

	
 
import logging
 
import math
 
from light9.namespaces import L9, RDF, DEV
 
from rdflib import Literal
 
from webcolors import hex_to_rgb, rgb_to_hex
 
from colormath.color_objects import sRGBColor, CMYColor
 
import colormath.color_conversions
 

	
 
log = logging.getLogger('device')
 

	
 

	
 
class Device(object):
 
    pass
 

	
 

	
 
class ChauvetColorStrip(Device):
 
    """
 
     device attrs:
 
       color
 
    """
 

	
 

	
 
class Mini15(Device):
 
    """
 
    plan:
 

	
 
      device attrs
 
        rx, ry
 
        color
 
        gobo
 
        goboShake
 
        imageAim (configured with a file of calibration data)
 
    """
 

	
 

	
 
def clamp255(x):
 
    return min(255, max(0, x))
 

	
 

	
 
def _8bit(f):
 
    if not isinstance(f, (int, float)):
 
        raise TypeError(repr(f))
 
    return clamp255(int(f * 255))
 

	
 

	
 
def resolve(deviceType, deviceAttr, values):
 
    """
 
    return one value to use for this attr, given a set of them that
light9/collector/output.py
Show inline comments
 

	
 
from rdflib import URIRef
 
import sys
 
import time
 
import usb.core
 
import logging
 
from twisted.internet import task, threads, reactor
 
from greplin import scales
 
log = logging.getLogger('output')
 

	
 

	
 
# eliminate this: lists are always padded now
 
def setListElem(outList, index, value, fill=0, combine=lambda old, new: new):
 
    if len(outList) < index:
 
        outList.extend([fill] * (index - len(outList)))
 
    if len(outList) <= index:
 
        outList.append(value)
 
    else:
 
        outList[index] = combine(outList[index], value)
 

	
 

	
 
class Output(object):
 
    """
 
    send an array of values to some output device. Call update as
 
    often as you want- the result will be sent as soon as possible,
 
    and with repeats as needed to outlast hardware timeouts.
 
    """
 
    uri = None  # type: URIRef
 
    numChannels = None  # type: int
 

	
 
    def __init__(self):
 
        raise NotImplementedError
 

	
 
    def allConnections(self):
 
        """
 
        sequence of (index, uri) for the uris we can output, and which
 
        index in 'values' to use for them
 
        """
 
        raise NotImplementedError
 

	
 
    def update(self, values):
 
        """
 
        output takes a flattened list of values, maybe dmx channels, or
 
        pin numbers, etc
 
        """
 
        raise NotImplementedError
 

	
 
    def flush(self):
 
        """
light9/curvecalc/client.py
Show inline comments
 
"""
 
client code for talking to curvecalc
 
"""
 
import cyclone.httpclient
 
from light9 import networking
 
import urllib.request, urllib.parse, urllib.error
 
from run_local import log
 

	
 

	
 
def sendLiveInputPoint(curve, value):
 
    f = cyclone.httpclient.fetch(networking.curveCalc.path('liveInputPoint'),
 
                                 method='POST',
 
                                 timeout=1,
 
                                 postdata=urllib.parse.urlencode({
 
                                     'curve': curve,
 
                                     'value': str(value),
 
                                     'curve':
 
                                     curve,
 
                                     'value':
 
                                     str(value),
 
                                 }))
 

	
 
    @f.addCallback
 
    def cb(result):
 
        if result.code // 100 != 2:
 
            raise ValueError("curveCalc said %s: %s", result.code, result.body)
 

	
 
    return f
light9/curvecalc/curve.py
Show inline comments
 

	
 
import glob, time, logging, ast, os
 
from bisect import bisect_left, bisect
 
import louie as dispatcher
 
from twisted.internet import reactor
 
from rdflib import Literal
 
from light9 import showconfig
 
from light9.namespaces import L9, RDF, RDFS
 
from rdfdb.patch import Patch
 

	
 
log = logging.getLogger()
 
# todo: move to config, consolidate with ascoltami, musicPad, etc
 
introPad = 4
 
postPad = 4
 

	
 

	
 
class Curve(object):
 
    """curve does not know its name. see Curveset"""
 

	
 
    def __init__(self, uri, pointsStorage='graph'):
 
        self.uri = uri
 
        self.pointsStorage = pointsStorage
 
        self.points = []  # x-sorted list of (x,y)
 
        self._muted = False
 

	
 
    def __repr__(self):
 
        return "<%s %s (%s points)>" % (self.__class__.__name__, self.uri,
 
                                        len(self.points))
 

	
 
    def muted():
 
        doc = "Whether to currently send levels (boolean, obviously)"
 

	
 
        def fget(self):
 
            return self._muted
 

	
 
        def fset(self, val):
 
            self._muted = val
 
            dispatcher.send('mute changed', sender=self)
 

	
 
        return locals()
 

	
 
    muted = property(**muted())
 

	
 
    def toggleMute(self):
 
        self.muted = not self.muted
 

	
 
    def load(self, filename):
 
        self.points[:] = []
 
        for line in file(filename):
light9/curvecalc/curveview.py
Show inline comments
 

	
 
import math, logging, traceback
 
from gi.repository import Gtk
 
from gi.repository import Gdk
 
from gi.repository import GooCanvas
 
import louie as dispatcher
 
from rdflib import Literal
 
from twisted.internet import reactor
 
from light9.curvecalc.zoomcontrol import RegionZoom
 
from light9.curvecalc import cursors
 
from light9.curvecalc.curve import introPad, postPad
 
from lib.goocanvas_compat import Points, polyline_new_line
 
import imp
 

	
 
log = logging.getLogger()
 
print("curveview.py toplevel")
 

	
 

	
 
def vlen(v):
 
    return math.sqrt(v[0] * v[0] + v[1] * v[1])
 

	
 

	
 
def angle_between(base, p0, p1):
 
    p0 = p0[0] - base[0], p0[1] - base[1]
 
    p1 = p1[0] - base[0], p1[1] - base[1]
 
    p0 = [x / vlen(p0) for x in p0]
 
    p1 = [x / vlen(p1) for x in p1]
 
    dot = p0[0] * p1[0] + p0[1] * p1[1]
 
    dot = max(-1, min(1, dot))
 
    return math.degrees(math.acos(dot))
 

	
 

	
 
class Sketch:
 
    """a sketch motion on a curveview, with temporary points while you
 
    draw, and simplification when you release"""
 

	
 
    def __init__(self, curveview, ev):
 
        self.curveview = curveview
 
        self.pts = []
 
        self.last_x = None
 

	
 
    def motion(self, ev):
 
        p = self.curveview.world_from_screen(ev.x, ev.y)
 
        p = p[0], max(0, min(1, p[1]))
 
        if self.last_x is not None and abs(ev.x - self.last_x) < 4:
 
            return
 
        self.last_x = ev.x
 
        self.pts.append(p)
 
        self.curveview.add_point(p)
 
@@ -510,98 +509,98 @@ class Curveview(object):
 
        return True
 

	
 
    def playPause(self):
 
        """
 
        user has pressed ctrl-p over a curve view, possibly this
 
        one. Returns the time under the mouse if we know it, or else
 
        None
 

	
 
        todo: there should be a faint timecursor line under the mouse
 
        so it's more obvious that we use that time for some
 
        events. Rt-click should include Ctrl+P as 'play/pause from
 
        here'
 
        """
 
        # maybe self.canvas.get_pointer would be ok for this? i didn't try it
 
        if self.entered and hasattr(self, 'lastMouseX'):
 
            t = self.world_from_screen(self.lastMouseX, 0)[0]
 
            return t
 
        return None
 

	
 
    def goLive(self):
 
        """this is for startup performance only, since the curves were
 
        getting redrawn many times. """
 
        self.redrawsEnabled = True
 
        self.update_curve()
 

	
 
    def knob_in(self, curve, value):
 
        """user turned a hardware knob, which edits the point to the
 
        left of the current time"""
 
        if curve != self.curve:
 
            return
 
        idx = self.curve.index_before(self.current_time())
 
        if idx is not None:
 
            pos = self.curve.points[idx]
 
            self.curve.set_points([(idx, (pos[0], value))])
 

	
 
    def slider_in(self, curve, value=None):
 
        """user pushed on a slider. make a new key.  if value is None,
 
        the value will be the same as the last."""
 
        if curve != self.curve:
 
            return
 

	
 
        if value is None:
 
            value = self.curve.eval(self.current_time())
 

	
 
        self.curve.insert_pt((self.current_time(), value))
 

	
 
    def print_state(self, msg=""):
 
        if 0:
 
            print("%s: dragging_dots=%s selecting=%s" % (
 
                msg, self.dragging_dots, self.selecting))
 
            print("%s: dragging_dots=%s selecting=%s" %
 
                  (msg, self.dragging_dots, self.selecting))
 

	
 
    def select_points(self, pts):
 
        """set selection to the given point values (tuples, not indices)"""
 
        idxs = []
 
        for p in pts:
 
            idxs.append(self.curve.points.index(p))
 
        self.select_indices(idxs)
 

	
 
    def select_indices(self, idxs):
 
        """set selection to these point indices. This is the only
 
        writer to self.selected_points"""
 
        self.selected_points = idxs
 
        self.highlight_selected_dots()
 
        if self.selected_points and not self.selectManip:
 
            self.selectManip = SelectManip(
 
                self.canvas.get_root_item(),
 
                getSelectedIndices=lambda: sorted(self.selected_points),
 
                getWorldPoint=lambda i: self.curve.points[i],
 
                getScreenPoint=lambda i: self.screen_from_world(self.curve.
 
                                                                points[i]),
 
                getWorldTime=lambda x: self.world_from_screen(x, 0)[0],
 
                getWorldValue=lambda y: self.world_from_screen(0, y)[1],
 
                getCanvasHeight=lambda: self.canvas.props.y2,
 
                setPoints=self.setPoints,
 
                getDragRange=self.getDragRange,
 
            )
 
        if not self.selected_points and self.selectManip:
 
            self.selectManip.destroy()
 
            self.selectManip = None
 

	
 
        self.selectionChanged()
 

	
 
    def getDragRange(self, idxs):
 
        """
 
        if you're dragging these points, what's the most time you can move
 
        left and right before colliding (exactly) with another
 
        point
 
        """
 
        maxLeft = maxRight = 99999
 
        cp = self.curve.points
 
        for i in idxs:
 
            nextStatic = i
 
            while nextStatic >= 0 and nextStatic in idxs:
 
                nextStatic -= 1
 
            if nextStatic >= 0:
 
                maxLeft = min(maxLeft, cp[i][0] - cp[nextStatic][0])
 

	
 
            nextStatic = i
light9/curvecalc/zoomcontrol.py
Show inline comments
 

	
 
from gi.repository import Gtk
 
from gi.repository import GObject
 
from gi.repository import GooCanvas
 
import louie as dispatcher
 
from light9.curvecalc import cursors
 
from lib.goocanvas_compat import Points, polyline_new_line
 
from twisted.internet import reactor
 

	
 

	
 
class ZoomControl(object):
 
    """
 
    please pack .widget
 
    """
 

	
 
    mintime = 0
 

	
 
    def maxtime():
 
        doc = "seconds at the right edge of the bar"
 

	
 
        def fget(self):
 
            return self._maxtime
 

	
 
        def fset(self, value):
 
            self._maxtime = value
 
            self.redrawzoom()
 

	
 
        return locals()
 

	
 
    maxtime = property(**maxtime())
 

	
 
    _end = _start = 0
 

	
 
    def start():
 

	
 
        def fget(self):
 
            return self._start
 

	
 
        def fset(self, v):
 
            v = max(self.mintime, v)
 
            # don't protect for start<end since zooming sometimes sets
 
            # start temporarily after end
 
            self._start = v
 

	
 
        return locals()
 

	
 
    start = property(**start())
 

	
 
    def end():
light9/effect/effecteval.py
Show inline comments
 

	
 
from rdflib import URIRef, Literal
 
from light9.namespaces import L9, RDF, DEV
 
from webcolors import rgb_to_hex, hex_to_rgb
 
from colorsys import hsv_to_rgb
 
from decimal import Decimal
 
import math
 
import traceback
 
from noise import pnoise1
 
import logging
 
import time
 
from light9.effect.settings import DeviceSettings
 
from light9.effect.scale import scale
 
import random
 
random.seed(0)
 
print("reload effecteval")
 

	
 
log = logging.getLogger('effecteval')
 

	
 

	
 
def literalColor(rnorm, gnorm, bnorm):
 
    return Literal(
 
        rgb_to_hex([int(rnorm * 255),
 
                    int(gnorm * 255),
 
                    int(bnorm * 255)]))
 

	
 

	
 
def literalColorHsv(h, s, v):
 
    return literalColor(*hsv_to_rgb(h, s, v))
 

	
 

	
 
def nsin(x):
 
    return (math.sin(x * (2 * math.pi)) + 1) / 2
 

	
 

	
 
def ncos(x):
 
    return (math.cos(x * (2 * math.pi)) + 1) / 2
 

	
 

	
 
def nsquare(t, on=.5):
 
    return (t % 1.0) < on
 

	
 

	
 
def lerp(a, b, t):
 
    return a + (b - a) * t
 

	
 

	
 
def noise(t):
 
    return pnoise1(t % 1000.0, 2)
light9/effect/scale.py
Show inline comments
 

	
 
from rdflib import Literal
 
from decimal import Decimal
 
from webcolors import rgb_to_hex, hex_to_rgb
 

	
 

	
 
def scale(value, strength):
 
    if isinstance(value, Literal):
 
        value = value.toPython()
 

	
 
    if isinstance(value, Decimal):
 
        value = float(value)
 

	
 
    if isinstance(value, str):
 
        if value[0] == '#':
 
            if strength == '#ffffff':
 
                return value
 
            r, g, b = hex_to_rgb(value)
 
            if isinstance(strength, Literal):
 
                strength = strength.toPython()
 
            if isinstance(strength, str):
 
                sr, sg, sb = [v / 255 for v in hex_to_rgb(strength)]
 
            else:
 
                sr = sg = sb = strength
 
            return rgb_to_hex([int(r * sr), int(g * sg), int(b * sb)])
 
    elif isinstance(value, (int, float)):
 
        return value * strength
 

	
 
    raise NotImplementedError("%r,%r" % (value, strength))
light9/effect/sequencer.py
Show inline comments
 
'''
 
copies from effectloop.py, which this should replace
 
'''
 

	
 

	
 
from louie import dispatcher
 
from rdflib import URIRef
 
from twisted.internet import reactor
 
from twisted.internet import defer
 
from twisted.internet.inotify import INotify
 
from twisted.python.filepath import FilePath
 
import cyclone.sse
 
import logging, bisect, time
 
import traceback
 

	
 
from light9.namespaces import L9, RDF
 
from light9.vidref.musictime import MusicTime
 
from light9.effect import effecteval
 
from light9.effect.settings import DeviceSettings
 
from light9.effect.simple_outputs import SimpleOutputs
 

	
 
from greplin import scales
 
import imp
 

	
 
log = logging.getLogger('sequencer')
 
stats = scales.collection(
 
    '/sequencer/',
 
    scales.PmfStat('update'),
 
    scales.PmfStat('compileGraph'),
 
    scales.PmfStat('compileSong'),
 
    scales.DoubleStat('recentFps'),
 
)
 

	
 

	
 
class Note(object):
 

	
 
    def __init__(self, graph, uri, effectevalModule, simpleOutputs):
 
        g = self.graph = graph
 
        self.uri = uri
 
        self.effectEval = effectevalModule.EffectEval(
 
            graph, g.value(uri, L9['effectClass']), simpleOutputs)
 
        self.baseEffectSettings = {}  # {effectAttr: value}
 
        for s in g.objects(uri, L9['setting']):
 
            settingValues = dict(g.predicate_objects(s))
 
            ea = settingValues[L9['effectAttr']]
 
            self.baseEffectSettings[ea] = settingValues[L9['value']]
 

	
 
        floatVal = lambda s, p: float(g.value(s, p).toPython())
 
        originTime = floatVal(uri, L9['originTime'])
 
        self.points = []
 
        for curve in g.objects(uri, L9['curve']):
 
            self.points.extend(
 
                self.getCurvePoints(curve, L9['strength'], originTime))
light9/effect/settings.py
Show inline comments
 

	
 
"""
 
Data structure and convertors for a table of (device,attr,value)
 
rows. These might be effect attrs ('strength'), device attrs ('rx'),
 
or output attrs (dmx channel).
 
"""
 
import decimal
 
import numpy
 
from rdflib import URIRef, Literal
 
from light9.namespaces import RDF, L9, DEV
 
from rdfdb.patch import Patch
 
import logging
 
log = logging.getLogger('settings')
 
from light9.collector.device import resolve
 

	
 

	
 
def parseHex(h):
 
    if h[0] != '#': raise ValueError(h)
 
    return [int(h[i:i + 2], 16) for i in (1, 3, 5)]
 

	
 

	
 
def parseHexNorm(h):
 
    return [x / 255 for x in parseHex(h)]
 

	
 

	
 
def toHex(rgbFloat):
 
    return '#%02x%02x%02x' % tuple(
 
        max(0, min(255, int(v * 255))) for v in rgbFloat)
 

	
 

	
 
def getVal(graph, subj):
 
    lit = graph.value(subj, L9['value']) or graph.value(subj, L9['scaledValue'])
 
    ret = lit.toPython()
 
    if isinstance(ret, decimal.Decimal):
 
        ret = float(ret)
 
    return ret
 

	
 

	
 
class _Settings(object):
 
    """
 
    default values are 0 or '#000000'. Internal rep must not store zeros or some
 
    comparisons will break.
 
    """
 

	
 
    def __init__(self, graph, settingsList):
 
        self.graph = graph  # for looking up all possible attrs
 
        self._compiled = {}  # dev: { attr: val }; val is number or colorhex
 
        for row in settingsList:
 
            self._compiled.setdefault(row[0], {})[row[1]] = row[2]
light9/effect/simple_outputs.py
Show inline comments
 

	
 
import traceback
 
from light9.namespaces import L9, RDF
 
from light9.effect.scale import scale
 

	
 

	
 
class SimpleOutputs(object):
 

	
 
    def __init__(self, graph):
 
        self.graph = graph
 

	
 
        # effect : [(dev, attr, value, isScaled)]
 
        self.effectOutputs = {}
 

	
 
        self.graph.addHandler(self.updateEffectsFromGraph)
 

	
 
    def updateEffectsFromGraph(self):
 
        for effect in self.graph.subjects(RDF.type, L9['Effect']):
 
            settings = []
 
            for setting in self.graph.objects(effect, L9['setting']):
 
                settingValues = dict(self.graph.predicate_objects(setting))
 
                try:
 
                    d = settingValues.get(L9['device'], None)
 
                    a = settingValues.get(L9['deviceAttr'], None)
 
                    v = settingValues.get(L9['value'], None)
 
                    sv = settingValues.get(L9['scaledValue'], None)
 
                    if not (bool(v) ^ bool(sv)):
 
                        raise NotImplementedError('no value for %s' % setting)
 
                    if d is None:
 
                        raise TypeError('no device on %s' % effect)
 
                    if a is None:
 
                        raise TypeError('no attr on %s' % effect)
 
                except Exception:
 
                    traceback.print_exc()
 
                    continue
 

	
 
                settings.append((d, a, v if v is not None else sv, bool(sv)))
 

	
 
            if settings:
 
                self.effectOutputs[effect] = settings
 
            # also have to read eff :effectAttr [ :tint x; :tintStrength y ]
 

	
 
    def values(self, effect, strength, colorScale):
 
        out = {}
 
        for dev, devAttr, value, isScaled in self.effectOutputs.get(effect, []):
 
            if isScaled:
 
                value = scale(value, strength)
 
            if colorScale is not None and devAttr == L9['color']:
 
                value = scale(value, colorScale)
light9/effecteval/effectloop.py
Show inline comments
 

	
 
import time, json, logging, traceback
 
import numpy
 
import serial
 
from twisted.internet import reactor, threads
 
from twisted.internet.defer import inlineCallbacks, returnValue, succeed
 
from twisted.internet.error import TimeoutError
 
from rdflib import URIRef, Literal
 
import cyclone.httpclient
 
from light9.namespaces import L9, RDF, RDFS
 
from light9.effecteval.effect import EffectNode
 
from light9 import Effects
 
from light9 import networking
 
from light9 import Submaster
 
from light9 import dmxclient
 
from light9 import prof
 
log = logging.getLogger('effectloop')
 

	
 

	
 
class EffectLoop(object):
 
    """maintains a collection of the current EffectNodes, gets time from
 
    music player, sends dmx"""
 

	
 
    def __init__(self, graph, stats):
 
        self.graph, self.stats = graph, stats
 
        self.currentSong = None
 
        self.currentEffects = [
 
        ]  # EffectNodes for the current song plus the submaster ones
 
        self.lastLogTime = 0
 
        self.lastLogMsg = ""
 
        self.lastErrorLog = 0
 
        self.graph.addHandler(self.setEffects)
 
        self.period = 1 / 30
 
        self.coastSecs = .3  # main reason to keep this low is to notice play/pause
 
        self.songTimeFetch = 0
 
        self.songIsPlaying = False
 
        self.songTimeFromRequest = 0
 
        self.requestTime = 0  # unix sec for when we fetched songTime
 
        self.initOutput()
 

	
 
    def initOutput(self):
 
        pass
 

	
 
    def startLoop(self):
 
        log.info("startLoop")
 
        self.lastSendLevelsTime = 0
 
        reactor.callLater(self.period, self.sendLevels)
 
        reactor.callLater(self.period, self.updateTimeFromMusic)
 

	
 
@@ -137,98 +136,99 @@ class EffectLoop(object):
 
        reactor.callLater(dt, self.sendLevels)
 

	
 
    def combineOutputs(self, outputs):
 
        """pick usable effect outputs and reduce them into one for sendOutput"""
 
        outputs = [x for x in outputs if isinstance(x, Submaster.Submaster)]
 
        out = Submaster.sub_maxes(*outputs)
 

	
 
        return out
 

	
 
    @inlineCallbacks
 
    def sendOutput(self, combined):
 
        dmx = combined.get_dmx_list()
 
        yield dmxclient.outputlevels(dmx, twisted=True)
 

	
 
    def allEffectOutputs(self, songTime):
 
        outputs = []
 
        for e in self.currentEffects:
 
            try:
 
                out = e.eval(songTime)
 
                if isinstance(out, (list, tuple)):
 
                    outputs.extend(out)
 
                else:
 
                    outputs.append(out)
 
            except Exception as exc:
 
                now = time.time()
 
                if now > self.lastErrorLog + 5:
 
                    if hasattr(exc, 'expr'):
 
                        log.exception('in expression %r', exc.expr)
 
                    log.error("effect %s: %s" % (e.uri, exc))
 
                    self.lastErrorLog = now
 
        log.debug('eval %s effects, got %s outputs', len(self.currentEffects),
 
                  len(outputs))
 

	
 
        return outputs
 

	
 
    def logLevels(self, now, out):
 
        # this would look nice on the top of the effecteval web pages too
 
        if log.isEnabledFor(logging.DEBUG):
 
            log.debug(self.logMessage(out))
 
        else:
 
            if now > self.lastLogTime + 5:
 
                msg = self.logMessage(out)
 
                if msg != self.lastLogMsg:
 
                    log.info(msg)
 
                    self.lastLogMsg = msg
 
                self.lastLogTime = now
 

	
 
    def logMessage(self, out):
 
        return ("send dmx: {%s}" % ", ".join(
 
            "%r: %.3g" % (str(k), v) for k, v in list(out.get_levels().items())))
 
        return ("send dmx: {%s}" %
 
                ", ".join("%r: %.3g" % (str(k), v)
 
                          for k, v in list(out.get_levels().items())))
 

	
 

	
 
Z = numpy.zeros((50, 3), dtype=numpy.float16)
 

	
 

	
 
class ControlBoard(object):
 

	
 
    def __init__(
 
            self,
 
            dev='/dev/serial/by-id/usb-FTDI_FT232R_USB_UART_A7027NYX-if00-port0'
 
    ):
 
        log.info('opening %s', dev)
 
        self._dev = serial.Serial(dev, baudrate=115200)
 

	
 
    def _8bitMessage(self, floatArray):
 
        px255 = (numpy.clip(floatArray, 0, 1) * 255).astype(numpy.uint8)
 
        return px255.reshape((-1,)).tostring()
 

	
 
    def setStrip(self, which, pixels):
 
        """
 
        which: 0 or 1 to pick the strip
 
        pixels: (50, 3) array of 0..1 floats
 
        """
 
        command = {0: '\x00', 1: '\x01'}[which]
 
        if pixels.shape != (50, 3):
 
            raise ValueError("pixels was %s" % pixels.shape)
 
        self._dev.write('\x60' + command + self._8bitMessage(pixels))
 
        self._dev.flush()
 

	
 
    def setUv(self, which, level):
 
        """
 
        which: 0 or 1
 
        level: 0 to 1
 
        """
 
        command = {0: '\x02', 1: '\x03'}[which]
 
        self._dev.write('\x60' + command +
 
                        chr(int(max(0, min(1, level)) * 255)))
 
        self._dev.flush()
 

	
 
    def setRgb(self, color):
 
        """
 
        color: (1, 3) array of 0..1 floats
 
        """
 
        if color.shape != (1, 3):
 
            raise ValueError("color was %s" % color.shape)
 
        self._dev.write('\x60\x04' + self._8bitMessage(color))
 
        self._dev.flush()
 

	
light9/io/__init__.py
Show inline comments
 

	
 
import sys
 

	
 

	
 
class BaseIO(object):
 

	
 
    def __init__(self):
 
        self.dummy = 1
 
        self.__name__ = 'BaseIO'
 
        # please override and set __name__ to your class name
 

	
 
    def golive(self):
 
        """call this if you want to promote the dummy object becomes a live object"""
 
        print("IO: %s is going live" % self.__name__)
 
        self.dummy = 0
 
        # you'd override with additional startup stuff here,
 
        # perhaps even loading a module and saving it to a class
 
        # attr so the subclass-specific functions can use it
 

	
 
    def godummy(self):
 
        print("IO: %s is going dummy" % self.__name__)
 
        self.dummy = 1
 
        # you might override this to close ports, etc
 

	
 
    def isdummy(self):
 
        return self.dummy
 

	
 
    def __repr__(self):
 
        if self.dummy:
 
            return "<dummy %s instance>" % self.__name__
 
        else:
 
            return "<live %s instance>" % self.__name__
 

	
 
    # the derived class will have more methods to do whatever it does,
 
    # and they should return dummy values if self.dummy==1.
 

	
 

	
 
class ParportDMX(BaseIO):
 

	
 
    def __init__(self, dimmers=68):
 
        BaseIO.__init__(self)
 
        self.__name__ = 'ParportDMX'
 
        self.dimmers = dimmers
 

	
 
    def golive(self):
 
        BaseIO.golive(self)
 
        from . import parport
 
        self.parport = parport
 
        self.parport.getparport()
light9/io/udmx.py
Show inline comments
 

	
 
import logging
 
import usb.core
 
from usb.util import CTRL_TYPE_VENDOR, CTRL_RECIPIENT_DEVICE, CTRL_OUT
 

	
 
log = logging.getLogger('udmx')
 
"""
 
Send dmx to one of these:
 
http://www.amazon.com/Interface-Adapter-Controller-Lighting-Freestyler/dp/B00W52VIOS
 

	
 
[4520784.059479] usb 1-2.3: new low-speed USB device number 6 using xhci_hcd
 
[4520784.157410] usb 1-2.3: New USB device found, idVendor=16c0, idProduct=05dc
 
[4520784.157416] usb 1-2.3: New USB device strings: Mfr=1, Product=2, SerialNumber=3
 
[4520784.157419] usb 1-2.3: Product: uDMX
 
[4520784.157422] usb 1-2.3: Manufacturer: www.anyma.ch
 
[4520784.157424] usb 1-2.3: SerialNumber: ilLUTZminator001
 

	
 
See https://www.illutzmination.de/udmxfirmware.html?&L=1
 
    sources/commandline/uDMX.c
 
or https://github.com/markusb/uDMX-linux/blob/master/uDMX.c
 
"""
 

	
 
cmd_SetChannelRange = 0x0002
 

	
 

	
 
class Udmx(object):
 

	
 
    def __init__(self, bus):
 
        self.dev = None
 
        for dev in usb.core.find(idVendor=0x16c0,
 
                                 idProduct=0x05dc,
 
                                 find_all=True):
 
            print("udmx device at %r" % dev.bus)
 
            if bus is None or bus == dev.bus:
 
                self.dev = dev
 
        if not self.dev:
 
            raise IOError('no matching udmx device found for requested bus %r' %
 
                          bus)
 
        log.info('found udmx at %r', self.dev)
 

	
 
    def SendDMX(self, buf):
 
        ret = self.dev.ctrl_transfer(bmRequestType=CTRL_TYPE_VENDOR |
 
                                     CTRL_RECIPIENT_DEVICE | CTRL_OUT,
 
                                     bRequest=cmd_SetChannelRange,
 
                                     wValue=len(buf),
 
                                     wIndex=0,
 
                                     data_or_wLength=buf)
 
        if ret < 0:
 
            raise ValueError("ctrl_transfer returned %r" % ret)
light9/paint/solve.py
Show inline comments
 

	
 
from light9.namespaces import RDF, L9, DEV
 
from PIL import Image
 
import numpy
 
import scipy.misc, scipy.ndimage, scipy.optimize
 
import cairo
 
import logging
 

	
 
from light9.effect.settings import DeviceSettings, parseHex, toHex
 

	
 
log = logging.getLogger('solve')
 

	
 
# numpy images in this file are (x, y, c) layout.
 

	
 

	
 
def numpyFromCairo(surface):
 
    w, h = surface.get_width(), surface.get_height()
 
    a = numpy.frombuffer(surface.get_data(), numpy.uint8)
 
    a.shape = h, w, 4
 
    a = a.transpose((1, 0, 2))
 
    return a[:w, :h, :3]
 

	
 

	
 
def numpyFromPil(img):
 
    return scipy.misc.fromimage(img, mode='RGB').transpose((1, 0, 2))
 

	
 

	
 
def loadNumpy(path, thumb=(100, 100)):
 
    img = Image.open(path)
 
    img.thumbnail(thumb)
 
    return numpyFromPil(img)
 

	
 

	
 
def saveNumpy(path, img):
 
    # maybe this should only run if log level is debug?
 
    scipy.misc.imsave(path, img.transpose((1, 0, 2)))
 

	
 

	
 
def scaledHex(h, scale):
 
    rgb = parseHex(h)
 
    rgb8 = (rgb * scale).astype(numpy.uint8)
 
    return '#%02x%02x%02x' % tuple(rgb8)
 

	
 

	
 
def colorRatio(col1, col2):
 
    rgb1 = parseHex(col1)
 
    rgb2 = parseHex(col2)
 

	
 
    def div(x, y):
light9/paint/solve_test.py
Show inline comments
 
@@ -24,108 +24,108 @@ class TestSolve(unittest.TestCase):
 
        self.assertEqual(DeviceSettings(self.graph, []), devAttrs)
 

	
 
    @unittest.skip("unfinished")
 
    def testSingleLightCloseMatch(self):
 
        devAttrs = self.solveMethod({
 
            'strokes': [{
 
                'pts': [[224, 141], [223, 159]],
 
                'color': '#ffffff'
 
            }]
 
        })
 
        self.assertEqual(
 
            DeviceSettings(self.graph, [
 
                (DEV['aura1'], L9['color'], "#ffffff"),
 
                (DEV['aura1'], L9['rx'], 0.5),
 
                (DEV['aura1'], L9['ry'], 0.573),
 
            ]), devAttrs)
 

	
 

	
 
class TestSolveBrute(TestSolve):
 

	
 
    def setUp(self):
 
        super(TestSolveBrute, self).setUp()
 
        self.solveMethod = self.solver.solveBrute
 

	
 

	
 
CAM_TEST = Namespace('http://light9.bigasterisk.com/test/cam/')
 

	
 

	
 
class TestSimulationLayers(unittest.TestCase):
 

	
 
    def setUp(self):
 
        self.graph = LocalSyncedGraph(
 
            files=['test/cam/lightConfig.n3', 'test/cam/bg.n3'])
 
        self.solver = solve.Solver(self.graph,
 
                                   imgSize=(100, 48),
 
                                   sessions=[L9['session0']])
 
        self.solver.loadSamples()
 

	
 
    def testBlack(self):
 
        self.assertEqual([],
 
                         self.solver.simulationLayers(
 
                             settings=DeviceSettings(self.graph, [])))
 

	
 
    def testPerfect1Match(self):
 
        layers = self.solver.simulationLayers(
 
            settings=DeviceSettings(self.graph, [(
 
                DEV['aura1'], L9['color'],
 
                "#ffffff"), (DEV['aura1'], L9['rx'],
 
                              0.5), (DEV['aura1'], L9['ry'], 0.573)]))
 
                             0.5), (DEV['aura1'], L9['ry'], 0.573)]))
 
        self.assertEqual([{
 
            'path': CAM_TEST['bg2-d.jpg'],
 
            'color': (1., 1., 1.)
 
        }], layers)
 

	
 
    def testPerfect1MatchTinted(self):
 
        layers = self.solver.simulationLayers(
 
            settings=DeviceSettings(self.graph, [(
 
                DEV['aura1'], L9['color'],
 
                "#304050"), (DEV['aura1'], L9['rx'],
 
                              0.5), (DEV['aura1'], L9['ry'], 0.573)]))
 
                             0.5), (DEV['aura1'], L9['ry'], 0.573)]))
 
        self.assertEqual([{
 
            'path': CAM_TEST['bg2-d.jpg'],
 
            'color': (.188, .251, .314)
 
        }], layers)
 

	
 
    def testPerfect2Matches(self):
 
        layers = self.solver.simulationLayers(
 
            settings=DeviceSettings(self.graph, [
 
                (DEV['aura1'], L9['color'], "#ffffff"),
 
                (DEV['aura1'], L9['rx'], 0.5),
 
                (DEV['aura1'], L9['ry'], 0.573),
 
                (DEV['aura2'], L9['color'], "#ffffff"),
 
                (DEV['aura2'], L9['rx'], 0.7),
 
                (DEV['aura2'], L9['ry'], 0.573),
 
            ]))
 
        self.assertItemsEqual([
 
            {
 
                'path': CAM_TEST['bg2-d.jpg'],
 
                'color': (1, 1, 1)
 
            },
 
            {
 
                'path': CAM_TEST['bg2-f.jpg'],
 
                'color': (1, 1, 1)
 
            },
 
        ], layers)
 

	
 

	
 
class TestCombineImages(unittest.TestCase):
 

	
 
    def setUp(self):
 
        graph = LocalSyncedGraph(
 
            files=['test/cam/lightConfig.n3', 'test/cam/bg.n3'])
 
        self.solver = solve.Solver(graph,
 
                                   imgSize=(100, 48),
 
                                   sessions=[L9['session0']])
 
        self.solver.loadSamples()
 

	
 
    def test(self):
 
        out = self.solver.combineImages(layers=[
 
            {
 
                'path': CAM_TEST['bg2-d.jpg'],
 
                'color': (.2, .2, .3)
 
            },
 
            {
 
                'path': CAM_TEST['bg2-a.jpg'],
 
                'color': (.888, 0, .3)
 
            },
 
        ])
light9/vidref/replay.py
Show inline comments
 

	
 
import os, gtk, shutil, logging, time
 
from bisect import bisect_left
 
from decimal import Decimal
 
log = logging.getLogger()
 

	
 
framerate = 15
 

	
 

	
 
def songDir(song):
 
    safeUri = song.split('://')[-1].replace('/', '_')
 
    return os.path.expanduser("~/light9-vidref/play-%s" % safeUri)
 

	
 

	
 
def takeDir(songDir, startTime):
 
    """
 
    startTime: unix seconds (str ok)
 
    """
 
    return os.path.join(songDir, str(int(startTime)))
 

	
 

	
 
def snapshotDir():
 
    return os.path.expanduser("~/light9-vidref/snapshot")
 

	
 

	
 
class ReplayViews(object):
 
    """
 
    the whole list of replay windows. parent is the scrolling area for
 
    these windows to be added
 
    """
 

	
 
    def __init__(self, parent):
 
        # today, parent is the vbox the replay windows should appear in
 
        self.parent = parent
 
        self.lastStart = None
 

	
 
        self.views = []
 

	
 
    def update(self, position):
 
        """
 
        freshen all replay windows. We get called this about every
 
        time there's a new live video frame.
 

	
 
        Calls loadViewsForSong if we change songs, or even if we just
 
        restart the playback of the current song (since there could be
 
        a new replay view)
 
        """
 
        t1 = time.time()
 
        if position.get('started') != self.lastStart and position['song']:
0 comments (0 inline, 0 general)