diff --git a/dmx.py b/dmx.py new file mode 100644 index 0000000..dbd4be1 --- /dev/null +++ b/dmx.py @@ -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() diff --git a/events.py b/events.py index 1463621..67f8900 100644 --- a/events.py +++ b/events.py @@ -193,3 +193,14 @@ class Lyrics(Event): if message == 'newdisplay': 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) + diff --git a/factory.py b/factory.py index 0f97be2..6a736e8 100644 --- a/factory.py +++ b/factory.py @@ -2,6 +2,7 @@ import events from item import Item +import dmx class MakeItems: def __new__(cls, config): @@ -45,3 +46,73 @@ class MakeSet: raise KeyError('{} not in items'.format(item)) 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 diff --git a/gig.py b/gig.py index 00a027f..dfb346e 100755 --- a/gig.py +++ b/gig.py @@ -10,12 +10,16 @@ from pyglet.window import key import factory import display +import dmx import yaml import time import dbus +from ola.ClientWrapper import ClientWrapper +from ola.DMXConstants import (DMX_MIN_SLOT_VALUE, DMX_MAX_SLOT_VALUE, DMX_UNIVERSE_SIZE) + class EventRunner: def __init__(self, gigconfig): self._gigconfig = gigconfig @@ -31,6 +35,9 @@ class EventRunner: for item in self._gigconfig['items']: self._gigconfig['items'][item].set_audiencecontent(audiencecontent) + def get_config(self, section): + return self._gigconfig[section] + def select(self, pos): self._selected = pos self._playing = None @@ -335,14 +342,22 @@ class AudienceWindow(pyglet.window.Window): self.audiencebatch.draw() def process_config(config): - items = factory.MakeItems(config['items']) - setlist = factory.MakeSet(config['set'], items) - preshow = factory.MakeSet(config['preshow'], items) - if 'defaults' in config: - defaults = config['defaults'] - else: - defaults = {} - return {'items' : items, 'setlist' : setlist, 'preshow' : preshow} + items = factory.MakeItems(config.get('items')) + setlist = factory.MakeSet(config.get('set'), items) + defaults = config.get('defaults') + fixture_types = factory.MakeFixtureTypes(config.get('fixture_types')) + fixtures = factory.MakeFixtures(config.get('fixtures'), fixture_types) + groups = factory.MakeGroups(config.get('groups'), fixtures) + scenes = factory.MakeScenes(config.get('scenes'), fixtures, groups) + return { + 'items' : items, + 'setlist' : setlist, + 'defaults' : defaults, + 'fixture_types' : fixture_types, + 'fixtures' : fixtures, + 'groups' : groups, + 'scenes' : scenes, + } def load_config(filename): with open(filename, encoding='utf8') as file: @@ -360,6 +375,7 @@ def main(): parser = argparse.ArgumentParser(prog='gig.py', description='Gig showrunning') parser.add_argument('filename') 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() config = load_config(args.filename) display = pyglet.display.get_display() @@ -376,6 +392,16 @@ def main(): 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: window2 = AudienceWindow(audiencecontent=audiencecontent, fullscreen=False, screen=audiencescreen, resizable=True) if not args.window: