LED strip control setup using Arduinos and Raspberry Pis

This article details the former control setup of my LED strip installation. The LED strips were controlled with Arduino Nanos, which receive serial commands from Raspberry Pis. Those in turn receive network messages from a Processing sketch running on my computer. This allowed me to set a colour or recall preprogrammed effects from the Arduinos’ memory.

I originally made this scheme for a theatrical gig. You can read all about it here. Let me stress first that while this setup works well, I would not recommend it for individually addressable strips like the WS2812 or SK6812. It’s much better to get a dedicated controller. Arduinos are great when you only need some preprogrammed effects, that might or might not be controlled by external inputs (sensors, buttons, …). They’ll also work fine for standalone projects. But as soon as you are planning on having a laptop control the setup, I suggest you look into controllers like the Pixelpusher. Then you can have individual pixel control across all the devices, changing programs from the comfort of your laptop instead of going out on stage reprogramming every Arduino. Also, a single dedicated controller is much cheaper than a lot of arduinos+raspis. Using the LED strip control setup described here, it will not be possible to stream entire frames to the strips, only recall preset effects.

With that out of the way, some people might find my research useful. In any case, this post will explain how to drive RGBW strips, as opposed to only RGB. This post is also useful for people looking into making a setup with synchronised devices that run without laptop. In addition, you can place the Arduino close to the LED strip, which is better. This can be impractical with a dedicated controller with a lot of outputs, but you want to place the LED strips a certain distance apart. If you require a single network node per strip or small group of strips, this setup will be cheaper than a single dedicated controller per strip.

LED strips running a preprogrammed rainbow effect

This blog post is very long and intensive, with very little media. Or, as the kids these days call it, “boring” and “tedious”. Just a little warning. And if you can make it to the end, there will be a link to all the code on my Github as a reward. So hang on to your head(s), and enjoy the read!

For this project, I’ve used the following parts (but the setup can scale to whatever hardware you need):

  • SK6812 RGBWW (warm white) LED strips, 60 LEDs/meter, 32 meter total (1920 LEDs), in eight strips of four meter
  • 8 Arduino Nano
  • 4 Raspberry Pi
  • 8 5V 20A power supplies
  • Network gear: wifi router, ethernet cables
  • Other cables: 3-pin JST connectors, solderless 3-pin clip-on connectors, power and signal cables, wire terminals
  • Hardware to glue the strips on (16 square tubes, 1.5 cm x 1.5 cm x 2 m)

Step 1: the Arduino code

Driving RGBW strips

Let’s start at the end point of the signal chain. A simple Arduino Nano does a great job of running LED strips. The Adafruit Neopixel library has some wonderful examples. It’s as simple as connecting the desired Arduino pin to the data line of the strip, the 5 V to the power supply and all grounds together, and boom, light! The Neopixel library even has RGBW support. The RGBWstrandtest example works fine for the SK6812RGBW strips. 

Other libraries, like the FastLed library, will not work as they do not have native RGBW support.

The Adafruit Neopixel library is fine, but limited in some ways. For example, there is a limit on the amount of pixels you can attach. By chance however, I stumbled on this: NeoPixels Revealed: How to (not need to) generate precisely timed signals, by Josh Levine.

Luckily, it turns out that NeoPixels are not really that picky about timing once you get to know them.

Josh

This article is a great read if you want to understand the way SK6812 or WS2812 strips work. It goes into detail about the timing constraints and what timing you can get away with. More importantly, the author generously provides an Arduino sketch that implements these timing constraints in ASM with minimal overhead! I’ll be using his code as a basis for this project.

SK6812 chips are similar to WS2812, but with slightly different timing. You can find these values in the SK6812RGBW data sheet

WS2812SK6812
T0H (ns)350
300
T1H (ns)
700600
T0L (ns)800900
T1L (ns)600600
Reset (ns)50 00080 000

To make Josh’ Arduino sketch work with RGBW strips, only a very simple modification is required to the sendPixel() line:

inline void sendPixel( unsigned char r, unsigned char g , unsigned char b, unsigned char w )  {

  sendByte(g * stripint / 255);      // Neopixel wants colors in green then red then blue order
  sendByte(r * stripint / 255);
  sendByte(b * stripint / 255);
  sendByte(w * stripint / 255);      // <--  The added line

}

And that’s it! The API is now compatible with RGBW strips. In theory, you should also change the timing constants to the values in the table above, but the WS2812 values should fall in the SK6812 error margins. Do as you like.

Conversely, if you’re using RGB WS2812 or SK6812 strips, you can just remove that line. It will not break any other code that follows. The extra white channel will just not get processed.

Programming some effects

As an exercice, let’s adjust the effects from Josh’ code to include the extra white colour.

showColor()

A simple function that sends a single colour to all pixels. Just add the white and be done with it.

// Display a single color on the whole string

void showColor( unsigned char r , unsigned char g , unsigned char b , unsigned char w) {

  cli();
  for ( int p = 0; p < PIXELS; p++ ) {
    sendPixel( r , g , b , w);
  }
  sei();
  show();

}

