Mercurial > code > home > repos > light9
changeset 209:1a84c5e83d3e
dmxserver and subcomposer work in new layout
author | drewp@bigasterisk.com |
---|---|
date | Sun, 10 Apr 2005 19:12:57 +0000 |
parents | 26704c2ab1ad |
children | f41004d5a507 |
files | bin/dmxserver bin/run_local.py bin/subcomposer flax/Subcomposer.py flax/Submaster.py flax/TLUtility.py flax/dmxchanedit.py light8/Makefile light8/Patch.py light8/dmxclient.py light8/dmxserver.py light8/io.py light8/parport.c light8/parport.i light8/serport.i light8/updatefreq.py light9/Patch.py light9/Submaster.py light9/TLUtility.py light9/dmxchanedit.py light9/dmxclient.py light9/io/Makefile light9/io/__init__.py light9/io/parport.c light9/io/parport.i light9/io/serport.i light9/updatefreq.py |
diffstat | 27 files changed, 1226 insertions(+), 1226 deletions(-) [+] |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bin/dmxserver Sun Apr 10 19:12:57 2005 +0000 @@ -0,0 +1,187 @@ +#!/usr/bin/python +""" + +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 __future__ import division +from twisted.internet import reactor +from twisted.web import xmlrpc, server +import sys,time,os +from optparse import OptionParser +import run_local +from light9.io import ParportDMX +from light9.updatefreq import Updatefreq + +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=ParportDMX() + 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=self.clientlevels.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)) + del self.clientlevels[cid] + 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() + + if self.clientschanged or time.time()>self.lastupdate+1: + 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 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]) + + 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("\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 + if cid not in self.lastseen: + print "hello new client %s" % cid + self.clientfreq[cid]=Updatefreq() + + self.lastseen[cid]=time.time() + self.clientfreq[cid].update() + return "ok" + +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, + help=('dmx output frequency')) +(options,songfiles)=parser.parse_args() + +print options + +print "starting xmlrpc server on port 8030" +reactor.listenTCP(8030,server.Site(XMLRPCServe(options))) +reactor.run() +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bin/run_local.py Sun Apr 10 19:12:57 2005 +0000 @@ -0,0 +1,8 @@ +# allows bin/* to work without installation + +# this should be turned off when the programs are installed + +import sys,os +sys.path.insert(0,os.path.join(os.path.dirname(sys.argv[0]),"..")) + +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bin/subcomposer Sun Apr 10 19:12:57 2005 +0000 @@ -0,0 +1,148 @@ +#!/usr/bin/python + +from __future__ import division, nested_scopes +import sys,os,time,atexit +import Tkinter as tk +from dispatch import dispatcher + +import run_local +from light9.dmxchanedit import Levelbox +from light9 import dmxclient, Patch, Submaster + +class Subcomposer(tk.Frame): + def __init__(self, master, levelboxopts=None, dmxdummy=0, numchannels=68, + use_persistentlevels=0): + tk.Frame.__init__(self, master, bg='black') + self.dmxdummy = dmxdummy + self.numchannels = numchannels + + self.levels = [0]*68 # levels should never get overwritten, just edited + + self.levelbox = Levelbox(self) + self.levelbox.pack(side='top') + # the dmx levels we edit and output, range is 0..1 (dmx chan 1 is + # the 0 element) + self.levelbox.setlevels(self.levels) + + self.savebox = Savebox(self, self.levels, cmd=self.savenewsub) + self.savebox.pack(side='top') + + self.loadbox = Savebox(self, self.levels, verb="Load", cmd=self.loadsub) + self.loadbox.pack(side='top') + + def alltozero(): + self.set_levels([0] * self.numchannels) + dispatcher.send("levelchanged") + + tk.Button(self, text="all to zero", command=alltozero).pack(side='top') + + dispatcher.connect(self.levelchanged,"levelchanged") + dispatcher.connect(self.sendupdate,"levelchanged") + + if use_persistentlevels: + self.persistentlevels() + + self.lastupdate=0 # time we last sent to dmx + + self.lastsent=[] # copy of levels + + def persistentlevels(self): + """adjusts levels from subcomposer.savedlevels, if possible; and + arranges to save the levels in that file upon exit""" + self.load_levels() + atexit.register(self.save_levels) + def save_levels(self, *args): + levelfile = file("subcomposer.savedlevels","w") + levelfile.write(" ".join(map(str, self.levels))) + def load_levels(self): + try: + levelfile = file("subcomposer.savedlevels","r") + levels = map(float, levelfile.read().split()) + self.set_levels(levels) + except IOError: + pass + def levelchanged(self, channel=None, newlevel=None): + if channel is not None and newlevel is not None: + if channel>len(self.levels): + return + self.levels[channel-1]=max(0,min(1,float(newlevel))) + self.levelbox.setlevels(self.levels) + def savenewsub(self, levels, subname): + leveldict={} + for i,lev in zip(range(len(self.levels)),self.levels): + if lev!=0: + leveldict[Patch.get_channel_name(i+1)]=lev + + s=Submaster.Submaster(subname,leveldict) + s.save() + def loadsub(self, levels, subname): + """puts a sub into the levels, replacing old level values""" + s=Submaster.Submasters().get_sub_by_name(subname) + self.levels[:]=[0]*68 + self.levels[:]=s.get_dmx_list() + dispatcher.send("levelchanged") + def sendupdate(self): + if not self.dmxdummy: + dmxclient.outputlevels(self.levels) + self.lastupdate = time.time() + self.lastsent = self.levels[:] + def considersendupdate(self, use_after_loop=0): + """If use_after_loop is true, it is the period of the after loop.""" + if self.lastsent != self.levels or time.time() > self.lastupdate + 1: + self.sendupdate() + if use_after_loop: + self.after(use_after_loop, self.considersendupdate, use_after_loop) + def set_levels(self, levels): + self.levels[:] = levels + dispatcher.send("levelchanged") + +def Savebox(master, levels, verb="Save", cmd=None): + f=tk.Frame(master,bd=2,relief='raised') + tk.Label(f,text="Sub name:").pack(side='left') + e=tk.Entry(f) + e.pack(side='left',exp=1,fill='x') + def cb(*args): + subname=e.get() + cmd(levels,subname) + print "sub",verb,subname + e.bind("<Return>",cb) + tk.Button(f,text=verb,command=cb).pack(side='left') + return f + +def open_sub_editing_window(subname, use_mainloop=1, dmxdummy=0): + if use_mainloop: + toplevel = tk.Tk() + else: + toplevel = tk.Toplevel() + if dmxdummy: + dummy_str = ' (dummy)' + else: + dummy_str = '' + toplevel.title("Subcomposer: %s%s" % (subname, dummy_str)) + sc = Subcomposer(toplevel, use_persistentlevels=0, dmxdummy=dmxdummy) + sc.pack(fill='both', expand=1) + sc.loadsub(None, subname) # don't ask + sc.considersendupdate(use_after_loop=10) + if use_mainloop: + tk.mainloop() + +############################# + +if __name__ == "__main__": + root=tk.Tk() + root.config(bg='black') + root.tk_setPalette("#004633") + + sc = Subcomposer(root, dmxdummy=0) + sc.pack() + + while 1: + if 0: + for i in range(20): # don't let Tk take all the time + tk._tkinter.dooneevent() + print "loop" + else: + root.update() + + sc.considersendupdate() + time.sleep(.01)
--- a/flax/Subcomposer.py Sun Apr 10 07:31:15 2005 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,150 +0,0 @@ -#!/usr/bin/python - -from __future__ import division, nested_scopes -import Tkinter as tk -from dmxchanedit import Levelbox -import sys,os,time,atexit -sys.path.append("../light8") -import dmxclient -import Patch -import Submaster - -from dispatch import dispatcher - -class Subcomposer(tk.Frame): - def __init__(self, master, levelboxopts=None, dmxdummy=0, numchannels=68, - use_persistentlevels=0): - tk.Frame.__init__(self, master, bg='black') - self.dmxdummy = dmxdummy - self.numchannels = numchannels - - self.levels = [0]*68 # levels should never get overwritten, just edited - - self.levelbox = Levelbox(self) - self.levelbox.pack(side='top') - # the dmx levels we edit and output, range is 0..1 (dmx chan 1 is - # the 0 element) - self.levelbox.setlevels(self.levels) - - self.savebox = Savebox(self, self.levels, cmd=self.savenewsub) - self.savebox.pack(side='top') - - self.loadbox = Savebox(self, self.levels, verb="Load", cmd=self.loadsub) - self.loadbox.pack(side='top') - - def alltozero(): - self.set_levels([0] * self.numchannels) - dispatcher.send("levelchanged") - - tk.Button(self, text="all to zero", command=alltozero).pack(side='top') - - dispatcher.connect(self.levelchanged,"levelchanged") - dispatcher.connect(self.sendupdate,"levelchanged") - - if use_persistentlevels: - self.persistentlevels() - - self.lastupdate=0 # time we last sent to dmx - - self.lastsent=[] # copy of levels - - def persistentlevels(self): - """adjusts levels from subcomposer.savedlevels, if possible; and - arranges to save the levels in that file upon exit""" - self.load_levels() - atexit.register(self.save_levels) - def save_levels(self, *args): - levelfile = file("subcomposer.savedlevels","w") - levelfile.write(" ".join(map(str, self.levels))) - def load_levels(self): - try: - levelfile = file("subcomposer.savedlevels","r") - levels = map(float, levelfile.read().split()) - self.set_levels(levels) - except IOError: - pass - def levelchanged(self, channel=None, newlevel=None): - if channel is not None and newlevel is not None: - if channel>len(self.levels): - return - self.levels[channel-1]=max(0,min(1,float(newlevel))) - self.levelbox.setlevels(self.levels) - def savenewsub(self, levels, subname): - leveldict={} - for i,lev in zip(range(len(self.levels)),self.levels): - if lev!=0: - leveldict[Patch.get_channel_name(i+1)]=lev - - s=Submaster.Submaster(subname,leveldict) - s.save() - def loadsub(self, levels, subname): - """puts a sub into the levels, replacing old level values""" - s=Submaster.Submasters().get_sub_by_name(subname) - self.levels[:]=[0]*68 - self.levels[:]=s.get_dmx_list() - dispatcher.send("levelchanged") - def sendupdate(self): - if not self.dmxdummy: - dmxclient.outputlevels(self.levels) - self.lastupdate = time.time() - self.lastsent = self.levels[:] - def considersendupdate(self, use_after_loop=0): - """If use_after_loop is true, it is the period of the after loop.""" - if self.lastsent != self.levels or time.time() > self.lastupdate + 1: - self.sendupdate() - if use_after_loop: - self.after(use_after_loop, self.considersendupdate, use_after_loop) - def set_levels(self, levels): - self.levels[:] = levels - dispatcher.send("levelchanged") - -def Savebox(master, levels, verb="Save", cmd=None): - f=tk.Frame(master,bd=2,relief='raised') - tk.Label(f,text="Sub name:").pack(side='left') - e=tk.Entry(f) - e.pack(side='left',exp=1,fill='x') - def cb(*args): - subname=e.get() - cmd(levels,subname) - print "sub",verb,subname - e.bind("<Return>",cb) - tk.Button(f,text=verb,command=cb).pack(side='left') - return f - -def open_sub_editing_window(subname, use_mainloop=1, dmxdummy=0): - if use_mainloop: - toplevel = tk.Tk() - else: - toplevel = tk.Toplevel() - if dmxdummy: - dummy_str = ' (dummy)' - else: - dummy_str = '' - toplevel.title("Subcomposer: %s%s" % (subname, dummy_str)) - sc = Subcomposer(toplevel, use_persistentlevels=0, dmxdummy=dmxdummy) - sc.pack(fill='both', expand=1) - sc.loadsub(None, subname) # don't ask - sc.considersendupdate(use_after_loop=10) - if use_mainloop: - tk.mainloop() - -############################# - -if __name__ == "__main__": - root=tk.Tk() - root.config(bg='black') - root.tk_setPalette("#004633") - - sc = Subcomposer(root, dmxdummy=0) - sc.pack() - - while 1: - if 0: - for i in range(20): # don't let Tk take all the time - tk._tkinter.dooneevent() - print "loop" - else: - root.update() - - sc.considersendupdate() - time.sleep(.01)
--- a/flax/Submaster.py Sun Apr 10 07:31:15 2005 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,161 +0,0 @@ -from __future__ import division -from TLUtility import dict_scale, dict_max - -import sys -sys.path.append('../light8') - -import Patch - -class Submaster: - "Contain a dictionary of levels, but you didn't need to know that" - def __init__(self, name, leveldict=None, temporary=0): - self.name = name - self.temporary = temporary - if leveldict: - self.levels = leveldict - else: - self.levels = {} - self.reload() - def reload(self): - if self.temporary: - return - try: - self.levels.clear() - subfile = file("subs/%s" % self.name) - for line in subfile.readlines(): - if not line.strip(): # if line is only whitespace - continue # "did i say newspace?" - - try: - name, val = line.split(':') - name = name.strip() - self.levels[name] = float(val) - except ValueError: - print "(%s) Error with this line: %s" % (self.name, - line[:-1]) - except IOError: - print "Can't read file for sub: %s" % self.name - def save(self): - if self.temporary: - print "not saving temporary sub named",self.name - return - - subfile = file("subs/%s" % self.name, 'w') - names = self.levels.keys() - names.sort() - for name in names: - val = self.levels[name] - subfile.write("%s : %s\n" % (name, val)) - def set_level(self, channelname, level, save=1): - self.levels[Patch.resolve_name(channelname)] = level - if save: - self.save() - def set_all_levels(self, leveldict): - self.levels.clear() - for k, v in leveldict.items(): - self.set_level(k, v, save=0) - self.save() - def get_levels(self): - return self.levels - def __mul__(self, scalar): - return Submaster("%s*%s" % (self.name, scalar), - dict_scale(self.levels, scalar), temporary=1) - __rmul__ = __mul__ - def max(self, *othersubs): - return sub_maxes(self, *othersubs) - def __repr__(self): - levels = ' '.join(["%s:%.2f" % item for item in self.levels.items()]) - return "<'%s': [%s]>" % (self.name, levels) - def get_dmx_list(self): - leveldict = self.get_levels() # gets levels of sub contents - - levels = [0] * 68 - for k, v in leveldict.items(): - dmxchan = Patch.get_dmx_channel(k) - 1 - levels[dmxchan] = max(v, levels[dmxchan]) - - return levels - def normalize_patch_names(self): - """Use only the primary patch names.""" - # possibly busted -- don't use unless you know what you're doing - self.set_all_levels(self.levels.copy()) - def get_normalized_copy(self): - """Get a copy of this sumbaster that only uses the primary patch - names. The levels will be the same.""" - newsub = Submaster("%s (normalized)" % self.name, temporary=1) - newsub.set_all_levels(self.levels) - return newsub - def crossfade(self, othersub, amount): - """Returns a new sub that is a crossfade between this sub and - another submaster. - - NOTE: You should only crossfade between normalized submasters.""" - otherlevels = othersub.get_levels() - keys_set = {} - for k in self.levels.keys() + otherlevels.keys(): - keys_set[k] = 1 - all_keys = keys_set.keys() - - xfaded_sub = Submaster("xfade", temporary=1) - for k in all_keys: - xfaded_sub.set_level(k, - linear_fade(self.levels.get(k, 0), - otherlevels.get(k, 0), - amount)) - - return xfaded_sub - -def linear_fade(start, end, amount): - """Fades between two floats by an amount. amount is a float between - 0 and 1. If amount is 0, it will return the start value. If it is 1, - the end value will be returned.""" - level = start + (amount * (end - start)) - return level - -def sub_maxes(*subs): - return Submaster("max(%r)" % (subs,), - dict_max(*[sub.levels for sub in subs]), temporary=1) - -class Submasters: - "Collection o' Submaster objects" - def __init__(self): - self.submasters = {} - - import os - files = os.listdir('subs') - - for filename in files: - # we don't want these files - if filename.startswith('.') or filename.endswith('~') or \ - filename.startswith('CVS'): - continue - self.submasters[filename] = Submaster(filename) - def get_all_subs(self): - "All Submaster objects" - l = self.submasters.items() - l.sort() - l = [x[1] for x in l] - songs = [] - notsongs = [] - for s in l: - if s.name.startswith('song'): - songs.append(s) - else: - notsongs.append(s) - combined = notsongs + songs - return combined - def get_sub_by_name(self, name): - "Makes a new sub if there isn't one." - return self.submasters.get(name, Submaster(name)) - __getitem__ = get_sub_by_name - -if __name__ == "__main__": - Patch.reload_data() - s = Submasters() - print s.get_all_subs() - if 0: # turn this on to normalize all subs - for sub in s.get_all_subs(): - print "before", sub - sub.normalize_patch_names() - sub.save() - print "after", sub
--- a/flax/TLUtility.py Sun Apr 10 07:31:15 2005 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,209 +0,0 @@ -"""Collected utility functions, many are taken from Drew's utils.py in -Cuisine CVS and Hiss's Utility.py.""" - -from __future__ import generators -import sys - -__author__ = "David McClosky <dmcc@bigasterisk.com>, " + \ - "Drew Perttula <drewp@bigasterisk.com>" -__cvsid__ = "$Id: TLUtility.py,v 1.1 2003/05/25 08:25:35 dmcc Exp $" -__version__ = "$Revision: 1.1 $"[11:-2] - -def make_attributes_from_args(*argnames): - """ - This function simulates the effect of running - self.foo=foo - for each of the given argument names ('foo' in the example just - now). Now you can write: - def __init__(self,foo,bar,baz): - copy_to_attributes('foo','bar','baz') - ... - instead of: - def __init__(self,foo,bar,baz): - self.foo=foo - self.bar=bar - self.baz=baz - ... - """ - - callerlocals=sys._getframe(1).f_locals - callerself=callerlocals['self'] - for a in argnames: - try: - setattr(callerself,a,callerlocals[a]) - except KeyError: - raise KeyError, "Function has no argument '%s'" % a - -def enumerate(*collections): - """Generates an indexed series: (0,coll[0]), (1,coll[1]) ... - - this is a multi-list version of the code from the PEP: - enumerate(a,b) gives (0,a[0],b[0]), (1,a[1],b[1]) ... - """ - i = 0 - iters = [iter(collection) for collection in collections] - while 1: - yield [i,] + [iterator.next() for iterator in iters] - i += 1 - -def dumpobj(o): - """Prints all the object's non-callable attributes""" - print repr(o) - for a in [x for x in dir(o) if not callable(getattr(o, x))]: - try: - print " %20s: %s " % (a, getattr(o, a)) - except: - pass - print "" - -def dict_filter_update(d, **newitems): - """Adds a set of new keys and values to dictionary 'd' if the values are - true: - - >>> some_dict = {} - >>> dict_filter_update(some_dict, a=None, b=0, c=1, e={}, s='hello') - >>> some_dict - {'c': 1, 's': 'hello'} - """ - for k, v in newitems.items(): - if v: d[k] = v - -def try_get_logger(channel): - """Tries to get a logger with the channel 'channel'. Will return a - silent DummyClass if logging is not available.""" - try: - import logging - log = logging.getLogger(channel) - except ImportError: - log = DummyClass() - return log - -class DummyClass: - """A class that can be instantiated but never used. It is intended to - be replaced when information is available. - - Usage: - >>> d = DummyClass(1, 2, x="xyzzy") - >>> d.someattr - Traceback (most recent call last): - File "<stdin>", line 1, in ? - File "Utility.py", line 33, in __getattr__ - raise AttributeError, "Attempted usage of a DummyClass: %s" % key - AttributeError: Attempted usage of a DummyClass: someattr - >>> d.somefunction() - Traceback (most recent call last): - File "<stdin>", line 1, in ? - File "Utility.py", line 33, in __getattr__ - raise AttributeError, "Attempted usage of a DummyClass: %s" % key - AttributeError: Attempted usage of a DummyClass: somefunction""" - def __init__(self, use_warnings=1, raise_exceptions=0, **kw): - """Constructs a DummyClass""" - make_attributes_from_args('use_warnings', 'raise_exceptions') - def __getattr__(self, key): - """Raises an exception to warn the user that a Dummy is not being - replaced in time.""" - if key == "__del__": - return - msg = "Attempted usage of '%s' on a DummyClass" % key - if self.use_warnings: - import warnings - warnings.warn(msg) - if self.raise_exceptions: - raise AttributeError, msg - return lambda *args, **kw: self.bogus_function() - def bogus_function(self): - pass - -class ClassyDict(dict): - """A dict that accepts attribute-style access as well (for keys - that are legal names, obviously). I used to call this Struct, but - chose the more colorful name to avoid confusion with the struct - module.""" - def __getattr__(self, a): - return self[a] - def __setattr__(self, a, v): - self[a] = v - def __delattr__(self, a): - del self[a] - -def trace(func): - """Good old fashioned Lisp-style tracing. Example usage: - - >>> def f(a, b, c=3): - >>> print a, b, c - >>> return a + b - >>> - >>> - >>> f = trace(f) - >>> f(1, 2) - |>> f called args: [1, 2] - 1 2 3 - <<| f returned 3 - 3 - - TODO: print out default keywords (maybe) - indent for recursive call like the lisp version (possible use of - generators?)""" - name = func.func_name - def tracer(*args, **kw): - s = '|>> %s called' % name - if args: - s += ' args: %r' % list(args) - if kw: - s += ' kw: %r' % kw - print s - ret = func(*args, **kw) - print '<<| %s returned %s' % (name, ret) - return ret - return tracer - -# these functions taken from old light8 code -def dict_max(*dicts): - """ - ({'a' : 5, 'b' : 9}, {'a' : 10, 'b' : 4}) - returns ==> {'a' : 10, 'b' : 9} - """ - newdict = {} - for d in dicts: - for k,v in d.items(): - newdict[k] = max(v, newdict.get(k, 0)) - return newdict - -def dict_scale(d,scl): - """scales all values in dict and returns a new dict""" - return dict([(k,v*scl) for k,v in d.items()]) - -def dict_subset(d, dkeys, default=0): - """Subset of dictionary d: only the keys in dkeys. If you plan on omitting - keys, make sure you like the default.""" - newd = {} # dirty variables! - for k in dkeys: - newd[k] = d.get(k, default) - return newd - -# functions specific to Timeline -# TBD -def last_less_than(array, x): - """array must be sorted""" - best = None - for elt in array: - if elt <= x: - best = elt - elif best is not None: - return best - return best - -# TBD -def first_greater_than(array, x): - """array must be sorted""" - array_rev = array[:] - array_rev.reverse() - best = None - for elt in array_rev: - if elt >= x: - best = elt - elif best is not None: - return best - return best - -
--- a/flax/dmxchanedit.py Sun Apr 10 07:31:15 2005 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,130 +0,0 @@ -""" - -widget to show all dmx channel levels and allow editing. levels might -not actually match what dmxserver is outputting. - -""" -from __future__ import nested_scopes,division -import Tkinter as tk -import sys -sys.path.append("../light8") -import Patch -from uihelpers import make_frame, colorlabel, eventtoparent -from dispatch import dispatcher - -stdfont = ('Arial', 12) - -class Onelevel(tk.Frame): - """a name/level pair""" - def __init__(self, parent, channelnum): - """channelnum is 1..68, like the real dmx""" - tk.Frame.__init__(self,parent) - - self.channelnum=channelnum - self.currentlevel=0 # the level we're displaying, 0..1 - - # 3 widgets, left-to-right: - - # channel number -- will turn yellow when being altered - self.num_lab = tk.Label(self, text=str(channelnum), - width=3, bg='grey40', - fg='white', font=stdfont, - padx=0, pady=0, bd=0, height=1) - self.num_lab.pack(side='left') - - # text description of channel - self.desc_lab=tk.Label(self, text=Patch.get_channel_name(channelnum), - width=14, font=stdfont, anchor='w', - padx=0, pady=0, bd=0, - height=1, bg='black', fg='white') - self.desc_lab.pack(side='left') - - # current level of channel, shows intensity with color - self.level_lab = tk.Label(self, width=3, bg='lightBlue', - font=stdfont, anchor='e', - padx=1, pady=0, bd=0, height=1) - self.level_lab.pack(side='left') - - self.setlevel(0) - self.setupmousebindings() - - def setupmousebindings(self): - def b1down(ev): - self.desc_lab.config(bg='cyan') - self._start_y=ev.y - self._start_lev=self.currentlevel - def b1motion(ev): - delta=self._start_y-ev.y - self.changelevel(self._start_lev+delta*.005) - def b1up(ev): - self.desc_lab.config(bg='black') - def b3up(ev): - self.changelevel(0.0) - def b3down(ev): - self.changelevel(1.0) - - # make the buttons work in the child windows - for w in self.winfo_children(): - for e,func in (('<ButtonPress-1>',b1down), - ('<B1-Motion>',b1motion), - ('<ButtonRelease-1>',b1up), - ('<ButtonRelease-3>', b3up), - ('<ButtonPress-3>', b3down)): - w.bind(e,func) -# w.bind(e,lambda ev,e=e: eventtoparent(ev,e)) - - def colorlabel(self): - """color the level label based on its own text (which is 0..100)""" - txt=self.level_lab['text'] or "0" - lev=float(txt)/100 - low=(80,80,180) - high=(255,55,050) - out = [int(l+lev*(h-l)) for h,l in zip(high,low)] - col="#%02X%02X%02X" % tuple(out) - self.level_lab.config(bg=col) - - def setlevel(self,newlev): - """the main program is telling us to change our - display. newlev is 0..1""" - self.currentlevel=newlev - newlev="%d"%(newlev*100) - olddisplay=self.level_lab.cget('text') - if newlev!=olddisplay: - self.level_lab.config(text=newlev) - self.colorlabel() - - def getlevel(self): - """returns currently displayed level, 0..1""" - return self.currentlevel - - def changelevel(self,newlev): - - """the user is adjusting the level on this widget. the main - program needs to hear about it. then the main program will - call setlevel()""" - - dispatcher.send("levelchanged",channel=self.channelnum,newlevel=newlev) - -class Levelbox(tk.Frame): - def __init__(self, parent, num_channels=68): - tk.Frame.__init__(self,parent) - - self.levels = [] # Onelevel objects - - frames = (make_frame(self), make_frame(self)) - - for channel in range(1, num_channels+1): - - # frame for this channel - f = Onelevel(frames[channel > (num_channels/2)],channel) - - self.levels.append(f) - f.pack(side='top') - - #dispatcher.connect(setalevel,"setlevel") - - def setlevels(self,newlevels): - """sets levels to the new list of dmx levels (0..1). list can - be any length""" - for l,newlev in zip(self.levels,newlevels): - l.setlevel(newlev)
--- a/light8/Makefile Sun Apr 10 07:31:15 2005 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,23 +0,0 @@ -LIB=/usr/local/lib -INC=-I/usr/local/include/python2.2 - -go: _parport.so FlyingFader.py _serport.so - result="your modules and links are now up to date" - -FlyingFader.py: - ln -s ../Widgets/FlyingFader.py - -_parport.so: parport_wrap.c - gcc -shared ${INC} parport_wrap.c parport.c -o _parport.so - -parport_wrap.c: parport.c parport.i - swig -python parport.i - -_serport.so: serport_wrap.c - gcc -shared -O ${INC} serport_wrap.c -o _serport.so - -serport_wrap.c: serport.i - swig -python serport.i - -clean: - rm -f parport_wrap.c serport_wrap.c *.o *.so
--- a/light8/Patch.py Sun Apr 10 07:31:15 2005 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,46 +0,0 @@ -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 name in patch: - return patch[name] - - try: - i = int(name) - return i - except ValueError: - raise ValueError("Invalid channel name: %s" % name) - -def get_channel_name(dmxnum): - try: - return reverse_patch[dmxnum] - except KeyError: - return str(dmxnum) - -def reload_data(): - global patch, reverse_patch - import patchdata - - reload(patchdata) - loadedpatch = patchdata.patch - patch = {} - reverse_patch = {} - for k, v in loadedpatch.items(): - if type(k) is tuple: - for name in k: - patch[name] = v - reverse_patch[v] = k[0] - else: - patch[k] = v - reverse_patch[v] = k - -# importing patch will load initial data -reload_data() -
--- a/light8/dmxclient.py Sun Apr 10 07:31:15 2005 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,53 +0,0 @@ -""" module for clients to use for easy talking to the dmx -server. sending levels is now a simple call to -dmxclient.outputlevels(..) - -client id is formed from sys.argv[0] and the PID. """ - -import xmlrpclib,os,sys,socket,time -from twisted.web.xmlrpc import Proxy -_dmx=None - -_id="%s-%s" % (sys.argv[0].replace('.py','').replace('./',''),os.getpid()) - -def outputlevels(levellist,twisted=0,clientid=_id): - """present a list of dmx channel levels, each scaled from - 0..1. list can be any length- it will apply to the first len() dmx - channels. - - if the server is not found, outputlevels will block for a - second.""" - - global _dmx,_id - - if _dmx is None: - host = os.getenv('DMXHOST', 'localhost') - url = "http://%s:8030" % host - if not twisted: - _dmx=xmlrpclib.Server(url) - else: - _dmx = Proxy(url) - - if not twisted: - try: - _dmx.outputlevels(clientid,levellist) - except socket.error,e: - print "dmx server error %s, waiting"%e - time.sleep(1) - except xmlrpclib.Fault,e: - print "outputlevels had xml fault: %s" % e - time.sleep(1) - else: - def err(error): - print "dmx server error",error - time.sleep(1) - d = _dmx.callRemote('outputlevels',clientid,levellist) - d.addErrback(err) - - -dummy = os.getenv('DMXDUMMY') -if dummy: - print "dmxclient: DMX is in dummy mode." - def bogus(*args): - pass - outputlevels = bogus
--- a/light8/dmxserver.py Sun Apr 10 07:31:15 2005 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,186 +0,0 @@ -#!/usr/bin/python -""" - -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 __future__ import division -from twisted.internet import reactor -from twisted.web import xmlrpc, server -import sys,time,os -from optparse import OptionParser -from io import ParportDMX -from updatefreq import Updatefreq - -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=ParportDMX() - 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=self.clientlevels.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)) - del self.clientlevels[cid] - 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() - - if self.clientschanged or time.time()>self.lastupdate+1: - 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 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]) - - 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("\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 - if cid not in self.lastseen: - print "hello new client %s" % cid - self.clientfreq[cid]=Updatefreq() - - self.lastseen[cid]=time.time() - self.clientfreq[cid].update() - return "ok" - -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, - help=('dmx output frequency')) -(options,songfiles)=parser.parse_args() - -print options - -print "starting xmlrpc server on port 8030" -reactor.listenTCP(8030,server.Site(XMLRPCServe(options))) -reactor.run() -
--- a/light8/io.py Sun Apr 10 07:31:15 2005 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,109 +0,0 @@ - - -class BaseIO: - 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) - import parport - self.parport = parport - self.parport.getparport() - - def sendlevels(self, levels): - if self.dummy: - return - - levels = list(levels) + [0] - # if levels[14] > 0: levels[14] = 100 # non-dim - self.parport.outstart() - for p in range(1, self.dimmers + 2): - self.parport.outbyte(levels[p-1]*255 / 100) - -class SerialPots(BaseIO): - """ - this is a dummy object (that returns zeros forever) until you call startup() - which makes it bind to the port, etc - - """ - def __init__(self): - # no init here- call getport() to actually initialize - self.dummy=1 - self.__name__='SerialPots' # i thought this was automatic! - - def golive(self): - """ - ls -l /dev/i2c-0 - crw-rw-rw- 1 root root 89, 0 Jul 11 12:27 /dev/i2c-0 - """ - import serport - self.serport = serport - - self.f = open("/dev/i2c-0","rw") - - # this is for a chip with A0,A1,A2 lines all low: - port = 72 - - from fcntl import * - - I2C_SLAVE = 0x0703 #/* Change slave address */ - ioctl(self.f,I2C_SLAVE,port) - self.dummy=0 - - def godummy(self): - BaseIO.godummy(self) - self.f.close() - - def getlevels(self): - if self.dummy: - return (0,0,0,0) - else: - return self.serport.read_all_adc(self.f.fileno()) - - -if __name__=='__main__': - - """ tester program that just dumps levels for a while """ - from time import sleep - from serport import * - - i=0 - while i<100: - sleep(.033) - i=i+1 - - print read_all_adc(f.fileno()) -
--- a/light8/parport.c Sun Apr 10 07:31:15 2005 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,65 +0,0 @@ -#include <stdio.h> -#include <stdlib.h> -#include <unistd.h> -#include <sys/ioctl.h> -#include <asm/io.h> -#include <fcntl.h> -#include <Python.h> - -int getparport() { - if( ioperm(888,3,1) ) { - printf("Couldn't get parallel port at 888-890\n"); - - // the following doesn't have any effect! - PyErr_SetString(PyExc_IOError,"Couldn't get parallel port at 888-890"); - return 0; - } - return 1; -} - -void outdata(unsigned char val) { - outb(val,888); -} - -void outcontrol( unsigned char val ) { - outb(val,890); -} - -void outbyte( unsigned char val ) { - // set data, raise clock, lower clock - outdata(val); - outcontrol(2); - outcontrol(2); - outcontrol(2); - outcontrol(2); - outcontrol(2); - outcontrol(2); - outcontrol(2); - outcontrol(2); - outcontrol(2); - outcontrol(2); - outcontrol(2); - outcontrol(2); - outcontrol(2); - outcontrol(2); - outcontrol(2); - outcontrol(2); - outcontrol(2); - outcontrol(2); - outcontrol(2); - outcontrol(2); - outcontrol(2); - outcontrol(2); - outcontrol(2); - outcontrol(2); - outcontrol(2); - outcontrol(2); - outcontrol(3); -} -void outstart() { - // send start code: pin 14 high, 5ms to let a dmx cycle finish, - // then pin14 low (pin1 stays low) - outcontrol(1); - usleep(5000); - outcontrol(3); -}
--- a/light8/parport.i Sun Apr 10 07:31:15 2005 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,11 +0,0 @@ -%module parport - - -extern void getparport(); -extern void outdata( unsigned char val); -extern void outcontrol( unsigned char val ); -extern void outbyte( unsigned char val ); -extern void outstart(); - - -
--- a/light8/serport.i Sun Apr 10 07:31:15 2005 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,50 +0,0 @@ -%module serport - -%{ -#include <sys/types.h> -#include <sys/stat.h> -#include <sys/ioctl.h> -#include <fcntl.h> -#include <linux/i2c.h> -#include <linux/i2c-dev.h> -#include <unistd.h> -%} - - -%typemap(python,in) __u8 { - if( !PyInt_Check($input)) { - PyErr_SetString(PyExc_TypeError,"not an integer"); - return NULL; - } - $1 = ($type) PyInt_AsLong($input); -} - -%typemap(python,out) __s32 { - $result = Py_BuildValue("i", ($type) $1); -} - -%inline %{ - - __s32 i2c_smbus_write_byte(int file, __u8 value); - __s32 i2c_smbus_read_byte(int file); - - PyObject *read_all_adc(int file) { - PyObject *t=PyTuple_New(4); - - #define CHAN_TO_TUPLE_POS(chan,idx) i2c_smbus_write_byte(file, chan);\ - PyTuple_SetItem(t,idx,PyInt_FromLong(i2c_smbus_read_byte(file))); - - /* - these are shuffled here to match the way the pots read in. in - the returned tuple, 0=left pot..3=right pot. - */ - CHAN_TO_TUPLE_POS(1,0) - CHAN_TO_TUPLE_POS(2,1) - CHAN_TO_TUPLE_POS(3,2) - CHAN_TO_TUPLE_POS(0,3) - - return t; - - } - -%}
--- a/light8/updatefreq.py Sun Apr 10 07:31:15 2005 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,33 +0,0 @@ -"""calculates your updates-per-second""" - -import time - -class Updatefreq: - """make one of these, call update() on it as much as you want, - and then float() or str() the object to learn the updates per second. - - the samples param to __init__ specifies how many past updates will - be stored. """ - - def __init__(self,samples=20): - self.times=[0] - self.samples=samples - - def update(self): - - """call this every time you do an update""" - self.times=self.times[-self.samples:] - self.times.append(time.time()) - - def __float__(self): - - """a cheap algorithm, for now, which looks at the first and - last times only""" - - try: - hz=len(self.times)/(self.times[-1]-self.times[0]) - except ZeroDivisionError: - return 0 - return hz - def __str__(self): - return "%.2fHz"%float(self)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/light9/Patch.py Sun Apr 10 19:12:57 2005 +0000 @@ -0,0 +1,46 @@ +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 name in patch: + return patch[name] + + try: + i = int(name) + return i + except ValueError: + raise ValueError("Invalid channel name: %s" % name) + +def get_channel_name(dmxnum): + try: + return reverse_patch[dmxnum] + except KeyError: + return str(dmxnum) + +def reload_data(): + global patch, reverse_patch + import patchdata + + reload(patchdata) + loadedpatch = patchdata.patch + patch = {} + reverse_patch = {} + for k, v in loadedpatch.items(): + if type(k) is tuple: + for name in k: + patch[name] = v + reverse_patch[v] = k[0] + else: + patch[k] = v + reverse_patch[v] = k + +# importing patch will load initial data +reload_data() +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/light9/Submaster.py Sun Apr 10 19:12:57 2005 +0000 @@ -0,0 +1,157 @@ +from __future__ import division +from light9.TLUtility import dict_scale, dict_max +from light9 import Patch + +class Submaster: + "Contain a dictionary of levels, but you didn't need to know that" + def __init__(self, name, leveldict=None, temporary=0): + self.name = name + self.temporary = temporary + if leveldict: + self.levels = leveldict + else: + self.levels = {} + self.reload() + def reload(self): + if self.temporary: + return + try: + self.levels.clear() + subfile = file("subs/%s" % self.name) + for line in subfile.readlines(): + if not line.strip(): # if line is only whitespace + continue # "did i say newspace?" + + try: + name, val = line.split(':') + name = name.strip() + self.levels[name] = float(val) + except ValueError: + print "(%s) Error with this line: %s" % (self.name, + line[:-1]) + except IOError: + print "Can't read file for sub: %s" % self.name + def save(self): + if self.temporary: + print "not saving temporary sub named",self.name + return + + subfile = file("subs/%s" % self.name, 'w') + names = self.levels.keys() + names.sort() + for name in names: + val = self.levels[name] + subfile.write("%s : %s\n" % (name, val)) + def set_level(self, channelname, level, save=1): + self.levels[Patch.resolve_name(channelname)] = level + if save: + self.save() + def set_all_levels(self, leveldict): + self.levels.clear() + for k, v in leveldict.items(): + self.set_level(k, v, save=0) + self.save() + def get_levels(self): + return self.levels + def __mul__(self, scalar): + return Submaster("%s*%s" % (self.name, scalar), + dict_scale(self.levels, scalar), temporary=1) + __rmul__ = __mul__ + def max(self, *othersubs): + return sub_maxes(self, *othersubs) + def __repr__(self): + levels = ' '.join(["%s:%.2f" % item for item in self.levels.items()]) + return "<'%s': [%s]>" % (self.name, levels) + def get_dmx_list(self): + leveldict = self.get_levels() # gets levels of sub contents + + levels = [0] * 68 + for k, v in leveldict.items(): + dmxchan = Patch.get_dmx_channel(k) - 1 + levels[dmxchan] = max(v, levels[dmxchan]) + + return levels + def normalize_patch_names(self): + """Use only the primary patch names.""" + # possibly busted -- don't use unless you know what you're doing + self.set_all_levels(self.levels.copy()) + def get_normalized_copy(self): + """Get a copy of this sumbaster that only uses the primary patch + names. The levels will be the same.""" + newsub = Submaster("%s (normalized)" % self.name, temporary=1) + newsub.set_all_levels(self.levels) + return newsub + def crossfade(self, othersub, amount): + """Returns a new sub that is a crossfade between this sub and + another submaster. + + NOTE: You should only crossfade between normalized submasters.""" + otherlevels = othersub.get_levels() + keys_set = {} + for k in self.levels.keys() + otherlevels.keys(): + keys_set[k] = 1 + all_keys = keys_set.keys() + + xfaded_sub = Submaster("xfade", temporary=1) + for k in all_keys: + xfaded_sub.set_level(k, + linear_fade(self.levels.get(k, 0), + otherlevels.get(k, 0), + amount)) + + return xfaded_sub + +def linear_fade(start, end, amount): + """Fades between two floats by an amount. amount is a float between + 0 and 1. If amount is 0, it will return the start value. If it is 1, + the end value will be returned.""" + level = start + (amount * (end - start)) + return level + +def sub_maxes(*subs): + return Submaster("max(%r)" % (subs,), + dict_max(*[sub.levels for sub in subs]), temporary=1) + +class Submasters: + "Collection o' Submaster objects" + def __init__(self): + self.submasters = {} + + import os + files = os.listdir('subs') + + for filename in files: + # we don't want these files + if filename.startswith('.') or filename.endswith('~') or \ + filename.startswith('CVS'): + continue + self.submasters[filename] = Submaster(filename) + def get_all_subs(self): + "All Submaster objects" + l = self.submasters.items() + l.sort() + l = [x[1] for x in l] + songs = [] + notsongs = [] + for s in l: + if s.name.startswith('song'): + songs.append(s) + else: + notsongs.append(s) + combined = notsongs + songs + return combined + def get_sub_by_name(self, name): + "Makes a new sub if there isn't one." + return self.submasters.get(name, Submaster(name)) + __getitem__ = get_sub_by_name + +if __name__ == "__main__": + Patch.reload_data() + s = Submasters() + print s.get_all_subs() + if 0: # turn this on to normalize all subs + for sub in s.get_all_subs(): + print "before", sub + sub.normalize_patch_names() + sub.save() + print "after", sub
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/light9/TLUtility.py Sun Apr 10 19:12:57 2005 +0000 @@ -0,0 +1,209 @@ +"""Collected utility functions, many are taken from Drew's utils.py in +Cuisine CVS and Hiss's Utility.py.""" + +from __future__ import generators +import sys + +__author__ = "David McClosky <dmcc@bigasterisk.com>, " + \ + "Drew Perttula <drewp@bigasterisk.com>" +__cvsid__ = "$Id: TLUtility.py,v 1.1 2003/05/25 08:25:35 dmcc Exp $" +__version__ = "$Revision: 1.1 $"[11:-2] + +def make_attributes_from_args(*argnames): + """ + This function simulates the effect of running + self.foo=foo + for each of the given argument names ('foo' in the example just + now). Now you can write: + def __init__(self,foo,bar,baz): + copy_to_attributes('foo','bar','baz') + ... + instead of: + def __init__(self,foo,bar,baz): + self.foo=foo + self.bar=bar + self.baz=baz + ... + """ + + callerlocals=sys._getframe(1).f_locals + callerself=callerlocals['self'] + for a in argnames: + try: + setattr(callerself,a,callerlocals[a]) + except KeyError: + raise KeyError, "Function has no argument '%s'" % a + +def enumerate(*collections): + """Generates an indexed series: (0,coll[0]), (1,coll[1]) ... + + this is a multi-list version of the code from the PEP: + enumerate(a,b) gives (0,a[0],b[0]), (1,a[1],b[1]) ... + """ + i = 0 + iters = [iter(collection) for collection in collections] + while 1: + yield [i,] + [iterator.next() for iterator in iters] + i += 1 + +def dumpobj(o): + """Prints all the object's non-callable attributes""" + print repr(o) + for a in [x for x in dir(o) if not callable(getattr(o, x))]: + try: + print " %20s: %s " % (a, getattr(o, a)) + except: + pass + print "" + +def dict_filter_update(d, **newitems): + """Adds a set of new keys and values to dictionary 'd' if the values are + true: + + >>> some_dict = {} + >>> dict_filter_update(some_dict, a=None, b=0, c=1, e={}, s='hello') + >>> some_dict + {'c': 1, 's': 'hello'} + """ + for k, v in newitems.items(): + if v: d[k] = v + +def try_get_logger(channel): + """Tries to get a logger with the channel 'channel'. Will return a + silent DummyClass if logging is not available.""" + try: + import logging + log = logging.getLogger(channel) + except ImportError: + log = DummyClass() + return log + +class DummyClass: + """A class that can be instantiated but never used. It is intended to + be replaced when information is available. + + Usage: + >>> d = DummyClass(1, 2, x="xyzzy") + >>> d.someattr + Traceback (most recent call last): + File "<stdin>", line 1, in ? + File "Utility.py", line 33, in __getattr__ + raise AttributeError, "Attempted usage of a DummyClass: %s" % key + AttributeError: Attempted usage of a DummyClass: someattr + >>> d.somefunction() + Traceback (most recent call last): + File "<stdin>", line 1, in ? + File "Utility.py", line 33, in __getattr__ + raise AttributeError, "Attempted usage of a DummyClass: %s" % key + AttributeError: Attempted usage of a DummyClass: somefunction""" + def __init__(self, use_warnings=1, raise_exceptions=0, **kw): + """Constructs a DummyClass""" + make_attributes_from_args('use_warnings', 'raise_exceptions') + def __getattr__(self, key): + """Raises an exception to warn the user that a Dummy is not being + replaced in time.""" + if key == "__del__": + return + msg = "Attempted usage of '%s' on a DummyClass" % key + if self.use_warnings: + import warnings + warnings.warn(msg) + if self.raise_exceptions: + raise AttributeError, msg + return lambda *args, **kw: self.bogus_function() + def bogus_function(self): + pass + +class ClassyDict(dict): + """A dict that accepts attribute-style access as well (for keys + that are legal names, obviously). I used to call this Struct, but + chose the more colorful name to avoid confusion with the struct + module.""" + def __getattr__(self, a): + return self[a] + def __setattr__(self, a, v): + self[a] = v + def __delattr__(self, a): + del self[a] + +def trace(func): + """Good old fashioned Lisp-style tracing. Example usage: + + >>> def f(a, b, c=3): + >>> print a, b, c + >>> return a + b + >>> + >>> + >>> f = trace(f) + >>> f(1, 2) + |>> f called args: [1, 2] + 1 2 3 + <<| f returned 3 + 3 + + TODO: print out default keywords (maybe) + indent for recursive call like the lisp version (possible use of + generators?)""" + name = func.func_name + def tracer(*args, **kw): + s = '|>> %s called' % name + if args: + s += ' args: %r' % list(args) + if kw: + s += ' kw: %r' % kw + print s + ret = func(*args, **kw) + print '<<| %s returned %s' % (name, ret) + return ret + return tracer + +# these functions taken from old light8 code +def dict_max(*dicts): + """ + ({'a' : 5, 'b' : 9}, {'a' : 10, 'b' : 4}) + returns ==> {'a' : 10, 'b' : 9} + """ + newdict = {} + for d in dicts: + for k,v in d.items(): + newdict[k] = max(v, newdict.get(k, 0)) + return newdict + +def dict_scale(d,scl): + """scales all values in dict and returns a new dict""" + return dict([(k,v*scl) for k,v in d.items()]) + +def dict_subset(d, dkeys, default=0): + """Subset of dictionary d: only the keys in dkeys. If you plan on omitting + keys, make sure you like the default.""" + newd = {} # dirty variables! + for k in dkeys: + newd[k] = d.get(k, default) + return newd + +# functions specific to Timeline +# TBD +def last_less_than(array, x): + """array must be sorted""" + best = None + for elt in array: + if elt <= x: + best = elt + elif best is not None: + return best + return best + +# TBD +def first_greater_than(array, x): + """array must be sorted""" + array_rev = array[:] + array_rev.reverse() + best = None + for elt in array_rev: + if elt >= x: + best = elt + elif best is not None: + return best + return best + +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/light9/dmxchanedit.py Sun Apr 10 19:12:57 2005 +0000 @@ -0,0 +1,130 @@ +""" + +widget to show all dmx channel levels and allow editing. levels might +not actually match what dmxserver is outputting. + +""" +from __future__ import nested_scopes,division +import Tkinter as tk +import sys +sys.path.append("../light8") +import Patch +from uihelpers import make_frame, colorlabel, eventtoparent +from dispatch import dispatcher + +stdfont = ('Arial', 12) + +class Onelevel(tk.Frame): + """a name/level pair""" + def __init__(self, parent, channelnum): + """channelnum is 1..68, like the real dmx""" + tk.Frame.__init__(self,parent) + + self.channelnum=channelnum + self.currentlevel=0 # the level we're displaying, 0..1 + + # 3 widgets, left-to-right: + + # channel number -- will turn yellow when being altered + self.num_lab = tk.Label(self, text=str(channelnum), + width=3, bg='grey40', + fg='white', font=stdfont, + padx=0, pady=0, bd=0, height=1) + self.num_lab.pack(side='left') + + # text description of channel + self.desc_lab=tk.Label(self, text=Patch.get_channel_name(channelnum), + width=14, font=stdfont, anchor='w', + padx=0, pady=0, bd=0, + height=1, bg='black', fg='white') + self.desc_lab.pack(side='left') + + # current level of channel, shows intensity with color + self.level_lab = tk.Label(self, width=3, bg='lightBlue', + font=stdfont, anchor='e', + padx=1, pady=0, bd=0, height=1) + self.level_lab.pack(side='left') + + self.setlevel(0) + self.setupmousebindings() + + def setupmousebindings(self): + def b1down(ev): + self.desc_lab.config(bg='cyan') + self._start_y=ev.y + self._start_lev=self.currentlevel + def b1motion(ev): + delta=self._start_y-ev.y + self.changelevel(self._start_lev+delta*.005) + def b1up(ev): + self.desc_lab.config(bg='black') + def b3up(ev): + self.changelevel(0.0) + def b3down(ev): + self.changelevel(1.0) + + # make the buttons work in the child windows + for w in self.winfo_children(): + for e,func in (('<ButtonPress-1>',b1down), + ('<B1-Motion>',b1motion), + ('<ButtonRelease-1>',b1up), + ('<ButtonRelease-3>', b3up), + ('<ButtonPress-3>', b3down)): + w.bind(e,func) +# w.bind(e,lambda ev,e=e: eventtoparent(ev,e)) + + def colorlabel(self): + """color the level label based on its own text (which is 0..100)""" + txt=self.level_lab['text'] or "0" + lev=float(txt)/100 + low=(80,80,180) + high=(255,55,050) + out = [int(l+lev*(h-l)) for h,l in zip(high,low)] + col="#%02X%02X%02X" % tuple(out) + self.level_lab.config(bg=col) + + def setlevel(self,newlev): + """the main program is telling us to change our + display. newlev is 0..1""" + self.currentlevel=newlev + newlev="%d"%(newlev*100) + olddisplay=self.level_lab.cget('text') + if newlev!=olddisplay: + self.level_lab.config(text=newlev) + self.colorlabel() + + def getlevel(self): + """returns currently displayed level, 0..1""" + return self.currentlevel + + def changelevel(self,newlev): + + """the user is adjusting the level on this widget. the main + program needs to hear about it. then the main program will + call setlevel()""" + + dispatcher.send("levelchanged",channel=self.channelnum,newlevel=newlev) + +class Levelbox(tk.Frame): + def __init__(self, parent, num_channels=68): + tk.Frame.__init__(self,parent) + + self.levels = [] # Onelevel objects + + frames = (make_frame(self), make_frame(self)) + + for channel in range(1, num_channels+1): + + # frame for this channel + f = Onelevel(frames[channel > (num_channels/2)],channel) + + self.levels.append(f) + f.pack(side='top') + + #dispatcher.connect(setalevel,"setlevel") + + def setlevels(self,newlevels): + """sets levels to the new list of dmx levels (0..1). list can + be any length""" + for l,newlev in zip(self.levels,newlevels): + l.setlevel(newlev)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/light9/dmxclient.py Sun Apr 10 19:12:57 2005 +0000 @@ -0,0 +1,53 @@ +""" module for clients to use for easy talking to the dmx +server. sending levels is now a simple call to +dmxclient.outputlevels(..) + +client id is formed from sys.argv[0] and the PID. """ + +import xmlrpclib,os,sys,socket,time +from twisted.web.xmlrpc import Proxy +_dmx=None + +_id="%s-%s" % (sys.argv[0].replace('.py','').replace('./',''),os.getpid()) + +def outputlevels(levellist,twisted=0,clientid=_id): + """present a list of dmx channel levels, each scaled from + 0..1. list can be any length- it will apply to the first len() dmx + channels. + + if the server is not found, outputlevels will block for a + second.""" + + global _dmx,_id + + if _dmx is None: + host = os.getenv('DMXHOST', 'localhost') + url = "http://%s:8030" % host + if not twisted: + _dmx=xmlrpclib.Server(url) + else: + _dmx = Proxy(url) + + if not twisted: + try: + _dmx.outputlevels(clientid,levellist) + except socket.error,e: + print "dmx server error %s, waiting"%e + time.sleep(1) + except xmlrpclib.Fault,e: + print "outputlevels had xml fault: %s" % e + time.sleep(1) + else: + def err(error): + print "dmx server error",error + time.sleep(1) + d = _dmx.callRemote('outputlevels',clientid,levellist) + d.addErrback(err) + + +dummy = os.getenv('DMXDUMMY') +if dummy: + print "dmxclient: DMX is in dummy mode." + def bogus(*args): + pass + outputlevels = bogus
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/light9/io/Makefile Sun Apr 10 19:12:57 2005 +0000 @@ -0,0 +1,20 @@ +LIB=/usr/local/lib +INC=-I/usr/local/include/python2.3 + +go: _parport.so _serport.so + result="your modules and links are now up to date" + +_parport.so: parport_wrap.c + gcc -shared ${INC} parport_wrap.c parport.c -o _parport.so + +parport_wrap.c: parport.c parport.i + swig -python parport.i + +_serport.so: serport_wrap.c + gcc -shared -O ${INC} serport_wrap.c -o _serport.so + +serport_wrap.c: serport.i + swig -python serport.i + +clean: + rm -f parport_wrap.c serport_wrap.c *.o *.so
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/light9/io/__init__.py Sun Apr 10 19:12:57 2005 +0000 @@ -0,0 +1,109 @@ + + +class BaseIO: + 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) + import parport + self.parport = parport + self.parport.getparport() + + def sendlevels(self, levels): + if self.dummy: + return + + levels = list(levels) + [0] + # if levels[14] > 0: levels[14] = 100 # non-dim + self.parport.outstart() + for p in range(1, self.dimmers + 2): + self.parport.outbyte(levels[p-1]*255 / 100) + +class SerialPots(BaseIO): + """ + this is a dummy object (that returns zeros forever) until you call startup() + which makes it bind to the port, etc + + """ + def __init__(self): + # no init here- call getport() to actually initialize + self.dummy=1 + self.__name__='SerialPots' # i thought this was automatic! + + def golive(self): + """ + ls -l /dev/i2c-0 + crw-rw-rw- 1 root root 89, 0 Jul 11 12:27 /dev/i2c-0 + """ + import serport + self.serport = serport + + self.f = open("/dev/i2c-0","rw") + + # this is for a chip with A0,A1,A2 lines all low: + port = 72 + + from fcntl import * + + I2C_SLAVE = 0x0703 #/* Change slave address */ + ioctl(self.f,I2C_SLAVE,port) + self.dummy=0 + + def godummy(self): + BaseIO.godummy(self) + self.f.close() + + def getlevels(self): + if self.dummy: + return (0,0,0,0) + else: + return self.serport.read_all_adc(self.f.fileno()) + + +if __name__=='__main__': + + """ tester program that just dumps levels for a while """ + from time import sleep + from serport import * + + i=0 + while i<100: + sleep(.033) + i=i+1 + + print read_all_adc(f.fileno()) +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/light9/io/parport.c Sun Apr 10 19:12:57 2005 +0000 @@ -0,0 +1,65 @@ +#include <stdio.h> +#include <stdlib.h> +#include <unistd.h> +#include <sys/ioctl.h> +#include <asm/io.h> +#include <fcntl.h> +#include <Python.h> + +int getparport() { + if( ioperm(888,3,1) ) { + printf("Couldn't get parallel port at 888-890\n"); + + // the following doesn't have any effect! + PyErr_SetString(PyExc_IOError,"Couldn't get parallel port at 888-890"); + return 0; + } + return 1; +} + +void outdata(unsigned char val) { + outb(val,888); +} + +void outcontrol( unsigned char val ) { + outb(val,890); +} + +void outbyte( unsigned char val ) { + // set data, raise clock, lower clock + outdata(val); + outcontrol(2); + outcontrol(2); + outcontrol(2); + outcontrol(2); + outcontrol(2); + outcontrol(2); + outcontrol(2); + outcontrol(2); + outcontrol(2); + outcontrol(2); + outcontrol(2); + outcontrol(2); + outcontrol(2); + outcontrol(2); + outcontrol(2); + outcontrol(2); + outcontrol(2); + outcontrol(2); + outcontrol(2); + outcontrol(2); + outcontrol(2); + outcontrol(2); + outcontrol(2); + outcontrol(2); + outcontrol(2); + outcontrol(2); + outcontrol(3); +} +void outstart() { + // send start code: pin 14 high, 5ms to let a dmx cycle finish, + // then pin14 low (pin1 stays low) + outcontrol(1); + usleep(5000); + outcontrol(3); +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/light9/io/parport.i Sun Apr 10 19:12:57 2005 +0000 @@ -0,0 +1,11 @@ +%module parport + + +extern void getparport(); +extern void outdata( unsigned char val); +extern void outcontrol( unsigned char val ); +extern void outbyte( unsigned char val ); +extern void outstart(); + + +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/light9/io/serport.i Sun Apr 10 19:12:57 2005 +0000 @@ -0,0 +1,50 @@ +%module serport + +%{ +#include <sys/types.h> +#include <sys/stat.h> +#include <sys/ioctl.h> +#include <fcntl.h> +#include <linux/i2c.h> +#include <linux/i2c-dev.h> +#include <unistd.h> +%} + + +%typemap(python,in) __u8 { + if( !PyInt_Check($input)) { + PyErr_SetString(PyExc_TypeError,"not an integer"); + return NULL; + } + $1 = ($type) PyInt_AsLong($input); +} + +%typemap(python,out) __s32 { + $result = Py_BuildValue("i", ($type) $1); +} + +%inline %{ + + __s32 i2c_smbus_write_byte(int file, __u8 value); + __s32 i2c_smbus_read_byte(int file); + + PyObject *read_all_adc(int file) { + PyObject *t=PyTuple_New(4); + + #define CHAN_TO_TUPLE_POS(chan,idx) i2c_smbus_write_byte(file, chan);\ + PyTuple_SetItem(t,idx,PyInt_FromLong(i2c_smbus_read_byte(file))); + + /* + these are shuffled here to match the way the pots read in. in + the returned tuple, 0=left pot..3=right pot. + */ + CHAN_TO_TUPLE_POS(1,0) + CHAN_TO_TUPLE_POS(2,1) + CHAN_TO_TUPLE_POS(3,2) + CHAN_TO_TUPLE_POS(0,3) + + return t; + + } + +%}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/light9/updatefreq.py Sun Apr 10 19:12:57 2005 +0000 @@ -0,0 +1,33 @@ +"""calculates your updates-per-second""" + +import time + +class Updatefreq: + """make one of these, call update() on it as much as you want, + and then float() or str() the object to learn the updates per second. + + the samples param to __init__ specifies how many past updates will + be stored. """ + + def __init__(self,samples=20): + self.times=[0] + self.samples=samples + + def update(self): + + """call this every time you do an update""" + self.times=self.times[-self.samples:] + self.times.append(time.time()) + + def __float__(self): + + """a cheap algorithm, for now, which looks at the first and + last times only""" + + try: + hz=len(self.times)/(self.times[-1]-self.times[0]) + except ZeroDivisionError: + return 0 + return hz + def __str__(self): + return "%.2fHz"%float(self)