Integrating custom effects into your show

1pet2_9

Active member
Since I had asked about this on different forums before and no one particularly had a solution, I thought I'd document this.


The problem:
For Halloween, I have some lightning effects that I coded in Arduino that are highly randomized. They don't fit nicely into Xlights, and they're different every time you play it. However, they are supposed to play at very specific times within the show. I could run them asynchronously, but I couldn't figure out how to integrate them into sequences. This solution can apply to any effect that doesn't quite fit into Xlights.


Solution:
There is no rule which says that E1.31 can only be used to run lights. Or motors, or on/off switches, or smoke.... You can integrate ESP32's into your show and code them to listen to E1.31--at which point, you can wire up the ESP's to do anything. There are multiple example sketches out there which demonstrate how to do this. Often these examples will marry together E1.31 with FastLED or WLED, but you can just delete the WLED part and keep only the E1.31. The incoming network packets will consist of short integer values on a bunch of channel numbers. Every ESP32 listening in will hear all these values on the network. From there, instead of thinking of these channels and values as lights, think of them as commands (ultimately, the network packets are nothing more than just 1's and 0's, which you can interpret any way you want). Value 127 on channel 7 is a command. 128 is another. You define what that command is. In your sketch, you basically have a gigantic switch() statement which dispatches off these various integer values to various callback functions which you define. The value can also serve as a parameter you can pass on to a function (e.g. in my case, to control the speed and intensity of the lightning, and to steer its randomness).

To integrate into Xights, Vixen...whatever...place down a DMX effect, with the appropriate channel number and value. Configure your controller/channel such that the DMX effect gets broadcast over E1.31 on your show network. Probably, you should give all custom effects of this sort their own universe (mostly because there is no reason not to). The ESP32's, of course, are listening in on the show network, on that universe.

You need to "debounce" your callback functions, since the DMX effect will repeatedly broadcast the same value over and over, for the duration of the effect. In essence, just ignore DMX traffic for that channel until that command you're already running completes, and until the DMX value at least changes to something else first. This repeat-broadcast feature of DMX is actually nice on unreliable wireless networks, since it makes them more reliable (albeit the timing might be slightly off if it takes 5 tries to finally receive the command).

Normally, this would be an application more suited to MQTT than E1.31, except that we are trying to integrate into Xlights (or Vixen, et. al.). Xlights supports outputting a "prop" to E1.31 on the network.


Other possibilities using this method that I'm currently testing:

- attach color macros to certain DMX values, so the effect just plays locally and reduces your network bandwidth. And to function more robustly in an unreliable wireless network, since only the start signal alone relies on the network.
- randomize other effects so they are different every playthrough
- respond to sensors or buttons over a certain time window, within a song. You can tie INPUTS to Xlights effects this way.
- play back effects differently based on sensor values.
- packing 8 non-dimmable elements into a single channel (such as mechanical relays)
- don't turn on certain GPIO's at all if that relay channel is known to have GFCI problems, and the moisture sensor is detecting moisture
- Nothing. Just echo out the network packets to your Serial Monitor and watch what's going on. Debug.


In case I made all this sound harder than it really is, I'll put it another way: in Xlights, you place down an effect which says, "Broadcast value 127 on channel 7 at this time...", and your ESP32's run an E1.31 program you can download from Github which says, "Whenever I hear a 127 on channel 7 on the network, run this custom command...." And that command can be whatever unorthodox, random thing you can dream of, that would never fit into Xlights. The ESP32 doesn't have to be an ESPixelStick, doesn't have to run FPP, nothing. A vanilla ESP32 will do.
 
Just a note that with the latest xLights build and latest FPP7 code, there is a new way for triggering things within FPP. You can create an "FPP Preset" timing track (just like any other timing track) and add labels to that track for when Command Presets should be triggered. Thus, there isn't really the need to track that "value 125 is this effect" and "value 140 is that one" and such. The information for when the presets need to be triggered are stored in the fseq file along with the channel data and FPP uses that when the fseq is run.

The fseq stuff in ESPixelStick could likely be updated to support that information to trigger various things there as well.
 
Thanks Dan.

I was thinking about triggering the built in effects and any play lists or fseq files that the user defines in a list/channel value. I will look at updating to the latest FPP functionality as well.
 
One would still need to track the command numbers in the ESP itself, wouldn't we? I'm coding my commands in Arduino. That's great that command presets, labels, etc. are being integrated into Xlights and FPP, but on the back-end (which I have to implement myself--that's why it's custom), that still has to know that it is hearing the command. Right now I have a vanilla ESP listening to E1.31 multicast mode. Universe #, channel #, and value # get decoded into a callback table in my own software. I could comment and #define enum values for the labels in the Arduino sketch, but I think that's the best I can do on my [custom] end.

There would need to be a means for ESPixelStick/FPP/Xlights to own the callback table, so as to enforce the label/value binding. While still allowing the end user to implement the callback. Of course, we also need to retain control over the GPIO as well (in hardware ESPixelSticks).

BTW like I mentioned, I really like the feature where the same DMX value gets re-broadcast over E1.31 over and over. Sometimes, the packets get dropped over wireless; and if the effect kicks in a frame or two late, I can afford that. I have some flaky DMX receivers on stage which depend on that all the time (granted, there is nothing on stage which uses FPP, but the principle is still the same: flaky wireless, but the DMX keeps getting broadcast, and the receiver needs only to catch the packet once).
 
Here is my Arduino sketch for ESP32 that I derived from forkineye's E1.31 example. This one bit-packs an 8-mechanical relay bank into a single channel, and switches them based on the value on universe=2, channel 5--which will come in as a value ranging from 0 to 255. It decodes that into 8 1's and 0's and switches the 8 mechanical relays based on that.


#include <ESPAsyncE131.h>

#define UNIVERSE 1 // First DMX Universe to listen for
#define UNIVERSE_COUNT 2 // Total number of Universes to listen for, starting at UNIVERSE


const char ssid[] = "dlink"; // Replace with your SSID
const char passphrase[] = ""; // Replace with your WPA2 passphrase

// ESPAsyncE131 instance with UNIVERSE_COUNT buffer slots
ESPAsyncE131 e131(UNIVERSE_COUNT);

void setup() {
Serial.begin(9600);
delay(10);

// Make sure you're in station mode
WiFi.mode(WIFI_STA);

Serial.println("");
Serial.print(F("Connecting to "));
Serial.print(ssid);

if (strlen(passphrase)>0) { //passphrase != NULL)
WiFi.begin(ssid, passphrase);
} else {
WiFi.begin(ssid);
Serial.println("Logging in without passphrase.");
}

delay(5000);
//while (WiFi.status() != WL_CONNECTED) {
// delay(500);
//Serial.print(WiFi.status());
// Serial.print(".");
//}

Serial.println("");
Serial.print(F("Connected with IP: "));
Serial.println(WiFi.localIP());

// Choose one to begin listening for E1.31 data
//if (e131.begin(E131_UNICAST)) // Listen via Unicast
if (e131.begin(E131_MULTICAST, UNIVERSE, UNIVERSE_COUNT)) // Listen via Multicast
Serial.println(F("Listening for data..."));
else
Serial.println(F("*** e131.begin failed ***"));

// These pins are optimized for a Wemos D1 R32.
pinMode(13, OUTPUT);
pinMode(12, OUTPUT);
pinMode(14, OUTPUT);
pinMode(27, OUTPUT);
pinMode(16, OUTPUT);
pinMode(17, OUTPUT);
pinMode(25, OUTPUT);
pinMode(26, OUTPUT);
}

unsigned int prev_value = 999;
unsigned int debounce = 0;
unsigned int relays[8] = {0, 0, 0, 0, 0, 0, 0, 0};


