wireless lighting control arduino

A lot of people ask me about how to control and sequence lighting, either for installations, displays, or wearable designs so I figured I'd put together this tutorial. There's both a 2-part video tutorial and a text-based tutorial for people that prefer either style. The videos and text tutorial are after the jump.

 

Timing Summary:

00:15 - Building the LED circuit on a breadboard
01:46 - Configuring Vixen software
03:40 - Setting up a new lighting sequence
05:12 - Inspecting sequencer serial output visually
07:08 - Writing Arduino sketch to decode sequencer data
10:28 - Testing lighting sequence on Arduino system

Timing Summary:

00:32 - Breaking up original system into transmitter & receiver
01:05 - writing freakduino transmitter code
03:00 - Writing freakduino receiver code
04:52 - Testing wireless system with Vixen
05:48 - Interfacing LED tape to our lighting circuit with transistors
06:55 - Testing system outdoors
07:42 - Real world examples: UPT flair bartending and Wrecking Crew Orchestra

 

A lot of people have contacted me with interest in wirelessly controlling lighting so I thought I'd put together this tutorial. Actually, this tutorial is an update since my original tutorial demonstrated how to wirelessly control lighting using DMX and the freeware, Vixen v2. Since then, they supported a generic serial interface which made things much easier. There is also a video tutorial that walks you through all the steps I'm going to detail in here and I recommend you go through that, especially if you're new to Arduino or Vixen. 

Before we get into the wireless side of things, I think it's best to demonstrate how to control lighting using a wired connection. Things get more complicated when wireless comes into the picture. We'll start by setting up and controlling six LEDs from an Arduino and a breadboard. I'm using the FredBoard (Freaklabs Breadboard), an integrated Arduino compatible system + breadboard but you can also just use a standard Arduino and breadboard if you have those lying around.  

Here is the connection diagram for the LEDs.

freakduino arduino LED connection

We'll be connecting them to digital pins D2 through D7 for a total of six outputs and those will correspond to six channels that we'll be using on the sequencer. The LEDs will be connected via 100 ohm current limiting resistor which puts about 20 mA through them when they're on. That's roughly the limit of current the AVR outputs can drive and usually the sweet spot for standard LEDs.

This is what the final system should look like:

2

I usually like to start off by blinking LEDs because if you can blink an LED, you can control anything. Once you have the LEDs going, it's just a matter of interface electronics to electrically control just about anything. 

Next we're going to go into the Vixen software. In my original tutorial, I demonstrated using Vixen 2. That software was actually a horrible mess which crashed all the time. That's definitely not what you want in a live performance situation. Vixen 3 is much better and much more stable. Make sure you have a PC capable of running Windows 7 since Vixen 3 does not support Windows XP. You can pick the software up here.

Vixen 3 is a bit more complex to set up than Vixen 2 and you have to wrap your head around the way they abstracted the lighting and map it to the outputs. I highty recommend going through the video tutorial for the setup part so you can see exactly how it's done, but I'll also try to explain it here. 

We're going to start off by setting up the display

3

We need to configure the lighting elements we'll be using. In our case, since we're just using six LEDs, I'm going to configure six single item elements. Single item elements are dimmable lights or devices that can have a value from 0 to 255. 

4

You can also set the dimming curve and the display color. The dimming curve comes in handy since many LEDs have more of a logarithmic dimming profile than linear. You can use the dimming controls to linearize the devices you are using so they fade evenly from 0 to 255 or whatever range you are using.  

5   

6

Now we can set up the sequencer output. I'll be using the Generic Serial output which sends data out to the serial port. You have to configure the number of channels you want the output to have. It will output one value for each channel to the serial  port. In our case, we want a one-to-one mapping of the lighting elements we created, the LEDs in our case, and the channels of our serial ports. So I'm going to create six channels.

7  

8