Notice how you need to use cli() and sei() around the sendPixel() calls. The statement cli() disables interrupts, the sei() statement reenables them. This is necessary as interrupts will screw up the timing of the strips. However, interrupts are required for the serial communication with for example Raspberry Pis. It is not possible to send serial data to the Arduinos while the LED strips are updating! This makes things more complicated in the future.

colorWipe()

Gradually add a colour to the LED strips. Adjust it in the same way as the showColor() function.

// Fill the dots one after the other with a color
// rewrite to lift the compare out of the loop
void colorWipe(unsigned char r , unsigned char g, unsigned char b, unsigned char w, unsigned  char wait ) {
  for (unsigned int i = 0; i < PIXELS; i += (PIXELS / 60) ) {

    cli();
    unsigned int p = 0;

    while (p++ <= i) {
      sendPixel(r, g, b, w);
    }

    while (p++ <= PIXELS) {
      sendPixel(0, 0, 0, 0);

    }

    sei();
    show();
    delay(wait);
  }
}

I’ll give you one more…

rainbowCycle()

Displays a nice scrolling spectrum on the strips. This one is a bit harder to adapt. Not because of the extra white channel – there’s no white in rainbows, so I’m just going to omit it – but because of the timing. This function runs through the whole cycle before exiting. While it is doing that, it cannot be interrupted and move to the next effect if desired. Therefore, it needs to be rewritten so that it moves on to the next step when we want it to. I’ve gotten rid of the outer for loop (that is now replaced by the Arduino loop() ) and made firstPixelHue a global variable (named counter, as I also reused it in other effects), so the effect can keep track of it.

void rainbowStep() {

  // Hue is a number between 0 and 3*256 than defines a mix of r->g->b where
  // hue of 0 = Full red
  // hue of 128 = 1/2 red and 1/2 green
  // hue of 256 = Full Green
  // hue of 384 = 1/2 green and 1/2 blue
  // ...

  unsigned int currentPixelHue = counter;

  cli();

  for (unsigned int i = 0; i < PIXELS; i++)
  {

    if (currentPixelHue >= (3 * 256)) {              // Normalize back down incase we incremented and overflowed
      currentPixelHue -= (3 * 256);
    }


  
    unsigned char phase = currentPixelHue >> 8;
    unsigned char step = currentPixelHue & 0xff;

    switch (phase) {

      case 0:
        sendPixel( ~step , step ,  0 , 0);
        break;

      case 1:
        sendPixel( 0 , ~step , step, 0 );
        break;

      case 2:
        sendPixel(  step , 0 , ~step , 0);
        break;

    }

    currentPixelHue += 10;


  }

  sei();

  show();
  delay(25);

  counter += value;


}

//Reusable counter variable
long counter = 0;

void loop()
{
  rainbowStep();
}

Making the Arduino code serially controllable

The program state variable

Now that we have some effects, it’s time to think about how we can recall them. I have assigned a character to every effect: ‘c’ for static colour, ‘r’ for rainbow, ‘s’ for strobo and so on. This character can be used as a key to determine what effect is currently active. The variable “program” stores the key of the currently enabled effect, as some kind of state variable.

//The program variable keeps track of which effect is currently enabled
unsigned char program = 'c';

//The current colour parameters (red, green, blue, white)
unsigned char data1 = 255, data2 = 0, data3 = 0, data4 = 0;

//Timing variables
long prevTime = 0;
long prevTimeProgram = 0;
boolean on = true;

//Reusable counter variable
long counter = 0;

//Selectable strobe frequency
unsigned char value = 0;

void loop() {
  //Pick the currently enabled program
  switch (program)
  {
    case 'c':   //static colour

      showColor(data1, data2, data3, data4);

      break;
    case 's':   //strobo
      if (millis() - prevTimeProgram > 1000 / value)
      {
        on = !on;
        prevTimeProgram = millis();
      }
      if (on) showColor(data1, data2, data3, data4);
      else showColor(0, 0, 0, 0);
      break;
    case 'r':   //rainbows!

      rainbowStep();

      break;
  }
}

The serial message structure

Now, we want to select the program variable using a serial command. In addition, it would be nice if we could also send an RGBW colour and a value for the effects (eg. strobe frequency, rainbow scroll speed, …). So, that would be a 6-byte message:

byte program (‘c’, ‘s’, ‘r’, …)
byte value (frequency, scroll speed, …)
byte data1 (red)
byte data2 (green)
byte data3 (blue)
byte data4 (white)

Receiving messages while interrupts are disabled

Now, before we try to send anything, remember that we’re not allowed to send any messages to the Arduino while it’s updating strips. Therefore, we have to add some way of telling the controller (Raspberry Pi or laptop) to wait sending stuff while the Arduino is still updating. This can be a simple byte sent as a serial message back to the controller, say, ‘u’ for Unsafe and ‘s’ for Safe. When the computer sees an ‘u’, it should wait with sending a command until it sees an ‘s’.