void Callback_Channel_5() {
Serial.printf("execute callback: %u\n", prev_value);

// In this example, control a bank of 8 mechanical relays by bit-packing channel 5.
// If you use this example literally in the real world, you will probably have to
// shift your voltage from 3.3V to 5V, in order for your ESP32 to talk to mechanical relays.
// Remember to invert your logic to active-low, and double-check to make sure your code
// debounces your transitions, so your relays don't switch too much.
relays[0] = prev_value & 1;
relays[1] = (prev_value & 2) >> 1;
relays[2] = (prev_value & 4) >> 2;
relays[3] = (prev_value & 8) >> 3;
relays[4] = (prev_value & 16) >> 4;
relays[5] = (prev_value & 32) >> 5;
relays[6] = (prev_value & 64) >> 6;
relays[7] = (prev_value & 128) >> 7;

Serial.printf("New relay readout: %u %u %u %u %u %u %u %u\n", relays[7], relays[6],
relays[5], relays[4], relays[3], relays[2], relays[1], relays[0]);

// These pins are optimized for a Wemos D1 R32.
digitalWrite(13, relays[0]);
digitalWrite(12, relays[1]);
digitalWrite(14, relays[2]);
digitalWrite(27, relays[3]);
digitalWrite(16, relays[4]);
digitalWrite(17, relays[5]);
digitalWrite(25, relays[6]);
digitalWrite(26, relays[7]);

}

void loop() {

if (!e131.isEmpty()) {
e131_packet_t packet;
e131.pull(&packet); // Pull packet from ring buffer

// Custom callbacks of this nature are probably better off being given their own dedicated
// universe. In this case, universe 2.
if (htons(packet.universe)==2) {
// This assumes E1.31 multicast is repeatedly re-broadcasting the same DMX values.
// That being the case, the debounce algorithm throws out as noise any packets which
// only appear once. Manual dimmers on a light board, for example, will produce a lot of noise.
if(packet.property_values[5]!=prev_value) {
Serial.printf("Universe %u / %u Channels | Packet#: %u / Errors: %u / CH: %u, prev_value: %u\n",
htons(packet.universe), // The Universe for this packet
htons(packet.property_value_count) - 1, // Start code is ignored, we're interested in dimmer data
e131.stats.num_packets, // Packet counter
e131.stats.packet_errors, // Packet error counter
packet.property_values[5], prev_value); // Dimmer data for Channel
prev_value = packet.property_values[5];
debounce = 0;

} else {
// This clause gets entered when the same DMX value appears twice-in-a-row.
// Three or more get thrown out, but only the second occurrence triggers the callback.
if (debounce==0) {
Callback_Channel_5();
debounce = 1;
}
} // if value==prev_value/else
} // if universe==2/else
} // if packet empty
} // loop
 
Great to see someone still experimenting with e1.31 sketches .

Have you tested this and measured realtime lag ?

Code:
#include <ESPAsyncE131.h>

#define UNIVERSE 1 // First DMX Universe to listen for
#define UNIVERSE_COUNT 2 // Total number of Universes to listen for, starting at UNIVERSE


const char ssid[] = "dlink"; // Replace with your SSID
const char passphrase[] = ""; // Replace with your WPA2 passphrase

// ESPAsyncE131 instance with UNIVERSE_COUNT buffer slots
ESPAsyncE131 e131(UNIVERSE_COUNT);