Then we need to configure the serial port parameters. Vixen 3 is so much more convenient than Vixen 2 since Vixen 2 only allowed you to use COM1-4. Vixen 2 automatically searches for the used COM ports and allows you to span the complete range of COM ports on Windows. The COM settings are fairly standard and I will be using 57600 bps, 8 bits, No parity, and 1 stop bit, ie: 57600, 8N1.

12

Next, I'm configuring the sequencer output to send a header which will be an ASCII “+>”. The header will help the software synchronize in case it gets lost in a frame somewhere. In the worst case, it will lose two frames of data before resynchronizing. This corresponds to about 20 msec if we set the sequencer to a 10 msec resolution so it's almost nothing. I'm also using two ASCII text symbols because it's less likely to run into two sequential values when you're lost in a frame than one. Actually, you can also configure a footer value to encapsulate the frame. Then you can auto-calculate the number of channels in software. This adds a bit more complexity to the software than I want to go into for this tutorial but in case you were curious, that's a pretty useful feature to have. 

11

Finally, we need to patch our LEDs to our outputs. This sets a relationship between the lighting element and the output channel. In this case, we have the simplest relationship which is one to one. To do this, you highlight both the lighting elements and the output channels and then click on the patch button. Once this is done, you should be able to see the relationship between the lighting elements and output channels using the graphical view.

9    

10

Now that the setup is finished, it’s time to start a new sequence. First click the start button to start the stream.

13

To start a new sequence, you click the New Sequence button, a new Sequencer window will pop up and using the information we just configured, you should see the lighting elements in the window.

14

Now it’s time to add in the audio and test out the music on the timeline.

15

This next step isn't required but it makes things much easier. I’ll be using the built-in beat detection algorithm to generate markers on the timeline. It makes sequencing the lighting much more automated.

16

17

After that, I'm going to configure the effects to snap to the markers with a high snap strength. Markers are a great way to precisely control lighting events since you can have them automatically generated and can tweak them as needed. You can also instruct the lighting effects to snap to the markers so you can align events easily

18

19>

I’d heavily recommend checking out the video to see exactly how all of this is done.

Once the sequence is finished, it’s time to start working on the Arduino code. To do this, its useful to first look at the data that comes out of the serial port to see what the format is. This part isn’t really needed, but it helps build insight into what we’re doing. Check out the video from to see how this is done.

#define MAX_CHANNELS 6

int ch;
int state;
int chVal[MAX_CHANNELS] = {0};
int pins[] = {2, 3, 4, 5, 6, 7};

enum states
{
  IDLE,
  DELIM,
  READ,
  DISP
};

void setup()
{
  for (ch=0; ch<MAX_CHANNELS; ch++)
  {
    pinMode(pins[ch], OUTPUT);
    digitalWrite(pins[ch], LOW);
  }
  
  state = IDLE;
  ch = 0;
  
  Serial.begin(57600);
}

void loop()
{  
  if (Serial.available())
  {
    switch (state)
    {
      case IDLE:  
        ch = 0;
        if (Serial.read() == '+')
        {
          state = DELIM;          
        }
        else
        {
          state = IDLE;
        }
      break;
        
      case DELIM:
        ch = 0;
        if (Serial.read() == '>')
        {
          state = READ;
        }
        else
        {
          state = IDLE;
        }
      break;
      
      case READ:
        chVal[ch++] = Serial.read();
        if (ch >= MAX_CHANNELS)
        {
          ch = 0;
          state = DISP;
        }
      break; 
      
      case DISP:
        state = IDLE;
        for (ch=0; ch<MAX_CHANNELS; ch++)
        {   
          if (chVal[ch] > 0)
          {
            digitalWrite(pins[ch], HIGH);
          }
          else
          {
            digitalWrite(pins[ch], LOW);
          }
        }
      break;
    }
  }
}

For the Arduino code, I implement a state machine which is useful for decoding serial protocols. The state machine will have four states. The first state is the IDLE state which is the default state. In this state, we wait for the ‘+’ symbol which is the first start of frame marker. If we see it, then we move to the second state, DELIM. In the DELIM state, we want to see the second delimiter marker, ‘>’, which will indicate the start of the frame. This is just to reduce the number of false positives for the start of frame since the chances of having two exact sequential numbers is quite low. 