It’s still possible that a serial message arrives while the leds are updating. It could be that the computer just missed the ‘u’ message while it has already started transmitting. When interrupts are disabled on the Arduino, the message will arrive garbled or incomplete. We have to be prepared for such an event. As a first measure, I’m going to state that the serial message should start with an extra character byte, ‘m’ for message. If the message does not have this byte in the correct position, it’s probably garbage and we’d better wait for the next one. It’s important that the computer is notified of this event, and the Arduino should respond with an error message, let’s say a simple character byte ‘e’. Then the computer can retry, to avoid any lost messages.

It can be that the message is missed entirely. Therefore the Arduino should let the computer know when it successfully got a message, with a response message ‘a’ for accepted. The computer should wait for this message and if it doesn’t receive them after a certain time, resend the command.

A checksum-ish thing

There’s one more thing we can do: add a checksum to the end of the message. This is a byte that somehow combines the bytes in the previous message. If the message is changed or incomplete, the checksum will give a different result and the receiver knows something goofed up. It can then respond with an error message, and the controller can send the message again. I’ve opted to simply add all values together in a byte. The result might overflow, but that’s not a problem for the checksum. Of course, it’s possible that a wrong message might accidentally result in the correct checksum, but it will still result in a great reduction of wrong messages. The probability of a message being wrong and a checksum beng accidentally correct is very small. 

If the checksum is wrong, the Arduino replies with an error code and the checksum it calculated. This is useful for debugging the checksum code. 

This isn’t a proper checksum implementation though – normally, if the checksum fails, the message should be abolished. 

Putting it together

Lots of explanations, let’s put it all in code! All the message integrity checking might seem overly complicated, and it probably is, but I’ve had problems with reliability so it’s worth it in my opinion.
I’ve also added a line that lets you send a master intensity using the ‘i’ command. 

// If a serial message is received, this code will execute

void serialEvent()
{
  //Discard messages that are too short
  if (Serial.available() > 7)
  {
    
    //Start parsing the incoming message


    //First byte: a check character, should always be 'm'
    unsigned char checkM = Serial.read();
    if (checkM != 'm')
    {
      //We lost track...

      //Let the computer know
      Serial.write('e');
      Serial.write(0);
      Serial.flush();

      //If there is anything left in the Serial buffer, we're no longer interested in it
      while (Serial.available() > 0) Serial.read(); 
      return;

    }

    //Second byte: the program byte - can be 'c', 'r', 's', 'i', ...
    unsigned char command = Serial.read() & 0xff;

    //Third byte: value - strobo frequency, rainbow scroll speed, etc.
    value = Serial.read() & 0xff;

    //Fourth - seventh bytes: data (mainly RGBW values)
    data1 = Serial.read() & 0xff;
    data2 = Serial.read() & 0xff;
    data3 = Serial.read() & 0xff;
    data4 = Serial.read() & 0xff;

    //Eight byte: checksum value
    unsigned char checkSum = Serial.read() & 0xff;
    
    //primitive checksum: just add all values together, and let it overflow at will
    unsigned char check = checkM + command + value + data1 + data2 + data3 + data4;

    //If checksum is not correct, something went wrong!
    if(check != checkSum)
    {
      //Let the computer know
      Serial.write('e');
      Serial.write(check);
      Serial.flush();

      //If there is anything left in the Serial buffer, we're no longer interested in it
      while (Serial.available() > 0) Serial.read(); 
      return;
    }

    //There really shouldn't be anything left
    while (Serial.available()) Serial.read(); //flush

    //With the 'i' command, you can change the global intensity of the strip
    if (command == 'i') stripint = value;
    //If the command isn't 'i', it's the next program value
    else program = command;

    //Reset the program if it changed
    if (program != prevProgram)
    {
      prevProgram = program;
      counter = 0;
    }

    

    //Great succes! Me like!
    Serial.write('a');
    Serial.write(check & 0xff);
  }
}

The complete Arduino code

can be found on my Github.


Step 2: the local Processing sketch

In principle, any program that can communicate serially with the Arduino can now control the LED strips, as long as it properly implements the requirements in the first step. My favorite framework is Processing, which is not only easy to program in, it’s also the IDE that the Arduino IDE is based on. A match made in heaven! The goal is now to write a Processing “sketch” as they call it, that sends messages to the Arduino serially. In a later stage, this sketch can in turn also be remotely controlled over network.

Signal flow layout for the led strip control setup. A master Processing sketch is running on the main laptop, generating commands based on the user input. It relays these commands to all connected remote or slave sketches via network. These slave sketches (running anywhere – on a Raspberry Pi, the main laptop itself or any other compatible device) then pass the command on to any connected Arduinos, serially over USB.  

Sketch concept

There will be two sketches: one (this one) that runs on Raspberry Pi on stage and communicates serially with the Arduinos, and another one that runs on the laptop and communicates with the first sketch running on the Raspberry Pis. Communication between the laptop and the Raspis happens via ethernet. Let’s first focus on the serial communication between the Raspi and the Arduino.

