Tutorial¶
This is a simple example controlling a single RGB LED par, set to address 50 with ola_pilot and ola.
Prequisites¶
$ pip install ola_pilot
Now lets write a showfile demo.py:
from ola_pilot import Controller, Fixture
from ola_pilot.trait import RGB
class ToyFixture(Fixture):
wash = RGB()
tf = ToyFixture()
controller = Controller()
controller.add_fixture(tf, univ=0, base=50)
We can launch this show file:
$ ola_pilot tui demo.py
You will see the terminal UI, with the fixtures and patching shown:
Navigate in the terminal app using the cursor keys within a table (Up, Left, Right, Down). Move to the ‘wash’ column for ToyFixture-0. Pressing Enter edits a trait value, and Escape dismisses the trait editor.
Within the Trait Editor, pressing and Tab (and Shift-tab) moves the focus between the three sliders. We can make adjustments rapidly with the cursor keys as follows:
Up and Down move the value up or down by 0.1%.
PageUp and PageDown move by +10% or -10%.
F jumps to the full value, Z to zero and H to halfway.
As we change the RGB sliders the DMX (universe 0) addresses for R (50), G (51) and B (52) change accordingly:
The DMX universes are being calculated but not yet sent to any hardware. To do this setup some DMX hardware (ie. DMX adapters) using ‘ola’ and then connect our controller object to the ola instance, like this:
from ola_pilot import controller, Fixture, ola_client
from ola_pilot.trait import RGB
class ToyFixture(Fixture):
wash = RGB()
tf = ToyFixture()
controller = Controller()
controller.add_fixture(tf, univ=0, base=50)
controller.add_output(ola_client(host='localhost:8000'))
This will connect to ola, fetch the list of universes, and any universes that intersect with what has been patched will be written to. For more advanced use cases, you can add a second ola_client handling other universes, and the ola instance can be running on the local machine or over a network.
Patching list¶
To create a patching list, do this:
$ ola_pilot patch demo.py
fixture univ base channels mode
ToyFixture-0 0 50 3 -
We can also get a per-channel view:
$ ola_pilot channels demo.py
fixture univ addr channel mode
ToyFixture-0 0 50 0 wash.red
ToyFixture-0 0 51 1 wash.green
ToyFixture-0 0 52 2 wash.blue
You might have some questions about the ToyFixture above.
how did it know to take 3 channels, and in what order
and what is the RGB object?
The default Fixture base class constructor iterates the dictionary-view of the Class looking for standard Traits (PTPos, RGB, Intensity, IndexedValue) and duplicates them as fields on the instance. It then iterates the traits in order, and queries their channels, asuming no gaps between then. Since an RGB Trait has three channels, they get allocated sequentially.
>>> tf = ToyFixture()
>>> tf.get_channel_count()
3
>>> tf.get_traits()
[wash]
>>> tf.get_channels()
[wash.red, wash.green, wash.blue]
Controlling a Moving Head¶
Another example demo2.py, using multiple Traits:
class ToyMovingHead(Fixture):
pos = PTPos(order='PPTT')
wash = RGBW(order='WRGB')
intentity = Intensity()
controller.add_fixture(0, 5, ToyMovingHead())
$ ola_pilot patch demo2.py
fixture univ base channels mode
ToyMovingHead-0 0 50 3 -
$ ola_pilot channels demo2.py
fixture univ addr channel mode
ToyMovingHead-0 0 5 0 pos.pan
ToyMovingHead-0 0 6 1 pos.pan_fine
ToyMovingHead-0 0 7 2 pos.tilt
ToyMovingHead-0 0 8 3 pos.tilt_fine
ToyMovingHead-0 0 9 4 wash.white
ToyMovingHead-0 0 10 5 wash.red
ToyMovingHead-0 0 11 6 wash.green
ToyMovingHead-0 0 12 7 wash.blue
ToyMovingHead-0 0 13 8 intensity.value
Controlling Multiple Fixtures¶
We now want to patch a second RGB LED, and drive a signal to it from a MIDI controller
controller.patch_fixture(0, 5, tf1 := ToyFixture())
controller.patch_fixture(0, 10, tf2 := ToyFixture())
controller.add_efx(h := HueToRGB())
h.rgb.bind(tf1.wash)
h.rgb.bind(tf2.wash)
h.hue.set(0.5)
h.intensity.set(0.5)
>>> print(controller.get_universe(0))
00 00 00 00 cc dd 00 00 00 00 00 00 00 00 00 00 00 00 ...
>>> print(tf1.wash.drivers)
[out(HueToRGB-0.rgb)]
>>> print(h.rgb.targets)
[in(ToyMovingHead-0.wash), in(ToyMovingHead-1.wash)]
midi.search_device('MK3')
midi.bind_cc(83, h.hue)
midi.bind_cc(84, h.intensity)
Connectable objects (fixtures, fx) need to be registered with a controller before they bind to other ports, otherwise there will be no unique naming available for the connection.
>>> h = HueToRGB()
>>> tf = ToyFixture()
>>> h.rgb.bind(tf.wash)
ValueError("source is not named, add to a controller")
>>> controller.add_efx(h)
>>> h.rgb.bind(tf.wash)
ValueError("dest is not named, add to a controller")
>>> controller.add_fixture(tf)
>>> h.rgb.bind(tf.wash)
>>> print(controller.bindings())
['HueToRGB-0.rgb->ToyFixture-0.wash']
Multiple drivers¶
If we connect two drivers to an input, we need to decide which value wins:
controller.patch_fixture(0, 5, tf1 := ToyFixture())
controller.patch_efx(h := HueToRGB())
controller.patch_efx(h2 := HueToRGB())
h1.rgb.bind(tf1.wash)
h2.rgb.bind(tf1.wash)
h1.hue.set(0.5)
h1.intensity.set(0.5)
h2.hue.set(0.1)
h2.intensity.set(0.3)
Here should the lights be a hue of 0.1 purple or 0.5 red? There are several common stratagies:
HTP highest-takes-precidence
LTP last-takes-precidence
PTP priority-takes-precidence
Highest takes precidence is a historical hang-over, and it not well defined for RGB colour space (which is higher, #FF0000 or #00FF00 ?). How can a blinder FX cause a flashing effect if it cannot drive the lights dimmer than existing driver?
Last takes precidence is quite common on boards where a manual fader/slider has been moved, by setting the priority from a global counter that is increased when any dial is changed. This allows e.g. two faders for the same channel, and you can raise the level up with one fader, then when you move the second, it immediately snaps to the most recent value.
Explitic priority (PTP) requires each driver to the output to have a strength, for instance a flasher effect driven by midi might have an output with priority 0 when the note is not played, and a priority of 200 when held, so that it can override any other effect driving the fixture. I have taken this approach - every driver has a priority for it’s outputs, the priority is potentially dynamic or set within the scene/preset, and an input with multiple drivers is controlled exclusively by the highest priority.
All inputs including every trait of every fixture, can have a default value saved in the global preset.
controller.patch_fixture(0, 5, tf1 := ToyFixture())
controller.patch_fixture(0, 10, tf2 := ToyFixture())
controller.patch_efx(h := HueToRGB())
controller.patch_efx(fl := Flasher())
h.rgb.bind(tf1.wash)
h.rgb.bind(tf2.wash)
h.hue.set(0.5)
h.intensity.set(0.5)
fl.rgb.bind(tf1.wash)
fl.rgb.bind(tf2.wash)
midi = RTMIDI()
midi.search_device('MK3')
midi.bind_cc(83, h.hue)
midi.bind_cc(84, h.intensity)
midi.bind_cc(85, fl.speed)
midi.bind_note(63, fl.trigger)
We can ask an output for the list of it’s drivers:
>>> tf1.wash.drivers()
[
[ToyFixture-0 wash] <- [rgb HueToRGB-0 hue] <- 0.5,
[ToyFixture-0 wash] <- [rgb HueToRGB-0 intensity] <- 0.5
[ToyFixture-0 wash] <- [rgb HueToRGB-0 hue] <- [cc83 RTMIDI]
[ToyFixture-0 wash] <- [rgb HueToRGB-0 intensity] <- [cc84 RTMIDI]
[ToyFixture-0 wash] <- [rgb Flasher-0 speed] <- [cc85 RTMIDI]
[ToyFixture-0 wash] <- [rgb Flasher-0 trigger] <- [note63 RTMIDI]
]
This tracing is possible because Trait.bind(other) records both sides of the binding, and within an EFX implementation, we use Trait.drives(other) to record the dependancy, even though it is calculated by code.
Webserver¶
This will start up the basic http server for web browser control:
$ ola_pilot web demo.py