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)