Since all this sketch will do is listen for incoming messages and push them straight to Arduinos, it should be possible to replace the sketch running on a Raspi by an Arduino ethernet expansion board. This is not a trivial task though as the timing constraints make it hard for reliable communication with the ethernet board. I’ve done some initial testing, but under pressure of a deadline, I’ve postponed this idea indefinitely. The raspis make handling timing much easier and have some benefits, like remote problem detection and synchronized standalone effects. You could use one Raspi as the master, sending messages to other Raspis on the network, and replacing the laptop as the central command station.

This sketch can run perfectly on a laptop for testing purposes, but let me introduce you first to a great tool in the Processing IDE developed by Gottfried Haider: Upload to Pi. This allows you to remotely upload sketches to a remote Raspi over network easily (via SSH) with a single button. The only requirements are that Processing is installed on the Raspi and SSH is enabled. You’ll also need to know the IP of the Raspi, and a username/password.

The tool can be installed by in the Processing IDE, doing Tools >> Add tools, and search for “Upload to Pi”. Install the tool, reboot Processing. In the preferences.txt file (see File >> Preferences) of Processing, you should now see these entries:

gohai.uploadtopi.autostart=true
gohai.uploadtopi.hostname=192.168.x.x
gohai.uploadtopi.logging=true
gohai.uploadtopi.password=xxx
gohai.uploadtopi.persistent=true
gohai.uploadtopi.username=xxx

Here you need to fill in the IP of the Raspi and a username/password. Note: only edit this file while the Processing IDE is closed!  Now, if you click on Tools >> Upload to Pi in the IDE, the Processing sketch should now be uploaded to the Raspberry Pi! Even better, when autostart=true, the sketch will automatically start running when the Raspi boots up. The only downside is if you have more Raspis, you need to close the IDE and rechange these values for every device. It’s of course still faster than any other alternative.

Communication requirements

It’s a good idea to reiterate what exactly is required to communicate with the Arduino.

  • Listen for serial messages from the Arduino
  • Do not send messages when the message ‘u’ is received
  • Recommence sending messages when the message ‘s’ is received
  • After every message, wait for the message ‘a’ and the checksum
  • If the Arduino replies with ‘e’ or a wrong checksum, or the message ‘a’ times out, retry

Message structure:

byte ‘m’ (fixed)
byte program (‘c’, ‘s’, ‘r’, …)
byte value (frequency, scroll speed, …)
byte data1 (red)
byte data2 (green)
byte data3 (blue)
byte data4 (white)
byte checksum (simple sum of the previous seven bytes)

Setting up the serial communication

It’s possible (and desirable) to connect multiple Arduinos to the same Raspi. So we need a structure that allows us to keep track of which Arduino is what. For this, I made a class named LedStrip, with its own Serial object.

import processing.serial.*;

ArrayList<LedStrip> ledStrips = new ArrayList();

void setup()
{
  size(400, 255);
  println(Serial.list());
}

void draw()
{
  background(0);
}

//Class containing all information belonging to a particular Arduino
class LedStrip
{
  String portName;
  Serial serial;

  public LedStrip(PApplet parent, String portName, int baudrate)
  {
    serial = new Serial(parent, portName, baudrate);
    this.portName = portName;
    println("Made new strip with name", portName);
  }
}

Experimentally, I’ve found that on Windows, there are no serial ports except connected Arduinos (this might depend on your setup). On Raspberry Pi however, some Serial objects are connected by default. Connected Arduinos show up as “USB”, which we can use to discriminate between LED strips and other stuff. Therefore, when scanning for new devices, a little check is required to determine the OS. Every led strip object gets a reference to its corresponding Serial object. Note that there is no code to formally identify a LED strip/Arduino combo. I’ve found that for static setups, the serial port name does not change unless the USB is replugged in another port. If you have issues with Arduinos randomly changing positions, you can use an unique identifier stored on the flash memory of the Arduino.

//Rescan for connected Arduinos
void resetStrips()
{
  //Get rid of the ones we already have
  ledStrips.clear();

  //Find all connected devices
  for (String s : Serial.list())
  {
    //On Windows, all connected devices should be Arduinos
    //On Raspi, only add the ones that have USB in their name
    if (System.getProperty("os.name").startsWith("Windows") || s.contains("USB"))
    {
      LedStrip newStrip = new LedStrip(this, s, 115200);
      ledStrips.add(newStrip);
      println("Added serial port", s);
    }
  }
}

Implementing the message format

Using a class to contain the message:

class StripMessage
{
  byte command;
  byte value;
  byte data1;
  byte data2;
  byte data3;
  byte data4;
  
  public StripMessage(char command, byte value, byte r, byte g, byte b, byte w)
  {
    this.command = (byte) (byte(command) & 0xff);
    this.value = (byte)(value & 0xff);
    data1 = (byte) (r & 0xff);
    data2 = (byte) (g & 0xff);
    data3 = (byte) (b & 0xff);
    data4 = (byte) (w & 0xff);
  }
  
