Add initial support for DMX lighting
This commit is contained in:
210
dmx.py
Normal file
210
dmx.py
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
# dmx.py
|
||||||
|
|
||||||
|
from ola.ClientWrapper import ClientWrapper
|
||||||
|
from ola.DMXConstants import (DMX_MIN_SLOT_VALUE, DMX_MAX_SLOT_VALUE, DMX_UNIVERSE_SIZE)
|
||||||
|
from array import array
|
||||||
|
import time
|
||||||
|
|
||||||
|
DMX_DATA_SIZE = DMX_UNIVERSE_SIZE
|
||||||
|
|
||||||
|
class CompatibleArray(array):
|
||||||
|
def tostring(self):
|
||||||
|
return self.tobytes()
|
||||||
|
|
||||||
|
class DMX:
|
||||||
|
def __init__(self, universe, client_wrapper, dmx_data_size=DMX_UNIVERSE_SIZE):
|
||||||
|
dmx_data_size = min(dmx_data_size, DMX_UNIVERSE_SIZE)
|
||||||
|
self._universe = universe
|
||||||
|
self._data = CompatibleArray('B', [DMX_MIN_SLOT_VALUE] * dmx_data_size)
|
||||||
|
self._wrapper = client_wrapper
|
||||||
|
self._client = client_wrapper.Client()
|
||||||
|
|
||||||
|
def set_channel(self, channel, value):
|
||||||
|
self._data[channel] = value
|
||||||
|
|
||||||
|
def submit(self):
|
||||||
|
self._client.SendDmx(self._universe, self._data)
|
||||||
|
|
||||||
|
class FixtureType:
|
||||||
|
def __init__(self, channels):
|
||||||
|
self._channels = channels
|
||||||
|
|
||||||
|
def max_channels(self):
|
||||||
|
return max([channel.get('channel', 0), channel.get('fine-channel', 0)] for channel in self._channels)
|
||||||
|
|
||||||
|
def is_channel(self, channel):
|
||||||
|
return channel in self._channels
|
||||||
|
|
||||||
|
def can_fade(self, channel):
|
||||||
|
return self._channels[channel]['fade']
|
||||||
|
|
||||||
|
def parse_value(self, value):
|
||||||
|
name = None
|
||||||
|
if isinstance(value, str):
|
||||||
|
values = value.split('/')
|
||||||
|
if len(values) > 2:
|
||||||
|
raise ValueError('{} is not a valid value'.format(value))
|
||||||
|
elif len(values) > 1:
|
||||||
|
(name, number) = values
|
||||||
|
else:
|
||||||
|
number = values[0]
|
||||||
|
else:
|
||||||
|
number = value
|
||||||
|
|
||||||
|
return (name, number)
|
||||||
|
|
||||||
|
def channel(self, name):
|
||||||
|
if name not in self._channels:
|
||||||
|
return KeyError('Channel {} not found in fixture type'.format(name))
|
||||||
|
return self._channels[name]
|
||||||
|
|
||||||
|
def range_lookup(self, channel, name, number):
|
||||||
|
channel = self.channel(channel)
|
||||||
|
range = channel['range']
|
||||||
|
if isinstance(range, dict):
|
||||||
|
if name not in range:
|
||||||
|
raise KeyError('Range {} not found'.format(name))
|
||||||
|
(a, b) = range['range']
|
||||||
|
if number < a or number > b:
|
||||||
|
raise ValueError('DMX value out of range')
|
||||||
|
return a + number
|
||||||
|
else:
|
||||||
|
if name is not None:
|
||||||
|
raise KeyError('Range {} not found'.format(name))
|
||||||
|
return number
|
||||||
|
|
||||||
|
def update(self, **kwargs):
|
||||||
|
output = []
|
||||||
|
for channel in kwargs:
|
||||||
|
value = kwargs[channel]
|
||||||
|
(name, number) = self.parse_value(value)
|
||||||
|
dmxvalue = self.range_lookup(channel, name, number)
|
||||||
|
ch = self.channel(channel)
|
||||||
|
if 'fine-channel' in ch and ch['fine-channel'] is not None:
|
||||||
|
output.extend([(ch['channel'], dmxvalue // 256), (ch['fine-channel'], dmxvalue % 256)])
|
||||||
|
else:
|
||||||
|
output.extend([(ch['channel'], dmxvalue)])
|
||||||
|
return output
|
||||||
|
|
||||||
|
class Fixture:
|
||||||
|
def __init__(self, dmx, type):
|
||||||
|
self._dmx = dmx
|
||||||
|
self._type = type
|
||||||
|
self._dmxoutput = None
|
||||||
|
self._values = {}
|
||||||
|
self._target = {}
|
||||||
|
self._velocity = {}
|
||||||
|
self._fadetime = 0
|
||||||
|
self._t = 0
|
||||||
|
|
||||||
|
def get(self, channel):
|
||||||
|
return self._values.get(channel, 0)
|
||||||
|
|
||||||
|
def set(self, channel, value):
|
||||||
|
self._values[channel] = value
|
||||||
|
|
||||||
|
def set_dmx(self, dmx):
|
||||||
|
self._dmxoutput = dmx
|
||||||
|
|
||||||
|
def is_channel(self, name):
|
||||||
|
return self._type.is_channel(name)
|
||||||
|
|
||||||
|
def set_fadetime(self, fadetime):
|
||||||
|
self._fadetime = fadetime
|
||||||
|
|
||||||
|
def update(self, **kwargs):
|
||||||
|
for channel in kwargs:
|
||||||
|
(name, number) = self._type.parse_value(kwargs[channel])
|
||||||
|
self._target[channel] = self._type.range_lookup(channel, name, number)
|
||||||
|
diff = self._target[channel] - self.get(channel)
|
||||||
|
self._velocity[channel] = diff
|
||||||
|
self._t = 0
|
||||||
|
self.tick(0)
|
||||||
|
|
||||||
|
def tick(self, dt):
|
||||||
|
def reached(value, target, velocity):
|
||||||
|
if velocity < 0:
|
||||||
|
return value <= target
|
||||||
|
else:
|
||||||
|
return value >= target
|
||||||
|
|
||||||
|
self._t += dt
|
||||||
|
for channel in self._target.keys():
|
||||||
|
if self._type.can_fade(channel) and self._fadetime > 0 and self._t <= self._fadetime:
|
||||||
|
# print("Fading: t = {}, ft = {}".format(self._t, self._fadetime))
|
||||||
|
value = self.get(channel)
|
||||||
|
value += self._velocity[channel] * dt / self._fadetime
|
||||||
|
if reached(value, self._target[channel], self._velocity[channel]):
|
||||||
|
self._velocity[channel] = 0
|
||||||
|
value = self._target[channel]
|
||||||
|
self.set(channel, value)
|
||||||
|
else:
|
||||||
|
# print("Not fading: t = {}, ft = {}".format(self._t, self._fadetime))
|
||||||
|
self.set(channel, self._target[channel])
|
||||||
|
output = self._type.update(**self._values)
|
||||||
|
output = [(ch-1+self._dmx-1, value) for (ch, value) in output]
|
||||||
|
if self._dmxoutput is None:
|
||||||
|
return
|
||||||
|
for (ch, value) in output:
|
||||||
|
self._dmxoutput.set_channel(ch, int(value))
|
||||||
|
|
||||||
|
class Group:
|
||||||
|
def __init__(self, objects):
|
||||||
|
self._objects = objects
|
||||||
|
|
||||||
|
def is_channel(self, name):
|
||||||
|
return any([x.is_channel(name) for x in self._objects])
|
||||||
|
|
||||||
|
def set_fadetime(self, fadetime):
|
||||||
|
for object in self._objects:
|
||||||
|
object.set_fadetime(fadetime)
|
||||||
|
|
||||||
|
def update(self, **kwargs):
|
||||||
|
for object in self._objects:
|
||||||
|
object.update(**kwargs)
|
||||||
|
|
||||||
|
class Scene:
|
||||||
|
def __init__(self, objects):
|
||||||
|
self._objects = objects
|
||||||
|
|
||||||
|
def set_fadetime(self, fadetime):
|
||||||
|
for object in self._objects:
|
||||||
|
(obj, settings) = self._objects[object]
|
||||||
|
obj.set_fadetime(fadetime)
|
||||||
|
|
||||||
|
def activate(self, fadetime=0):
|
||||||
|
self.set_fadetime(fadetime)
|
||||||
|
for object in self._objects:
|
||||||
|
(obj, settings) = self._objects[object]
|
||||||
|
obj.update(**settings)
|
||||||
|
|
||||||
|
|
||||||
|
class FixtureOutput:
|
||||||
|
def __init__(self, fixtures, dmx):
|
||||||
|
self._fixtures = fixtures
|
||||||
|
self._dmx = dmx
|
||||||
|
for fixture in self._fixtures:
|
||||||
|
fixture.set_dmx(dmx)
|
||||||
|
|
||||||
|
def tick(self, dt):
|
||||||
|
for fixture in self._fixtures:
|
||||||
|
fixture.tick(dt)
|
||||||
|
self._dmx.submit()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
wrapper = ClientWrapper()
|
||||||
|
universe = 0
|
||||||
|
dmx = DMX(universe, wrapper, DMX_DATA_SIZE)
|
||||||
|
value = 0
|
||||||
|
while True:
|
||||||
|
dmx.set_channel(0, 255)
|
||||||
|
dmx.set_channel(1, int((value * 3) % 256))
|
||||||
|
dmx.set_channel(2, int((value * 4) % 256))
|
||||||
|
dmx.set_channel(3, int((value * 5) % 256))
|
||||||
|
dmx.submit()
|
||||||
|
value += 0.1
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
11
events.py
11
events.py
@@ -193,3 +193,14 @@ class Lyrics(Event):
|
|||||||
if message == 'newdisplay':
|
if message == 'newdisplay':
|
||||||
self._eventrunner.finished(self)
|
self._eventrunner.finished(self)
|
||||||
|
|
||||||
|
class LightScene(Event):
|
||||||
|
def __init__(self, scene, fade=0, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self._scene = scene
|
||||||
|
self._fade = fade
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
scenes = self._eventrunner.get_config('scenes')
|
||||||
|
scene = scenes[self._scene]
|
||||||
|
scene.activate(self._fade)
|
||||||
|
|
||||||
|
|||||||
71
factory.py
71
factory.py
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import events
|
import events
|
||||||
from item import Item
|
from item import Item
|
||||||
|
import dmx
|
||||||
|
|
||||||
class MakeItems:
|
class MakeItems:
|
||||||
def __new__(cls, config):
|
def __new__(cls, config):
|
||||||
@@ -45,3 +46,73 @@ class MakeSet:
|
|||||||
raise KeyError('{} not in items'.format(item))
|
raise KeyError('{} not in items'.format(item))
|
||||||
return config
|
return config
|
||||||
|
|
||||||
|
class MakeFixtureTypes:
|
||||||
|
def __new__(cls, config):
|
||||||
|
|
||||||
|
def parse_range(src):
|
||||||
|
(a, b) = src.split('-')
|
||||||
|
return (int(a), int(b))
|
||||||
|
|
||||||
|
types = {}
|
||||||
|
for ftype in config:
|
||||||
|
channels = {}
|
||||||
|
for channel in config[ftype].keys():
|
||||||
|
dmxchannel = int(config[ftype][channel]['channel'])
|
||||||
|
finechannel = config[ftype][channel].get('fine-channel')
|
||||||
|
if finechannel is not None:
|
||||||
|
finechannel = int(finechannel)
|
||||||
|
fade = config[ftype][channel].get('fade', True)
|
||||||
|
rangesrc = config[ftype][channel]['range']
|
||||||
|
if isinstance(rangesrc, dict):
|
||||||
|
range = {}
|
||||||
|
for entry in rangesrc:
|
||||||
|
range[entry] = parse_range(rangesrc[entry])
|
||||||
|
else:
|
||||||
|
range = parse_range(rangesrc)
|
||||||
|
channels[channel] = {'channel' : dmxchannel, 'fine-channel' : finechannel, 'range' : range, 'fade' : fade}
|
||||||
|
types[ftype] = dmx.FixtureType(channels)
|
||||||
|
|
||||||
|
return types
|
||||||
|
|
||||||
|
class MakeFixtures:
|
||||||
|
def __new__(cls, config, fixture_types):
|
||||||
|
fixtures = {}
|
||||||
|
for fixture in config:
|
||||||
|
dmxch = config[fixture]['dmx']
|
||||||
|
type = config[fixture]['type']
|
||||||
|
if type not in fixture_types:
|
||||||
|
raise KeyError('{} not in fixture_types'.format(type))
|
||||||
|
fixtures[fixture] = dmx.Fixture(dmxch, fixture_types[type])
|
||||||
|
return fixtures
|
||||||
|
|
||||||
|
class MakeGroups:
|
||||||
|
def __new__(cls, config, fixtures):
|
||||||
|
groups = {}
|
||||||
|
for group in config:
|
||||||
|
fobjects = []
|
||||||
|
for fixture in config[group]:
|
||||||
|
if fixture not in fixtures:
|
||||||
|
raise KeyError('{} not in fixtures'.format(fixture))
|
||||||
|
fobjects.append(fixtures[fixture])
|
||||||
|
# XXX process modifiers
|
||||||
|
groups[group] = dmx.Group(fobjects)
|
||||||
|
return groups
|
||||||
|
|
||||||
|
class MakeScenes:
|
||||||
|
def __new__(cls, config, fixtures, groups):
|
||||||
|
scenes = {}
|
||||||
|
for scene in config:
|
||||||
|
objects = {}
|
||||||
|
for name in config[scene].keys():
|
||||||
|
if name in groups:
|
||||||
|
obj = groups[name]
|
||||||
|
elif name in fixtures:
|
||||||
|
obj = fixtures[name]
|
||||||
|
else:
|
||||||
|
raise KeyError('{} is not a group or a fixture'.format(name))
|
||||||
|
for channel in config[scene][name].keys():
|
||||||
|
if not obj.is_channel(channel):
|
||||||
|
raise KeyError('{} is not a channel within {}'.format(channel, name))
|
||||||
|
objects[name] = (obj, config[scene][name])
|
||||||
|
scenes[scene] = dmx.Scene(objects)
|
||||||
|
return scenes
|
||||||
|
|||||||
42
gig.py
42
gig.py
@@ -10,12 +10,16 @@ from pyglet.window import key
|
|||||||
|
|
||||||
import factory
|
import factory
|
||||||
import display
|
import display
|
||||||
|
import dmx
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
import time
|
import time
|
||||||
|
|
||||||
import dbus
|
import dbus
|
||||||
|
|
||||||
|
from ola.ClientWrapper import ClientWrapper
|
||||||
|
from ola.DMXConstants import (DMX_MIN_SLOT_VALUE, DMX_MAX_SLOT_VALUE, DMX_UNIVERSE_SIZE)
|
||||||
|
|
||||||
class EventRunner:
|
class EventRunner:
|
||||||
def __init__(self, gigconfig):
|
def __init__(self, gigconfig):
|
||||||
self._gigconfig = gigconfig
|
self._gigconfig = gigconfig
|
||||||
@@ -31,6 +35,9 @@ class EventRunner:
|
|||||||
for item in self._gigconfig['items']:
|
for item in self._gigconfig['items']:
|
||||||
self._gigconfig['items'][item].set_audiencecontent(audiencecontent)
|
self._gigconfig['items'][item].set_audiencecontent(audiencecontent)
|
||||||
|
|
||||||
|
def get_config(self, section):
|
||||||
|
return self._gigconfig[section]
|
||||||
|
|
||||||
def select(self, pos):
|
def select(self, pos):
|
||||||
self._selected = pos
|
self._selected = pos
|
||||||
self._playing = None
|
self._playing = None
|
||||||
@@ -335,14 +342,22 @@ class AudienceWindow(pyglet.window.Window):
|
|||||||
self.audiencebatch.draw()
|
self.audiencebatch.draw()
|
||||||
|
|
||||||
def process_config(config):
|
def process_config(config):
|
||||||
items = factory.MakeItems(config['items'])
|
items = factory.MakeItems(config.get('items'))
|
||||||
setlist = factory.MakeSet(config['set'], items)
|
setlist = factory.MakeSet(config.get('set'), items)
|
||||||
preshow = factory.MakeSet(config['preshow'], items)
|
defaults = config.get('defaults')
|
||||||
if 'defaults' in config:
|
fixture_types = factory.MakeFixtureTypes(config.get('fixture_types'))
|
||||||
defaults = config['defaults']
|
fixtures = factory.MakeFixtures(config.get('fixtures'), fixture_types)
|
||||||
else:
|
groups = factory.MakeGroups(config.get('groups'), fixtures)
|
||||||
defaults = {}
|
scenes = factory.MakeScenes(config.get('scenes'), fixtures, groups)
|
||||||
return {'items' : items, 'setlist' : setlist, 'preshow' : preshow}
|
return {
|
||||||
|
'items' : items,
|
||||||
|
'setlist' : setlist,
|
||||||
|
'defaults' : defaults,
|
||||||
|
'fixture_types' : fixture_types,
|
||||||
|
'fixtures' : fixtures,
|
||||||
|
'groups' : groups,
|
||||||
|
'scenes' : scenes,
|
||||||
|
}
|
||||||
|
|
||||||
def load_config(filename):
|
def load_config(filename):
|
||||||
with open(filename, encoding='utf8') as file:
|
with open(filename, encoding='utf8') as file:
|
||||||
@@ -360,6 +375,7 @@ def main():
|
|||||||
parser = argparse.ArgumentParser(prog='gig.py', description='Gig showrunning')
|
parser = argparse.ArgumentParser(prog='gig.py', description='Gig showrunning')
|
||||||
parser.add_argument('filename')
|
parser.add_argument('filename')
|
||||||
parser.add_argument('-w', '--window', action='store_true', help='Run in a window')
|
parser.add_argument('-w', '--window', action='store_true', help='Run in a window')
|
||||||
|
parser.add_argument('-u', '--universe', default=0, type=int, help='DMX universe')
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
config = load_config(args.filename)
|
config = load_config(args.filename)
|
||||||
display = pyglet.display.get_display()
|
display = pyglet.display.get_display()
|
||||||
@@ -376,6 +392,16 @@ def main():
|
|||||||
|
|
||||||
audiencecontent = AudienceContent(gigconfig=config, eventrunner=eventrunner)
|
audiencecontent = AudienceContent(gigconfig=config, eventrunner=eventrunner)
|
||||||
|
|
||||||
|
# try:
|
||||||
|
wrapper = ClientWrapper()
|
||||||
|
DMX_DATA_SIZE = DMX_UNIVERSE_SIZE
|
||||||
|
dmx_object = dmx.DMX(args.universe, wrapper, DMX_DATA_SIZE)
|
||||||
|
# except Exception:
|
||||||
|
# pass
|
||||||
|
|
||||||
|
fixtureoutput = dmx.FixtureOutput(fixtures=[config['fixtures'][x] for x in config['fixtures']], dmx=dmx_object)
|
||||||
|
pyglet.clock.schedule_interval(fixtureoutput.tick, 1/30.0)
|
||||||
|
|
||||||
if audiencescreen is not None:
|
if audiencescreen is not None:
|
||||||
window2 = AudienceWindow(audiencecontent=audiencecontent, fullscreen=False, screen=audiencescreen, resizable=True)
|
window2 = AudienceWindow(audiencecontent=audiencecontent, fullscreen=False, screen=audiencescreen, resizable=True)
|
||||||
if not args.window:
|
if not args.window:
|
||||||
|
|||||||
Reference in New Issue
Block a user