void setup() {
Serial.begin(9600);
delay(10);

// Make sure you're in station mode
WiFi.mode(WIFI_STA);

Serial.println("");
Serial.print(F("Connecting to "));
Serial.print(ssid);

if (strlen(passphrase)>0) { //passphrase != NULL)
WiFi.begin(ssid, passphrase);
} else {
WiFi.begin(ssid);
Serial.println("Logging in without passphrase.");
}

delay(5000);
//while (WiFi.status() != WL_CONNECTED) {
// delay(500);
//Serial.print(WiFi.status());
// Serial.print(".");
//}

Serial.println("");
Serial.print(F("Connected with IP: "));
Serial.println(WiFi.localIP());

// Choose one to begin listening for E1.31 data
//if (e131.begin(E131_UNICAST)) // Listen via Unicast
if (e131.begin(E131_MULTICAST, UNIVERSE, UNIVERSE_COUNT)) // Listen via Multicast
Serial.println(F("Listening for data..."));
else
Serial.println(F("*** e131.begin failed ***"));

// These pins are optimized for a Wemos D1 R32.
pinMode(13, OUTPUT);
pinMode(12, OUTPUT);
pinMode(14, OUTPUT);
pinMode(27, OUTPUT);
pinMode(16, OUTPUT);
pinMode(17, OUTPUT);
pinMode(25, OUTPUT);
pinMode(26, OUTPUT);
}

unsigned int prev_value = 999;
unsigned int debounce = 0;
unsigned int relays[8] = {0, 0, 0, 0, 0, 0, 0, 0};


void Callback_Channel_5() {
Serial.printf("execute callback: %u\n", prev_value);

// In this example, control a bank of 8 mechanical relays by bit-packing channel 5.
// If you use this example literally in the real world, you will probably have to
// shift your voltage from 3.3V to 5V, in order for your ESP32 to talk to mechanical relays.
// Remember to invert your logic to active-low, and double-check to make sure your code
// debounces your transitions, so your relays don't switch too much.
relays[0] = prev_value & 1;
relays[1] = (prev_value & 2) >> 1;
relays[2] = (prev_value & 4) >> 2;
relays[3] = (prev_value & 8) >> 3;
relays[4] = (prev_value & 16) >> 4;
relays[5] = (prev_value & 32) >> 5;
relays[6] = (prev_value & 64) >> 6;
relays[7] = (prev_value & 128) >> 7;

Serial.printf("New relay readout: %u %u %u %u %u %u %u %u\n", relays[7], relays[6],
relays[5], relays[4], relays[3], relays[2], relays[1], relays[0]);

// These pins are optimized for a Wemos D1 R32.
digitalWrite(13, relays[0]);
digitalWrite(12, relays[1]);
digitalWrite(14, relays[2]);
digitalWrite(27, relays[3]);
digitalWrite(16, relays[4]);
digitalWrite(17, relays[5]);
digitalWrite(25, relays[6]);
digitalWrite(26, relays[7]);

}

void loop() {

if (!e131.isEmpty()) {
e131_packet_t packet;
e131.pull(&packet); // Pull packet from ring buffer

// Custom callbacks of this nature are probably better off being given their own dedicated
// universe. In this case, universe 2.
if (htons(packet.universe)==2) {
// This assumes E1.31 multicast is repeatedly re-broadcasting the same DMX values.
// That being the case, the debounce algorithm throws out as noise any packets which
// only appear once. Manual dimmers on a light board, for example, will produce a lot of noise.
if(packet.property_values[5]!=prev_value) {
Serial.printf("Universe %u / %u Channels | Packet#: %u / Errors: %u / CH: %u, prev_value: %u\n",
htons(packet.universe), // The Universe for this packet
htons(packet.property_value_count) - 1, // Start code is ignored, we're interested in dimmer data
e131.stats.num_packets, // Packet counter
e131.stats.packet_errors, // Packet error counter
packet.property_values[5], prev_value); // Dimmer data for Channel
prev_value = packet.property_values[5];
debounce = 0;

} else {
// This clause gets entered when the same DMX value appears twice-in-a-row.
// Three or more get thrown out, but only the second occurrence triggers the callback.
if (debounce==0) {
Callback_Channel_5();
debounce = 1;
}
} // if value==prev_value/else
} // if universe==2/else
} // if packet empty
} // loop
 
I'm using this on-stage right now, and it works. I haven't measured the lag, but it is acceptable for us. I would be more concerned about lag in trying to stop your effect than starting it: if you want the network to tell you when to stop the effect as well, you're going to have to poll the network even as you are playing the effect. That's going to be more a function of your specific callback.
 
Back
Top