  //Returns this message as a properly formatted 7-byte array, like the Arduino expects it
  byte[] getMessageAsByteArray()
  {
    byte checkSum = (byte) (byte('m') + command + value + data1 + data2 + data3 + data4);
    checkSum = (byte) (checkSum & 0xff);
    return new byte[]{'m', command, value, data1, data2, data3, data4, checkSum};
  }
}

Then, assign a message to a LED strip using its index in the StripMessage arraylist as a key:

//Set up the next scheduled message for the LED strip
void setMessage(int strip, char command, byte value, color c)
{
  //Somebody has not been paying attention
  if(strip >= ledStrips.size())
  {
    println("Error: invalid strip number! Connected strips:", ledStrips.size(), "Addressed strip:", strip);
    return;
  }
  
  //Create a new message (the bit shuffling is a faster way to extract colour data)
  //the alpha channel of the color is used as the value for white
  StripMessage message = new StripMessage(command, value, (byte) ((c >> 16) & 0xff), (byte) ((c >> 8) & 0xff), (byte) ((c ) & 0xff), (byte) ((c >> 24) & 0xff));
  
  
  try
  {
    ledStrips.get(strip).setMessage(message);
  }
  catch(Exception e)
  {
    println("Error happened when trying to set message:", e);
    e.printStackTrace();
  }
}

What creates such a message, and how that message is handled in the led strip object, is explained in later sections.

Handling communication with the Arduino

There are some strict constraints for sending serial message to the Arduino. For starters, no messages are allowed between ‘u’ and ‘s’, only between ‘s’ and ‘u’. Also, there is the need for error detection and checksum handling. It’s quite tedious to handle all this in the draw loop of the Processing sketch. Therefore, I have opted to give every led strip object its own thread for communication. The thread can run and keep on checking on the Arduino, while the main thread handles incoming messages and distributes them to the led strip threads. To make a new thread in Processing (or Java), make a class implement Runnable and implement the run() method. Then, call start() on an object of this class to make it run on a new thread. This is all done in the StripListener class, which listens to Arduino messages, and occasionally sends some too.

class StripListener implements Runnable
{
  Serial serial;
  String portName;

  //Runs until this variable is false
  boolean running = true;

  //Keeps track of the current Arduino state - if the Arduino sends 'u', this variable turns false and message sending is suspended
  boolean halt = false;



  //The next scheduled message, as a byte array
  byte[] messageByte;

  //If set to true, the message will be communicated to the Arduino as soon as allowed
  boolean sendMessage = false;

  //Timer variable to allow timeout detection
  float prevMessageTime = millis();

  //Time to wait for response until timeout occurs, in ms
  float timeoutTime = 50;

  //Should the listener check for timeouts?
  boolean checkForTimeout = false;

  //If there are a lot of timeouts in a row, the Arduino is probably disconnected
  int retries = 0;

  //if timeouts occur, connected will turn false again
  boolean connected = false;

  StripListener(Serial serial, String portName)
  {
    this.serial = serial;
    this.portName= portName;
  }

  public void run()
  {
    while (running)
    {
      synchronized(this)
      {
        if (serial.available() > 1)
        {
          //A message arrived!
          int message = serial.read();

          //If success is false, the message should be resent
          boolean success = false;

          if (char(message) == 'a')
          {
            success = true;
          } else if (char(message) == 'u') halt = true;
          else if (char(message) == 's') halt = false;
          else if (char(message) == 'e') 
          {
            //You can remove these printlns if they're annoying
            println("The Arduino received an invalid message!");
            success = false;
          }

          //A checksum always follows a status byte
          int checksum = serial.read();
          if (success) 
          {
            success = checksum((byte) (checksum & 0xff));
            if (!success)
            {
              //println("Checksum failed! Resending message...");
            }
          }

          //Send message again if an error occurred
          if (!success) sendMessage = true;
          //If successful, don't check for timeouts & reset timeout counter
          else 
          {
            checkForTimeout = false;
            retries = 0;
          }
        }

        //If allowed, send the message
        if (!halt && sendMessage)
        {
          sendMessage = false;
          checkForTimeout = true;
          //Reset the timer
          prevMessageTime = millis();
          sendDataToStrip();
        }

        if (checkForTimeout)
        {
          if (millis() > prevMessageTime + timeoutTime)
          {
            //Resend message
            sendMessage = true;
            retries++;
          }
          if (retries > 20) 
          {
            connected = false;
            mentionStripDisconnect(portName);

            //Suspend the thread
            running = false;
          }
        }
      }
    }
  }

  public void sendDataToStrip()
  {


    //Write the message to the Arduino
    if (messageByte != null)
    {
      serial.write(messageByte);
    }
  }


  //Performs the checksum (simply adding all bytes together)
  public boolean checksum(byte checksum)
  {

    int check = 0;
    if (messageByte != null)
    {
      for (byte b : messageByte)
      {
        check += (b & 0xff);
      }

      check = (byte) (check & 0xff);

      return checksum == check;
    }
    return false;
  }

