Files
@ 9f4d68612d56
Branch filter:
Location: light9/light9/effecteval/effectloop.py
9f4d68612d56
6.8 KiB
text/x-python
spelling
Ignore-this: 289a7704c3e9d694c0b668deb5be048e
Ignore-this: 289a7704c3e9d694c0b668deb5be048e
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 | from __future__ import division
import time, json, logging, traceback
import numpy
import serial
from twisted.internet import reactor
from twisted.internet.defer import inlineCallbacks, returnValue, succeed
from rdflib import URIRef, Literal
import cyclone.httpclient
from light9.namespaces import L9, RDF, RDFS
from light9.effecteval.effect import EffectNode
from light9 import Effects
from light9 import networking
from light9 import Submaster
from light9 import dmxclient
log = logging.getLogger('effectloop')
class EffectLoop(object):
"""maintains a collection of the current EffectNodes, gets time from
music player, sends dmx"""
def __init__(self, graph, stats):
self.graph, self.stats = graph, stats
self.currentSong = None
self.currentEffects = []
self.lastLogTime = 0
self.lastLogMsg = ""
self.lastErrorLog = 0
self.graph.addHandler(self.setEffects)
self.period = 1 / 30
self.coastSecs = .3 # main reason to keep this low is to notice play/pause
self.songTimeFromRequest = 0
self.requestTime = 0 # unix sec for when we fetched songTime
self.initOutput()
def initOutput(self):
pass
def startLoop(self):
log.info("startLoop")
reactor.callLater(self.period, self.sendLevels)
def setEffects(self):
self.currentEffects = []
if self.currentSong is None:
return
for effectUri in self.graph.objects(self.currentSong, L9['effect']):
self.currentEffects.append(EffectNode(self.graph, effectUri))
@inlineCallbacks
def getSongTime(self):
now = time.time()
if now - self.requestTime < self.coastSecs:
estimated = self.songTimeFromRequest
if self.currentSong is not None and self.currentPlaying:
estimated += now - self.requestTime
returnValue((estimated, self.currentSong))
else:
response = json.loads((yield cyclone.httpclient.fetch(
networking.musicPlayer.path('time'))).body)
self.requestTime = now
self.currentPlaying = response['playing']
self.songTimeFromRequest = response['t']
returnValue(
(response['t'], (response['song'] and URIRef(response['song']))))
@inlineCallbacks
def sendLevels(self):
t1 = time.time()
try:
with self.stats.sendLevels.time():
with self.stats.getMusic.time():
songTime, song = yield self.getSongTime()
if song != self.currentSong:
self.currentSong = song
# this may be piling on the handlers
self.graph.addHandler(self.setEffects)
if song is None:
return
with self.stats.evals.time():
outputs = self.allEffectOutputs(songTime)
combined = self.combineOutputs(outputs)
self.logLevels(t1, combined)
with self.stats.sendOutput.time():
yield self.sendOutput(combined)
elapsed = time.time() - t1
dt = max(0, self.period - elapsed)
except Exception:
self.stats.errors += 1
traceback.print_exc()
dt = 1
reactor.callLater(dt, self.sendLevels)
def combineOutputs(self, outputs):
"""pick usable effect outputs and reduce them into one for sendOutput"""
outputs = [x for x in outputs if isinstance(x, Submaster.Submaster)]
log.info('outputs %r', outputs)
out = Submaster.sub_maxes(*outputs)
return out
@inlineCallbacks
def sendOutput(self, combined):
dmx = combined.get_dmx_list()
yield dmxclient.outputlevels(dmx, twisted=True)
def allEffectOutputs(self, songTime):
outputs = []
for e in self.currentEffects:
try:
outputs.append(e.eval(songTime))
except Exception as exc:
now = time.time()
if now > self.lastErrorLog + 5:
log.error("effect %s: %s" % (e.uri, exc))
self.lastErrorLog = now
return outputs
def logLevels(self, now, out):
# this would look nice on the top of the effecteval web pages too
if log.isEnabledFor(logging.DEBUG):
log.debug(self.logMessage(out))
else:
if now > self.lastLogTime + 5:
msg = self.logMessage(out)
if msg != self.lastLogMsg:
log.info(msg)
self.lastLogMsg = msg
self.lastLogTime = now
def logMessage(self, out):
return ("send dmx: {%s}" %
", ".join("%r: %.3g" % (str(k), v)
for k,v in out.get_levels().items()))
Z = numpy.zeros((50, 3), dtype=numpy.uint8)
class LedLoop(EffectLoop):
def initOutput(self):
kw = dict(baudrate=115200)
self.boards = {
'L': serial.Serial('/dev/serial/by-id/usb-FTDI_FT232R_USB_UART_A7027JI6-if00-port0', **kw),
'R': serial.Serial('/dev/serial/by-id/usb-FTDI_FT232R_USB_UART_A7027NYX-if00-port0', **kw),
}
self.lastSentBacklight = None
def combineOutputs(self, outputs):
combined = {'L': Z, 'R': Z, 'blacklight': 0}
for out in outputs:
if isinstance(out, Effects.Blacklight):
combined['blacklight'] = max(combined['blacklight'], int(out * 255))
elif isinstance(out, Effects.ColorStrip):
pixels = numpy.array(out.pixels, dtype=numpy.float16)
px255 = (numpy.clip(pixels, 0, 1) * 255).astype(numpy.uint8)
combined[out.which] = numpy.maximum(combined[out.which], px255)
return combined
def sendOutput(self, combined):
for which, px255 in combined.items():
if which == 'blacklight':
if px255 != self.lastSentBacklight:
self.boards['L'].write('\x60\x01' + chr(px255))
self.boards['L'].flush()
self.lastSentBacklight = px255
else:
board = self.boards[which]
msg = '\x60\x00' + px255.reshape((-1,)).tostring()
board.write(msg)
board.flush()
return succeed(None)
def logMessage(self, out):
return str([(w, p.tolist() if isinstance(p, numpy.ndarray) else p) for w,p in out.items()])
def makeEffectLoop(graph, stats, outputWhere):
if outputWhere == 'dmx':
return EffectLoop(graph, stats)
elif outputWhere == 'leds':
return LedLoop(graph, stats)
else:
raise NotImplementedError
|