If we see the second delimiter, then we go into the READ state where we read the remaining frame and store it into our array. Once this is finished, we transition to the DISP state which is where we display our data. In the DISP state, we loop through each value in the array and if its nonzero, we turn the LED on. Otherwise, we turn the LED off. If we implemented PWM on these pins, then we can actually display the brightness corresponding to the numerical value instead. But I’m just using on/off to keep the code and this demo simple.

That’s pretty much it for controlling and sequencing lighting with an Arduino. In the next part, we’re going to extend what we learned to the case of wireless.

For this second part, you can see that I've moved away from using the Fredboard, which was the integrated Freaklabs Arduino + breadboard, and implemented the same circuit using a discrete breadboard + a freakduino.

DSC06008  DSC06009

DSC06010  DSC06010

In this case, I'm using the 900 MHz long range version of the freakduino which is ideal for going through places with a lot of thick walls or where you need a lot of range. Depending on your application, it might be overkill and a standard 900 MHz version would also work. I prefer using the 900 MHz license free band whenever I can since the range is much better and its less crowded due to the lack of WiFi and other devices that use this band. For theater performances which are mission-critical, it’s best to use this band since many people use WiFi during theatrical performances which can interfere with communications.

Now we're going to dive straight into the transmitter code. It’s almost the same as our original code from the wired portion we just finished. The main difference is that we'll be using the chibiArduino stack to send the data wirelessly, rather than acting on it locally. chibiArduino is a wireless protocol stack that I wrote based on the open IEEE 802.15.4 standard. I simplified a lot of the configuration and features of that standard so that you basically just need to initialize it and then send and receive data. I realized a while back that most people don't care what standard they use and just want to send and receive wireless data. 

#include <chibi.h>
#define MAX_CHANNELS 6
#define MY_ADDR 5
#define DEST_ADDR 3

int ch;
int state;
byte chVal[MAX_CHANNELS] = {0};

enum states
{
  IDLE,
  DELIM,
  READ,
  DISP
};

void setup()
{
  state = IDLE;
  ch = 0;
  
  chibiInit();
  chibiSetShortAddr(MY_ADDR);
  
  Serial.begin(57600);
}

void loop()
{  
  if (Serial.available())
  {
    switch (state)
    {
      case IDLE:  
        ch = 0;
        if (Serial.read() == '+')
        {
          state = DELIM;          
        }
        else
        {
          state = IDLE;
        }
      break;
        
      case DELIM:
        ch = 0;
        if (Serial.read() == '>')
        {
          state = READ;
        }
        else
        {
          state = IDLE;
        }
      break;
      
      case READ:
        chVal[ch++] = Serial.read();
        if (ch >= MAX_CHANNELS)
        {
          ch = 0;
          state = DISP;
        }
      break; 
      
      case DISP:
        state = IDLE;
        chibiTx(DEST_ADDR, chVal, MAX_CHANNELS);
      break;
    }
  }
}

If you look at the code, there are a few changes that I made like defining the source and destination addresses. The source address is our own address and the destination address is the address we want to send the data to. 

I’m also initializing the chibiArduino stack using the chibiInit() function which sets the stack to default values and programs the registers to let it know that we’ll be sending data through soon. I’m also setting the address of the device using the chibiSetShortAddr(…) function in case any other device wants to talk to us. Once we set the address on the device, that will be the identifier other devices can use to talk to us. We only need to set the short address once and it gets stored in nonvolatile memory but in this case, we set the address each time we power up. 

We'll still be using the state machine to decode the serial protocol from Vixen. In the loop code, the main difference is in the DISP state where instead of looping through the data array and switching the LEDs, we take the array and transmit it wirelessly to the receiver using the chibiTx(…) function. The chibiTx(…) function takes three arguments, the destination address we want to send the data to, the data in byte array form, and the length of data that will be sent. That's pretty much it for our wireless transmitter.