  public synchronized void setMessage(StripMessage message)
  {
    messageByte = message.getMessageAsByteArray();
    //If a new message is set, send it as soon as possible
    sendMessage = true;
  }

  //Don't leave loose ends dangling
  public void dispose()
  {
    running = false;
    serial.dispose();
  }
}

The timeout code not only resends missed messages, it also detects when the Arduino was disconnected. This can be stored as a state in the StripListener object and communicated to the main thread. Then, the sketch can let this information know to the main control sketch, where the human can decide to investigate why a strip suddenly disconnects (did Karen trip over the wire again?).

The LedStrip class has a reference to its StripListener object, and starts it up too:

//Class containing all information belonging to a particular Arduino
class LedStrip
{
  String portName;
  StripListener listener;

  public LedStrip(PApplet parent, String portName, int baudrate)
  {
    this.portName = portName;
    
    //Make a new StripListener and start its thread
    Serial serial = new Serial(parent, portName, baudrate);
    listener = new StripListener(serial, portName);
    (new Thread(listener)).start();
    
    println("Made new strip with name", portName);
  }

  public void setMessage(StripMessage message)
  {
    if(listener != null) listener.setMessage(message);
  }
}

That should cover communication with the Arduino entirely!

Testing the sketch with some test messages

It should now be very simple to test the sketch by placing a setMessage() call somewhere in setup() or draw():

  //Strobe all strips
  for (int i = 0; i < ledStrips.size(); i++)
  {
    setMessage(i, 's', (byte) 100, color(0,0,0,255));
  }

or

  //Rainbows and unicorns
  for (int i = 0; i < ledStrips.size(); i++)
  {
    setMessage(i, 'r', (byte) 25, color(0, 0, 0, 255));
  }

I like to make my strips do a spectrum fade when nothing has taken external control yet:

boolean idle = true;

float hue = 0;

void draw()
{

  if (idle)
  {
    colorMode(HSB);
    hue += 0.1;
    if (hue > 255) hue = 0;
    color c = color(hue, 255, 255, 0);
    background(c);

    colorMode(RGB);
    
    for (int i = 0; i < ledStrips.size(); i++)
    {
      setMessage(i, 'c', (byte) 25, c);
    }
  }
}

Whew… we’re nearly there! All that’s left is making this sketch accept data over a network connection.

Make the sketch accept data over network

The default Processing network library makes it easy to send messages over a local network. In many ways, the ideas and syntax are similar to the serial library. 

We’ll be running a Server object at a certain network port. The code for this is simply

Server s;

void setup()
{
  s = new Server(this, 8008);
}

Then, when a client on a remote machine sent a message to the IP of the device running the sketch, a client will become available in the server. This client object will contain the message.

Client c = server.available();
  try
  {
    if (c != null)
    {
      //Quit idle mode when a network message has been received
      idle = false;

      //Parse the message

    }
  }
  catch(Exception e)
  {
    println("An error happened while trying to parse network message!");
    e.printStackTrace();
  }

Now is the time to determine what the structure should be of the messages. Ideally there’s two-way communication: the Raspi can tell if and how many strips are connected, and if an error has occurred. Also, there should be a variety of possible messages: one type to send data to the strips, another that makes the sketch call its resetStrips() method, one that asks how many serial devices are connected, and whatever else you might think is necessary.

I made the message structure a String detailing what the content of the message is (eg. “strip command” to push values to a led strip, or “status” to poll the connected devices). Then a new line character ‘\n’ follows, after which extra data does or does not follow.

In my experience, it’s not necessary to implement the excessive checks that were required for the Arduino communication. Network communication is quite reliable. No checksum, no special extra bytes.

The strip command message body is very similar to what is sent to the Arduino (so it can be pushed straight through). Of course an extra byte is required telling the program which strip is addressed. I omitted the initial ‘m’ byte and the checksum.

byte stripIndex (0, 1, 2, …)
byte program (‘c’, ‘s’, ‘r’, …)
byte value (strobe frequency, scroll speed, …)
byte data1 (red)
byte data2 (green)
byte data3 (blue)
byte data4 (white)

The complete message looks like this:

String “strip command”
character ‘\n’ (new line)
byte[] data (table above this one)

As soon as such a message is received, it should be pushed to the appropriate Arduino.

I added two extra messages: one that is simply composed of the String “reset”, which resets the serial objects, and “status”, which polls which strips are connected. Both messages should end with the newline character ‘\n’.

