comparison bin/attic/dmxserver @ 2376:4556eebe5d73

topdir reorgs; let pdm have its src/ dir; separate vite area from light9/
author drewp@bigasterisk.com
date Sun, 12 May 2024 19:02:10 -0700
parents bin/dmxserver@5bcb950024af
children
comparison
equal deleted inserted replaced
2375:623836db99af 2376:4556eebe5d73
1 #!bin/python
2 """
3 Replaced by bin/collector
4
5
6 this is the only process to talk to the dmx hardware. other clients
7 can connect to this server and present dmx output, and this server
8 will max ('pile-on') all the client requests.
9
10 this server has a level display which is the final set of values that
11 goes to the hardware.
12
13 clients shall connect to the xmlrpc server and send:
14
15 their PID (or some other cookie)
16
17 a length-n list of 0..1 levels which will represent the channel
18 values for the n first dmx channels.
19
20 server is port 8030; xmlrpc method is called outputlevels(pid,levellist).
21
22 todo:
23 save dmx on quit and restore on restart
24 if parport fails, run in dummy mode (and make an option for that too)
25 """
26
27 from twisted.internet import reactor
28 from twisted.web import xmlrpc, server
29 import sys, time, os
30 from optparse import OptionParser
31 import run_local
32 import txosc.dispatch, txosc. async
33 from light9.io import ParportDMX, UsbDMX
34
35 from light9.updatefreq import Updatefreq
36 from light9 import networking
37
38 from txzmq import ZmqEndpoint, ZmqFactory, ZmqPullConnection, ZmqRequestTimeoutError
39 import json
40
41
42 def startZmq(port, outputlevels):
43 zf = ZmqFactory()
44 e = ZmqEndpoint('bind', 'tcp://*:%s' % port)
45 s = ZmqPullConnection(zf, e)
46
47 def onPull(message):
48 msg = json.loads(message[0])
49 outputlevels(msg['clientid'], msg['levellist'])
50
51 s.onPull = onPull
52
53
54 class ReceiverApplication(object):
55 """
56 receive UDP OSC messages. address is /dmx/1 for dmx channel 1,
57 arguments are 0-1 floats for that channel and any number of
58 following channels.
59 """
60
61 def __init__(self, port, lightServer):
62 self.port = port
63 self.lightServer = lightServer
64 self.receiver = txosc.dispatch.Receiver()
65 self.receiver.addCallback("/dmx/*", self.pixel_handler)
66 self._server_port = reactor.listenUDP(
67 self.port,
68 txosc. async .DatagramServerProtocol(self.receiver),
69 interface='0.0.0.0')
70 print("Listening OSC on udp port %s" % (self.port))
71
72 def pixel_handler(self, message, address):
73 # this is already 1-based though I don't know why
74 startChannel = int(message.address.split('/')[2])
75 levels = [a.value for a in message.arguments]
76 allLevels = [0] * (startChannel - 1) + levels
77 self.lightServer.xmlrpc_outputlevels("osc@%s" % startChannel, allLevels)
78
79
80 class XMLRPCServe(xmlrpc.XMLRPC):
81
82 def __init__(self, options):
83
84 xmlrpc.XMLRPC.__init__(self)
85
86 self.clientlevels = {} # clientID : list of levels
87 self.lastseen = {} # clientID : time last seen
88 self.clientfreq = {} # clientID : updatefreq
89
90 self.combinedlevels = [] # list of levels, after max'ing the clients
91 self.clientschanged = 1 # have clients sent anything since the last send?
92 self.options = options
93 self.lastupdate = 0 # time of last dmx send
94 self.laststatsprint = 0 # time
95
96 # desired seconds between sendlevels() calls
97 self.calldelay = 1 / options.updates_per_sec
98
99 print("starting parport connection")
100 self.parportdmx = UsbDMX(dimmers=90, port=options.dmx_device)
101 if os.environ.get('DMXDUMMY', 0):
102 self.parportdmx.godummy()
103 else:
104 self.parportdmx.golive()
105
106 self.updatefreq = Updatefreq() # freq of actual dmx sends
107 self.num_unshown_updates = None
108 self.lastshownlevels = None
109 # start the loop
110 self.sendlevels()
111
112 # the other loop
113 self.purgeclients()
114
115 def purgeclients(self):
116 """forget about any clients who haven't sent levels in a while.
117 this runs in a loop"""
118
119 purge_age = 10 # seconds
120
121 reactor.callLater(1, self.purgeclients)
122
123 now = time.time()
124 cids = list(self.lastseen.keys())
125 for cid in cids:
126 lastseen = self.lastseen[cid]
127 if lastseen < now - purge_age:
128 print(("forgetting client %s (no activity for %s sec)" %
129 (cid, purge_age)))
130 try:
131 del self.clientlevels[cid]
132 except KeyError:
133 pass
134 del self.clientfreq[cid]
135 del self.lastseen[cid]
136
137 def sendlevels(self):
138 """sends to dmx if levels have changed, or if we havent sent
139 in a while"""
140
141 reactor.callLater(self.calldelay, self.sendlevels)
142
143 if self.clientschanged:
144 # recalc levels
145
146 self.calclevels()
147
148 if (self.num_unshown_updates is None or # first time
149 self.options.fast_updates or # show always
150 (
151 self.combinedlevels != self.lastshownlevels and # changed
152 self.num_unshown_updates > 5)): # not too frequent
153 self.num_unshown_updates = 0
154 self.printlevels()
155 self.lastshownlevels = self.combinedlevels[:]
156 else:
157 self.num_unshown_updates += 1
158
159 if time.time() > self.laststatsprint + 2:
160 self.laststatsprint = time.time()
161 self.printstats()
162
163 # used to be a fixed 1 in here, for the max delay between
164 # calls, instead of calldelay
165 if self.clientschanged or time.time(
166 ) > self.lastupdate + self.calldelay:
167 self.lastupdate = time.time()
168 self.sendlevels_dmx()
169
170 self.clientschanged = 0 # clear the flag
171
172 def calclevels(self):
173 """combine all the known client levels into self.combinedlevels"""
174 self.combinedlevels = []
175 for chan in range(0, self.parportdmx.dimmers):
176 x = 0
177 for clientlist in list(self.clientlevels.values()):
178 if len(clientlist) > chan:
179 # clamp client levels to 0..1
180 cl = max(0, min(1, clientlist[chan]))
181 x = max(x, cl)
182 self.combinedlevels.append(x)
183
184 def printlevels(self):
185 """write all the levels to stdout"""
186 print("Levels:",
187 "".join(["% 2d " % (x * 100) for x in self.combinedlevels]))
188
189 def printstats(self):
190 """print the clock, freq, etc, with a \r at the end"""
191
192 sys.stdout.write("dmxserver up at %s, [polls %s] " % (
193 time.strftime("%H:%M:%S"),
194 str(self.updatefreq),
195 ))
196 for cid, freq in list(self.clientfreq.items()):
197 sys.stdout.write("[%s %s] " % (cid, str(freq)))
198 sys.stdout.write("\r")
199 sys.stdout.flush()
200
201 def sendlevels_dmx(self):
202 """output self.combinedlevels to dmx, and keep the updates/sec stats"""
203 # they'll get divided by 100
204 if self.parportdmx:
205 self.parportdmx.sendlevels([l * 100 for l in self.combinedlevels])
206 self.updatefreq.update()
207
208 def xmlrpc_echo(self, x):
209 return x
210
211 def xmlrpc_outputlevels(self, cid, levellist):
212 """send a unique id for your client (name+pid maybe), then
213 the variable-length dmx levellist (scaled 0..1)"""
214 if levellist != self.clientlevels.get(cid, None):
215 self.clientlevels[cid] = levellist
216 self.clientschanged = 1
217 self.trackClientFreq(cid)
218 return "ok"
219
220 def xmlrpc_currentlevels(self, cid):
221 """get a list of levels we're currently sending out. All
222 channels beyond the list you get back, they're at zero."""
223 # if this is still too slow, it might help to return a single
224 # pickled string
225 self.trackClientFreq(cid)
226 trunc = self.combinedlevels[:]
227 i = len(trunc) - 1
228 if i < 0:
229 return []
230 while trunc[i] == 0 and i >= 0:
231 i -= 1
232 if i < 0:
233 return []
234 trunc = trunc[:i + 1]
235 return trunc
236
237 def trackClientFreq(self, cid):
238 if cid not in self.lastseen:
239 print("hello new client %s" % cid)
240 self.clientfreq[cid] = Updatefreq()
241 self.lastseen[cid] = time.time()
242 self.clientfreq[cid].update()
243
244
245 parser = OptionParser()
246 parser.add_option("-f",
247 "--fast-updates",
248 action='store_true',
249 help=('display all dmx output to stdout instead '
250 'of the usual reduced output'))
251 parser.add_option("-r",
252 "--updates-per-sec",
253 type='float',
254 default=20,
255 help=('dmx output frequency'))
256 parser.add_option("-d",
257 "--dmx-device",
258 default='/dev/dmx0',
259 help='dmx device name')
260 parser.add_option("-n",
261 "--dummy",
262 action="store_true",
263 help="dummy mode, same as DMXDUMMY=1 env variable")
264 (options, songfiles) = parser.parse_args()
265
266 print(options)
267
268 if options.dummy:
269 os.environ['DMXDUMMY'] = "1"
270
271 port = networking.dmxServer.port
272 print("starting xmlrpc server on port %s" % port)
273 xmlrpcServe = XMLRPCServe(options)
274 reactor.listenTCP(port, server.Site(xmlrpcServe))
275
276 startZmq(networking.dmxServerZmq.port, xmlrpcServe.xmlrpc_outputlevels)
277
278 oscApp = ReceiverApplication(9051, xmlrpcServe)
279
280 reactor.run()