#include <chibi.h>

#define MAX_CHANNELS 6
#define MY_ADDR 3
#define DEST_ADDR 5

int pins[] = {2, 3, 4, 5, 6, 7};
int i;

void setup()
{
  for (i=0; i<MAX_CHANNELS; i++)
  {
    pinMode(pins[i], OUTPUT);
    digitalWrite(pins[i], LOW);
  }
  
  chibiInit();
  chibiSetShortAddr(MY_ADDR);
  Serial.begin(57600);
}

void loop()
{
  int i;
  
  // Check if any data was received from the radio. If so, then handle it.
  if (chibiDataRcvd() == true)
  { 
    int len, rssi, src_addr;
    byte buf[100];  // this is where we store the received data
    
    // retrieve the data and the signal strength
    len = chibiGetData(buf);

    // discard the data if the length is 0. that means its a duplicate packet
    if (len == 0) return;    

    rssi = chibiGetRSSI();
    src_addr = chibiGetSrcAddr();
    
    // Print out the message and the signal strength
    Serial.print("Data from node 0x");
    Serial.print(src_addr, HEX);
    Serial.print(": "); 
    for (i=0; i<len; i++)
    {
      if (buf[i] > 0)
      {
        digitalWrite(pins[i], HIGH);
      }
      else
      {
        digitalWrite(pins[i], LOW);
      }
      
      Serial.print(buf[i]);
      Serial.print(" ");
    }
    Serial.print(", RSSI = 0x"); Serial.println(rssi, HEX);
  }
}

Now we're going to go to the receiver side where we get the data wirelessly and act on it by blinking the LEDs. Again we start off by setting our defines including our source and destination addresses. In this case, the addresses are reversed. Since we're on the receiver side, our source address is the transmitter's destination address and vice versa. We then initialize the pins and chibi stack in the setup function, and this time we have a bit of luxury where our serial port is free. This means we can also print out debug messages to the serial port. Be careful about this though since printing messages takes a lot of time. If you print too much under heavy traffic, you could overflow your receive buffer. 

In the loop function we have a bit of new code. I’ll start by writing out the basic code to receive data via the chibiArduino stack. In the loop function, we check if data was received from the stack using the chibiDataRcvd() function. If data was received, the function returns true which means we will begin our receive procedure. To receive data, we declare a variable called len which holds the length in bytes of data we will receive as well as a byte array of data 100 bytes long. The reason I choose 100 bytes is because that’s the maximum payload size of the chibiArduino stack. You can also choose a smaller array size to save RAM. 

The chibiGetData(…) function is then called with the byte array as an argument. It fills the array in with the received data and returns the length of data that was received. I also do a length check to make sure the length is not zero. If the length is zero, it means it’s a duplicate packet, or one that we've already received before, so I just discard it. The data received should be six bytes so I then go through the received data byte by byte and switch the corresponding LED pin based on if it's 0 or 255.

I’ve also added a bit of embellishment for the development side of things so that we can get some extra data from the chibiArduino stack. It's not necessary but could help debug issues we might have. I’ve added two extra variables, one for the source address and one for the RSSI or the received signal strength indicator. The source address is useful in case there are multiple transmitters. It’s always a good idea to log the address that data is coming from since it’s sometimes possible there might be a rogue transmitter or a device that just goes crazy. For RSSI, higher values correspond to higher signal strengths. This is useful information that can be used to debug situations where the signal strength is too weak for the receiver to decode properly. It’s also very useful to conduct site surveys and understand where weak areas of signal are. 

Now that we can wirelessly control and sequence our LEDs, controlling larger, more substantial lighting displays is just a matter of interface circuitry. If you check out the video (5:51 of Part 2), you can see examples of interfacing to other types of lighting as well as some case studies of lighting control used in real life situations.

That’s it for the tutorial. Hope you enjoyed it.

Akiba

Add comment


Security code
Refresh