Implementing all this in code:

  Client c = server.available();
  try
  {
    if (c != null)
    {
      //Quit idle mode when a network message has been received
      idle = false;

      String message = c.readStringUntil('\n');

      if (message.equals("strip command\n"))
      {

        //Parse the message
        byte strip = (byte) (c.read() & 0xff);  //com port idx
        byte command = (byte) (c.read() & 0xff);
        byte value = (byte) (c.read() & 0xff);
        byte data1 = (byte) (c.read() & 0xff);
        byte data2 = (byte) (c.read() & 0xff);
        byte data3 = (byte) (c.read() & 0xff);
        byte data4 = (byte) (c.read() & 0xff);

        setMessage(strip, (char)command, value, color(data1, data2, data3, data4));

        //Reply with "S" for success
        c.write("S");
      }
      else if(message.equals("reset\n"))
      {
        resetStrips();
        c.write("strips reset");
      }
      else if(message.equals("status\n"))
      {
        String reply = "Connected strips: " + ledStrips.size() + " - ";
        
        for (LedStrip strip : ledStrips)
        {
          reply += strip.portName + " ";
        }
        c.write(reply);

      }

      while (c.available() > 0) c.read(); //flush
    }
  }
  catch(Exception e)
  {
    println("An error happened while trying to parse network message!");
    e.printStackTrace();

    //If possible, reply with "F" for failure
    if (c != null) c.write("F");
  }

The complete sketch

For my purposes, the sketch is finished. The next step is to write a sketch that sends the above-mentioned network messages to this sketch. For your applications, you might not need the additional sketch. You could just add a GUI to this sketch and use it to send messages to the Arduino, using the setMessage() calls as demonstrated here.

It’s also possible to use one Raspi (or any other device that can run Processing and communicate over network) as a master device, sending network messages to the other sketches without user input. This is great for a standalone setup that requires synchronised LED strips.

I’ve added the complete sketch to my Github page.


Step 3: Global sketch for led strip control

When I started out with this project, I envisioned individual pixel control from the computer until quite late into the project. I had already started working on software for this, until it became clear that it would not be feasible with this hardware. By then, there was no time to start coding anew. Making the LED strips function reliably took so much time, there was simply no time left to create a proper master control sketch. Since I had already written some parts of the master control sketch, I started hacking into it and hardcoded everything. Add in the time constraints (I think the code for this whole project took me only two crazy, sleepless weeks!), and you end up with some huge spaghetti. For this reason, I’ll not share that code. To give you an idea of what the interface looked like, here’s a photo of my screen:

I don’t have any proper screenshots, sorry

On the left, you see the seven connected strips. The center left console allowed me to pick two colours. I could assign one of those to each led strip individually. I could also assign a selectable effect. There was a little status icon displaying the connectivity status of each Raspberry Pi, and a button to reestablish connection if it was lost somehow.

Since I can’t share this sketch with you, I’ll do my best to recreate the functionality.

Network shizzle

To send a message to a server, we need an IP and a port. The port is chosen freely in the slave sketch (8008), the IP needs to be found in another way. You can do a simple println(server.ip()); in the slave sketch, but that result is not always correct (if you have multiple network adapers for example). If you’re running the master sketch on the same device as a slave sketch, you can just use “127.0.0.1”. Otherwise, you’ll need to find the IP of the remote device. On Windows, you can open a command prompt and type “ipconfig”. On Linux, open a terminal window and type “ifconfig”. On a typical local network, the IP will look something like “192.168.X.X”, but this will depend on your network setup. 

It’s best that your devices have a static IP. In your router setup page (typically accessed by browsing to 192.168.1.1 in a web browser – look in the user manual of your router) you can assign a static IP to a device over DHCP. Or, most devices allow you to specify a fixed IP that they will assume whenever possible (in a setup on the device itself, not in the router settings). The latter is usually best. Keep track of which device has what IP.

Sending a message to a remote device

Let’s be prepared to connect to a large amount (i.e. larger than one) of LED strips, and start right off with creating a class that contains all the information belonging to one strip.

class RemoteClient
{
  //Network client to send messages to
  Client client;
  String ip;
  int port;
  int comPortId;

  String message;
}

It’s possible that multiple Arduinos are connected to the same device. In that case, we have to reuse the processing.net Client object. So, while initialising a new RemoteClient object, we’ll have to check if the ip is already in use.

ArrayList<RemoteClient> clients = new ArrayList();

//Searches current clients for the given ip & port
//If no such clients are found, a new one is returned
Client getClient(String ip, int port)
{
  for(RemoteClient c : clients)
  {
    if(c.ip.equals(ip) && c.port == port) return c.client;
  }
  return new Client(this, ip, port);
}


class RemoteClient
{
  //Network client to send messages to
  Client client;
  String ip;
  int port;
  int comPortId;

  String message;
  
  RemoteClient(String ip, int port, int comPort)
  {
    this.ip = ip;
    this.port = port;
    this.comPortId = comPort;
    client = getClient(ip, port);
  }
}

Let’s implement some methods to send messages into the RemoteClient sketch:

  void sendReset()
  {
    message = "reset\n"+new String(new char[]{(char)comPortId,0,0,0,0,0,0});
    sendMessage();
  }
  
  void sendStatus()
  {
    message = "status\n"+new String(new char[]{(char)comPortId,0,0,0,0,0,0});
    sendMessage();
  }
  
  void sendCommand(char command, char value, color c)
  {
    message = "strip command\n"+new String(new char[]{(char)comPortId,command,value,(char) ((c >> 16) & 0xff), (char) ((c >> 8) & 0xff), (char) ((c ) & 0xff), (char) ((c >> 24) & 0xff)});
    sendMessage();
  }
  
  void sendMessage()
  {
    client.write(message);
  }

