Files @ d4a07ad96aad
Branch filter:

Location: light9/bin/dmxserver - annotation

drewp@bigasterisk.com
a301a0039c66
1a84c5e83d3e
a5a44077c54c
a5a44077c54c
1a84c5e83d3e
1a84c5e83d3e
1a84c5e83d3e
1a84c5e83d3e
1a84c5e83d3e
1a84c5e83d3e
1a84c5e83d3e
1a84c5e83d3e
1a84c5e83d3e
1a84c5e83d3e
1a84c5e83d3e
1a84c5e83d3e
1a84c5e83d3e
1a84c5e83d3e
1a84c5e83d3e
1a84c5e83d3e
1a84c5e83d3e
1a84c5e83d3e
1a84c5e83d3e
1a84c5e83d3e
1a84c5e83d3e
1a84c5e83d3e
1a84c5e83d3e
1a84c5e83d3e
7772cc48e016
1a84c5e83d3e
1a84c5e83d3e
7772cc48e016
44189a37a876
44189a37a876
1a84c5e83d3e
f41004d5a507
1a84c5e83d3e
d5e99fee786d
d5e99fee786d
d5e99fee786d
7772cc48e016
d5e99fee786d
d5e99fee786d
d5e99fee786d
d5e99fee786d
7772cc48e016
d5e99fee786d
d5e99fee786d
d5e99fee786d
7772cc48e016
d5e99fee786d
ba47676dde49
7772cc48e016
ba47676dde49
ba47676dde49
ba47676dde49
ba47676dde49
ba47676dde49
ba47676dde49
7772cc48e016
ba47676dde49
ba47676dde49
ba47676dde49
ba47676dde49
ba47676dde49
ba47676dde49
ba47676dde49
7772cc48e016
ba47676dde49
f066d6e874db
ba47676dde49
ba47676dde49
ba47676dde49
ba47676dde49
ba47676dde49
ba47676dde49
7772cc48e016
7772cc48e016
ba47676dde49
1a84c5e83d3e
7772cc48e016
7772cc48e016
1a84c5e83d3e
1a84c5e83d3e
7772cc48e016
7772cc48e016
7772cc48e016
7772cc48e016
7772cc48e016
7772cc48e016
7772cc48e016
7772cc48e016
7772cc48e016
7772cc48e016
1a84c5e83d3e
1a84c5e83d3e
7772cc48e016
1a84c5e83d3e
f066d6e874db
ce159eea90b5
7772cc48e016
1a84c5e83d3e
1a84c5e83d3e
1a84c5e83d3e
1a84c5e83d3e
7772cc48e016
7772cc48e016
7772cc48e016
1a84c5e83d3e
1a84c5e83d3e
1a84c5e83d3e
1a84c5e83d3e
1a84c5e83d3e
7772cc48e016
1a84c5e83d3e
1a84c5e83d3e
1a84c5e83d3e
1a84c5e83d3e
7772cc48e016
1a84c5e83d3e
7772cc48e016
7772cc48e016
7772cc48e016
f066d6e874db
1a84c5e83d3e
7772cc48e016
941cfe1e1691
f066d6e874db
5bcb950024af
941cfe1e1691
941cfe1e1691
941cfe1e1691
941cfe1e1691
1a84c5e83d3e
1a84c5e83d3e
7772cc48e016
1a84c5e83d3e
1a84c5e83d3e
1a84c5e83d3e
1a84c5e83d3e
7772cc48e016
1a84c5e83d3e
1a84c5e83d3e
1a84c5e83d3e
1a84c5e83d3e
1a84c5e83d3e
7772cc48e016
7772cc48e016
7772cc48e016
7772cc48e016
7772cc48e016
7772cc48e016
7772cc48e016
1a84c5e83d3e
7772cc48e016
1a84c5e83d3e
7772cc48e016
1a84c5e83d3e
7772cc48e016
7772cc48e016
1a84c5e83d3e
1a84c5e83d3e
44189a37a876
44189a37a876
7772cc48e016
7772cc48e016
7772cc48e016
1a84c5e83d3e
1a84c5e83d3e
7772cc48e016
7772cc48e016
1a84c5e83d3e
1a84c5e83d3e
7772cc48e016
7772cc48e016
7772cc48e016
f066d6e874db
7772cc48e016
1a84c5e83d3e
7772cc48e016
7772cc48e016
1a84c5e83d3e
1a84c5e83d3e
1a84c5e83d3e
1a84c5e83d3e
5bcb950024af
5bcb950024af
7772cc48e016
1a84c5e83d3e
1a84c5e83d3e
1a84c5e83d3e
7772cc48e016
7772cc48e016
7772cc48e016
7772cc48e016
f066d6e874db
7772cc48e016
1a84c5e83d3e
1a84c5e83d3e
1a84c5e83d3e
1a84c5e83d3e
1a84c5e83d3e
1a84c5e83d3e
1a84c5e83d3e
7772cc48e016
1a84c5e83d3e
7772cc48e016
7772cc48e016
1a84c5e83d3e
7772cc48e016
7772cc48e016
1a84c5e83d3e
1a84c5e83d3e
7772cc48e016
7772cc48e016
7772cc48e016
941cfe1e1691
941cfe1e1691
941cfe1e1691
941cfe1e1691
941cfe1e1691
941cfe1e1691
941cfe1e1691
941cfe1e1691
941cfe1e1691
941cfe1e1691
941cfe1e1691
941cfe1e1691
941cfe1e1691
941cfe1e1691
941cfe1e1691
941cfe1e1691
941cfe1e1691
7772cc48e016
941cfe1e1691
7772cc48e016
941cfe1e1691
941cfe1e1691
f066d6e874db
7772cc48e016
7772cc48e016
1a84c5e83d3e
7772cc48e016
9d1f323fb3d3
7772cc48e016
7772cc48e016
7772cc48e016
7772cc48e016
1a84c5e83d3e
1a84c5e83d3e
7772cc48e016
7772cc48e016
7772cc48e016
7772cc48e016
1a84c5e83d3e
7772cc48e016
7772cc48e016
7772cc48e016
e263c4bd73f9
7772cc48e016
7772cc48e016
7772cc48e016
632539a8c30e
7772cc48e016
1a84c5e83d3e
f066d6e874db
1a84c5e83d3e
632539a8c30e
632539a8c30e
632539a8c30e
46d319974176
f066d6e874db
ba47676dde49
7772cc48e016
ba47676dde49
d5e99fee786d
7772cc48e016
ba47676dde49
ba47676dde49
1a84c5e83d3e
#!bin/python
"""
Replaced by bin/collector


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

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

clients shall connect to the xmlrpc server and send:

  their PID (or some other cookie)

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

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

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

from twisted.internet import reactor
from twisted.web import xmlrpc, server
import sys, time, os
from optparse import OptionParser
import run_local
import txosc.dispatch, txosc. async
from light9.io import ParportDMX, UsbDMX

from light9.updatefreq import Updatefreq
from light9 import networking

from txzmq import ZmqEndpoint, ZmqFactory, ZmqPullConnection, ZmqRequestTimeoutError
import json


def startZmq(port, outputlevels):
    zf = ZmqFactory()
    e = ZmqEndpoint('bind', 'tcp://*:%s' % port)
    s = ZmqPullConnection(zf, e)

    def onPull(message):
        msg = json.loads(message[0])
        outputlevels(msg['clientid'], msg['levellist'])

    s.onPull = onPull


class ReceiverApplication(object):
    """
    receive UDP OSC messages. address is /dmx/1 for dmx channel 1,
    arguments are 0-1 floats for that channel and any number of
    following channels.
    """

    def __init__(self, port, lightServer):
        self.port = port
        self.lightServer = lightServer
        self.receiver = txosc.dispatch.Receiver()
        self.receiver.addCallback("/dmx/*", self.pixel_handler)
        self._server_port = reactor.listenUDP(
            self.port,
            txosc. async .DatagramServerProtocol(self.receiver),
            interface='0.0.0.0')
        print("Listening OSC on udp port %s" % (self.port))

    def pixel_handler(self, message, address):
        # this is already 1-based though I don't know why
        startChannel = int(message.address.split('/')[2])
        levels = [a.value for a in message.arguments]
        allLevels = [0] * (startChannel - 1) + levels
        self.lightServer.xmlrpc_outputlevels("osc@%s" % startChannel, allLevels)


class XMLRPCServe(xmlrpc.XMLRPC):

    def __init__(self, options):

        xmlrpc.XMLRPC.__init__(self)

        self.clientlevels = {}  # clientID : list of levels
        self.lastseen = {}  # clientID : time last seen
        self.clientfreq = {}  # clientID : updatefreq

        self.combinedlevels = []  # list of levels, after max'ing the clients
        self.clientschanged = 1  # have clients sent anything since the last send?
        self.options = options
        self.lastupdate = 0  # time of last dmx send
        self.laststatsprint = 0  # time

        # desired seconds between sendlevels() calls
        self.calldelay = 1 / options.updates_per_sec

        print("starting parport connection")
        self.parportdmx = UsbDMX(dimmers=90, port=options.dmx_device)
        if os.environ.get('DMXDUMMY', 0):
            self.parportdmx.godummy()
        else:
            self.parportdmx.golive()

        self.updatefreq = Updatefreq()  # freq of actual dmx sends
        self.num_unshown_updates = None
        self.lastshownlevels = None
        # start the loop
        self.sendlevels()

        # the other loop
        self.purgeclients()

    def purgeclients(self):
        """forget about any clients who haven't sent levels in a while.
        this runs in a loop"""

        purge_age = 10  # seconds

        reactor.callLater(1, self.purgeclients)

        now = time.time()
        cids = list(self.lastseen.keys())
        for cid in cids:
            lastseen = self.lastseen[cid]
            if lastseen < now - purge_age:
                print(("forgetting client %s (no activity for %s sec)" %
                       (cid, purge_age)))
                try:
                    del self.clientlevels[cid]
                except KeyError:
                    pass
                del self.clientfreq[cid]
                del self.lastseen[cid]

    def sendlevels(self):
        """sends to dmx if levels have changed, or if we havent sent
        in a while"""

        reactor.callLater(self.calldelay, self.sendlevels)

        if self.clientschanged:
            # recalc levels

            self.calclevels()

            if (self.num_unshown_updates is None or  # first time
                    self.options.fast_updates or  # show always
                (
                    self.combinedlevels != self.lastshownlevels and  # changed
                    self.num_unshown_updates > 5)):  # not too frequent
                self.num_unshown_updates = 0
                self.printlevels()
                self.lastshownlevels = self.combinedlevels[:]
            else:
                self.num_unshown_updates += 1

        if time.time() > self.laststatsprint + 2:
            self.laststatsprint = time.time()
            self.printstats()

        # used to be a fixed 1 in here, for the max delay between
        # calls, instead of calldelay
        if self.clientschanged or time.time(
        ) > self.lastupdate + self.calldelay:
            self.lastupdate = time.time()
            self.sendlevels_dmx()

        self.clientschanged = 0  # clear the flag

    def calclevels(self):
        """combine all the known client levels into self.combinedlevels"""
        self.combinedlevels = []
        for chan in range(0, self.parportdmx.dimmers):
            x = 0
            for clientlist in list(self.clientlevels.values()):
                if len(clientlist) > chan:
                    # clamp client levels to 0..1
                    cl = max(0, min(1, clientlist[chan]))
                    x = max(x, cl)
            self.combinedlevels.append(x)

    def printlevels(self):
        """write all the levels to stdout"""
        print("Levels:",
              "".join(["% 2d " % (x * 100) for x in self.combinedlevels]))

    def printstats(self):
        """print the clock, freq, etc, with a \r at the end"""

        sys.stdout.write("dmxserver up at %s, [polls %s] " % (
            time.strftime("%H:%M:%S"),
            str(self.updatefreq),
        ))
        for cid, freq in list(self.clientfreq.items()):
            sys.stdout.write("[%s %s] " % (cid, str(freq)))
        sys.stdout.write("\r")
        sys.stdout.flush()

    def sendlevels_dmx(self):
        """output self.combinedlevels to dmx, and keep the updates/sec stats"""
        # they'll get divided by 100
        if self.parportdmx:
            self.parportdmx.sendlevels([l * 100 for l in self.combinedlevels])
        self.updatefreq.update()

    def xmlrpc_echo(self, x):
        return x

    def xmlrpc_outputlevels(self, cid, levellist):
        """send a unique id for your client (name+pid maybe), then
        the variable-length dmx levellist (scaled 0..1)"""
        if levellist != self.clientlevels.get(cid, None):
            self.clientlevels[cid] = levellist
            self.clientschanged = 1
        self.trackClientFreq(cid)
        return "ok"

    def xmlrpc_currentlevels(self, cid):
        """get a list of levels we're currently sending out. All
        channels beyond the list you get back, they're at zero."""
        # if this is still too slow, it might help to return a single
        # pickled string
        self.trackClientFreq(cid)
        trunc = self.combinedlevels[:]
        i = len(trunc) - 1
        if i < 0:
            return []
        while trunc[i] == 0 and i >= 0:
            i -= 1
        if i < 0:
            return []
        trunc = trunc[:i + 1]
        return trunc

    def trackClientFreq(self, cid):
        if cid not in self.lastseen:
            print("hello new client %s" % cid)
            self.clientfreq[cid] = Updatefreq()
        self.lastseen[cid] = time.time()
        self.clientfreq[cid].update()


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'))
parser.add_option("-d",
                  "--dmx-device",
                  default='/dev/dmx0',
                  help='dmx device name')
parser.add_option("-n",
                  "--dummy",
                  action="store_true",
                  help="dummy mode, same as DMXDUMMY=1 env variable")
(options, songfiles) = parser.parse_args()

print(options)

if options.dummy:
    os.environ['DMXDUMMY'] = "1"

port = networking.dmxServer.port
print("starting xmlrpc server on port %s" % port)
xmlrpcServe = XMLRPCServe(options)
reactor.listenTCP(port, server.Site(xmlrpcServe))

startZmq(networking.dmxServerZmq.port, xmlrpcServe.xmlrpc_outputlevels)

oscApp = ReceiverApplication(9051, xmlrpcServe)

reactor.run()