Mercurial > code > home > repos > light9
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() |