Now, we can finally test the network message:

void setup()
{
  size(1200, 800);
  RemoteClient newClient = new RemoteClient("192.168.1.3", 8008, 0);
  newClient.sendReset();
  clients.add(newClient);
}

The Arduino should now reset. 

Listening for return messages and polling status

If the remote sketch dropped from network, client.active() will return false. This can be used as a first measure to detect trouble. 

Since we’ve gone through all that effort to implement a way of replying to a status message in the slave sketch, let’s add some code that implements this.

void draw()
{
  background(0);

  for (RemoteClient client : clients)
  {
    client.listen();
  }
}

class RemoteClient
{
  //(...)
  void listen()
  {
    if (client.available()>0)
    {
      reply = client.readString();
      println(reply);
    }
  }

Now, we should see a reply printed to the console whenever we send a message to the remote sketch. Using the mousePressed() method for quick testing:

void mousePressed()
{
  if (mouseButton == LEFT)
  {
    for (RemoteClient client : clients)
    {
      client.sendCommand('r', (char)20, 0);
    }
  } else if (mouseButton == RIGHT)
  {
    for (RemoteClient client : clients)
    {
      client.sendReset();
    }
  }
}

Left clicking should trigger the rainbow effect, right clicking the reset command. We’re all set now to build a GUI!

Building a GUI

This is completely up to you how you want to implement this. I’ve chosen ControlP5 as the GUI library. It has a new colour wheel thing I wanted to check out. Explaining in all detail how I implemented the GUI would take way too long, so I’m just going to refer to the code. I’m happy to answer any questions in comments here or on Github.

Here were the goals I wanted to implement in the GUI:

  • See the network and serial status of all connected devices in real time
  • Ability to send the reset command to all devices individually
  • See the reply of the device
  • Change the IP of the target on runtime
  • Add new devices on runtime
  • Change the behaviour of devices: send a colour or effect command
  • Choose between two colours and an effect
  • A rudimentary scene/chase editor
  • Export and import settings

It would take me much too far to explain all the GUI code individually, so I’m just going to link to my Github again for the finished product.

GUI elements of the program
  1. Green rectangle by “Network”: Good news! The specified IP could be reached and a reply was received by the remote sketch – 1a: Red rectangle: Bad news! The IP could not be reached or is not running a remote sketch
  2. Green rectangle by “Serial”: a device was detected on this port! The number is the index of the connected device. The number between brackets after Serial is the amount of connected devices detected. – 2a: Red rectangle: either no device was detected, or the specified device index (in 3.) is too high.
  3. Device drag box. Drag down to change the index of the addressed serial device
  4. Enter IP address of the machine running the remote sketch here
  5. Select what should be sent to this controller: colour 1, colour 2 or an effect
  6. Execute: write the specified data (colour 1, colour 2 or effect) to the controllers
  7. Auto update toggle: for when you’re tired of hitting the Execute button
  8. Colour picker for colour 1. Drag the circle in the colour wheel to update, or enter RGB values (0-255) in the text fields below
  9. Slider for the extra white channel
  10. Colour picker for colour 2
  11. Value fader for effects. Actual property dependent on effect: rainbow scroll speed or strobe frequency (or whatever else you have programmed)
  12. Effect selector
  13. Load controllers and scenes file
  14. Export current controller and scene lists
  15. Reset all controllers. Don’t press during a show!
  16. Reset all scenes
  17. Fade time between colours. Higher = faster
  18. Toggles autoplay of the preset scenes
  19. Play the current scene
  20. Record the current values for all controllers into the scene
  21. State: records the state as opposed to the value. Instead of the current colour of the strip, it will send whatever value is currently set to colour 1, colour 2 or effect (depending on what is currently active in the controller)
  22. Value: records the current colour value
  23. Displays what the stored values are for every controller
  24. see 21 and 22
  25. add a new scene
  26. Playback duration of current scene, in seconds
  27. name of the scene
  28. add a new controller

There was more that I’d liked to add, but it’s functional and relatively simple. 

The complete sketch can be downloaded here.

3 Comments

  1. Pingback: The LED wall - CMB's blog %

  2. Reply

    Great project!

    BTW, if you ever need to do serial to the LED controllers again, you can take advantage of the fact that the Arduinos all have at least a 1 byte serial receive buffer in them. As long as you only send 1 byte at a time, then nothing will get lost. So in you system, you could basically just halt after ever byte sent and then wait for a `u` to come back to send the next byte.

    I’d love to see video of the thing running in all of its glory if you have any!

  3. Pingback: Turning an Ultra DMX Micro into an Art-Net node - CMB's blog

Leave Comment

Your email address will not be published. Required fields are marked *