Introduction: Remote-controlled Arduino Mood Lighting

Combine a close-out LED light strip with a tiny Arduino Pro Mini board, add remote control and you have a cheap, versatile mood lighting setup!

My bedroom light is on the ceiling. It's nice and bright, which is good when it's time to clean or put clothes away, but when it's time for bed, it's a bit harsh.

I wanted a soft light that would provide enough illumination to keep from stubbing my toes, but would be more appropriate for nighttime activities. And mood lighting may also enhance certain other shared bedtime activities.

A while back, while visiting my doomed Radio Shack (RIP), I ran across a meter-long LED strip that was on sale for under $10. I thought this would be a nice basis for an Arduino-based mood-lighting project so I bought all they had. As the project progressed, it had to have automation, so I added remote control capabilities using a spare remote and a $2 IR receiver. Finally, a small speaker was added to provide feedback to know when a button on the remote was pressed.

To save space, I used an Arduino Pro Mini (about ten bucks) and put the whole thing in a mint tin.

Step 1: Collect Parts

The full source code is below, so you don't need all these parts if you'd like to remove some things, like the remote control or speaker. You can also, of course, add other components or make changes to the color presets or beeps.

I used:

  • Arduino Pro Mini (I used 5v 16Mhz, ATmega 328). You'll need a little over 16K for this sketch. You'll also need an FTDI programmer as the Pro Mini does not have a programmer chip on-board.
  • Radio Shack (RIP) light strip.
  • Mint tin. I got this at a trade show, tossed the toxic mints and scratched off the vendor logo.
  • Barrel power connector. I pulled this out of an old radio
  • 12v wall wart. The Arduino Pro Mega requires an input of 5-12v, but the light strip requires 12v. I got this out of my big box 'o wall warts.
  • Remote control. Pretty much any remote you have lying around. I wanted a pair so that I could put one by the room door and the other on the bedstand.This is from an old Apple TV. I have two that look identical, but are different electrically. More on this later when you see the code.
  • Some kind of connector to disconnect the light strip. This is important if you want to re-program the Arduino. You can quickly unhook without bringing the light strip with you.
  • Speaker. This is a tiny speaker a friend gave me. You could also use a piezo transducer. You'll also need a resistor to limit current and protect the Arduino's digital pin. I used a small 100-ohm resistor.
  • IR receiver. There are many out there. I used a TSOP38238.
  • Code (listing in a later step)

Step 2: Prototype It

The Arduino Pro Mini is a great little board with a lot of power, but it isn't very good for prototyping. To build the prototype, I used an Arduino Uno. It has the same memory as the Pro Mini, which is probably the most important factor to make sure everything will port over.

Notice that the 12v barrel connector is utilized, in addition to the USB. The USB is usually used to power the Arduino board, and it also provides a serial interface for loading programs. But if you just use the 5v USB connector, the light strip will not light. You can program the Arduino just fine, but there is not enough voltage for the light strip. By connecting a 12v power supply as well, you will get the light.

Notice, also, that the red wire from the light strip is hooked to Vin, which is the 12v from the barrel connector. If you are accustomed to getting power off of the 5v pin, you won't have enough to drive the light strip.

Step 3: The Code

In writing this, I'm assuming the builder knows how to add a library to their Arduino development environment. You'll need to add two libraries. Here's an Instructable that will help you get started using an IR receiver.

The code makes use of two required libraries, and a third optional library that I always use when debugging. The Streaming library allows quick one-line statements that can be sent to the console terminal.

The excellent IRLib library is an improvement over several other IR libraries in that it encapsulates functionality in a way that only requires a couple lines to get IR functionality. It also has support for IR transmitting, which I don't use in this project. It is very well documented, and an example of good, tight code. This one is definitely one to keep in your bag of tricks.

There is a company that makes light strips using the same chip used by the Radio Shack (RIP) strip. Their library, PololuLedStrip, provides an easy-to-use interface into any strip that uses the TM1804 chip. The Pololu library also supports other chips used by most current LED strips.

The code is commented, but there are some things to point out:

  • The array presetColor is a list of colors that will be loaded when you press a button on the remote. I programmed this to the "Play" button on mine, but you can assign it to whatever you like. These are designated as RGB values, {255, 255, 255} being brightest white and {0, 64, 0} being a dimmer green, for example. You can create as many presets as you'd like. Notice that every other preset is {0,0,0}, which essentially turns off the lights. This is nice to quickly turn things off for sleepy-time.
  • In the loop() function, you'll see the IR decoder being called. There are several different types of infrared remotes out there, and the IRLib library can find many of them. You'll need to play around with this code to see exactly what your remote is sending so you can isolate the codes and assign them to functionality in your program. There is excellent documentation in a PDF file in the library to help you.
  • This particular remote delivers a unique value for each button pushed, but when the button is held down, it sends a value of 0xFFFFFFFF. All we need to do is remember the previous code sent and, upon seeing all F's, just process the last value.
  • There's a little function, playTone, that sends pulses out to the speaker. The Arduino development environment has a tone function, but it uses interrupts. Unfortunately, those same interrupts are used by the IRLib library, so we can't use tone and IRLib in the same sketch without significant modifications to the libraries. This simple function does everything we need for this simple application without using interrupts.Know that the higher the number, the lower the tone.
  • I have two remotes that look identical, but they provide different codes for each button. That's no problem when we use the magic of the C switch/case structure. You'll notice two cases for each block. This indicates the codes sent by each remote. I could conceivably have a dozen remotes, each giving a different code and trap them all using this method.
  • The up and down keys adjust the brightness and the right and left buttons walk through the color wheel.
  • The "Menu" button starts the color wheel mode. The idea is that you use the up/down/right/left keys to get the starting color and press the Menu button. The colors will start to change, running all the way around the color wheel until another key is pressed. This provides a relaxing mood lighting if you're into such things.
  • The "Play" button provides quick access to the preset colors that were mentioned earlier.
  • The program wraps up with conversion functions to convert between RGB and HSV.

The pins that I used were mostly for convenience or habit. You can change the pins to which the various parts are connected as long as you make the appropriate changes in the code.

HeadboardLight.h

typedef struct hsv_color
{
    int h;
    double s, v;
} hsv_color;

HeadboardLight.ino

// HeadboardLight.ino

// IRLib.h from IRLib – an Arduino library for infrared encoding and decoding<br>// Version 1.5   June 2014
// Copyright 2014 by Chris Young <a href="http://cyborg5.com" rel="nofollow"> http://cyborg5.com</a>
#include <IRLib.h>
   
// Streaming.h - Arduino library for supporting the << streaming operator
// Copyright (c) 2010-2012 Mikal Hart.  All rights reserved.
#include <Streaming.h>      // handy library to make debugging statements easier
   
// Pololu LED strip library. Available from <a href="https://github.com/pololu/pololu-led-strip-arduino" rel="nofollow"> http://cyborg5.com</a>
#include <PololuLedStrip.h> // library for light strip
#include "HeadboardLight.h" // include file for this project
   
//#define DEBUG // global debug declaration. uncomment to include debug code
   
// Create a receiver object to listen on pin 11
IRrecv My_Receiver(9);
IRdecode My_Decoder;
unsigned long value;
unsigned long last_value;
int j;
   
// Create an ledStrip object on pin 12.
PololuLedStrip<12> ledStrip;
int onboard_led = 13; // just for feedback to make sure we're receiving something
   
// Create a buffer for holding 10 colors.  Takes 30 bytes.
#define LED_COUNT 10
rgb_color colors[LED_COUNT];
rgb_color color;
// there are two color standards used here. RGB drives the light strip, 
// but HSV is more convenient for doing color wheel progressions. There
// are functions to convert between the two.
int hsv_h = 0;
double hsv_s = 1;
double hsv_v = 0.1;
bool colorWheel = false;
   
hsv_color hsv_current;
   
// quick presets. Every other one is off for an easy way to turn everything off
rgb_color presetColor[] = 
{
    {0, 0, 0},
    {255, 255, 255},
    {0, 0, 0},
    {16, 16, 16},
    {0, 0, 0},
    {64, 0, 0},
    {0, 0, 0},
    {0, 64, 0},
    {0, 0, 0},
    {0, 0, 64},
    {0, 0, 0},
    {64, 64, 0},
    {0, 0, 0},
    {64, 0, 64},
    {0, 0, 0},
    {0, 64, 64}
};
char presetColorSize = sizeof(presetColor)/3;
char presetTracker = 0;
int buzzPin = 7;

void setup()
{
#ifdef DEBUG
    Serial.begin(9600);
#endif
    My_Receiver.enableIRIn(); // Start the IR receiver
    color = hsvToRgb(hsv_h, hsv_s, hsv_v);
    writeLeds(color);
    // make sure pins are pointing the proper direction
    pinMode(onboard_led, OUTPUT);
    pinMode(buzzPin, OUTPUT);
}
   
void loop()
{
    //Continuously look for results. When you have them pass them to the decoder
    if (My_Receiver.GetResults(&My_Decoder)) 
    {
        My_Decoder.decode(); //Decode the data
#ifdef DEBUG
        Serial << "0x" << _HEX(value) << endl;
#endif
   
        if (My_Decoder.decode_type== NEC) 
        {
            value = My_Decoder.value;
#ifdef DEBUG
            Serial << "\t\tcase 0x" << _HEX(value) << endl;
#endif
            if (value == 0xFFFFFFFF)  // holding down for repeat
            {
                value = last_value;
            }
#ifdef DEBUG
            Serial << _HEX(value) << endl;
#endif
            switch(value) 
            {
            case 0x77E1D074:
            case 0x77E15054:
#ifdef DEBUG
                Serial << "up" << endl;
#endif
                hsv_v += 0.05;
                if (hsv_v > 0.95)
                {
                    hsv_v = 0.95;
                    playTone(244);
                }
                else
                {
                    playTone(122);
                }
                color = hsvToRgb(hsv_h, hsv_s, hsv_v);
                break;
            case 0x77E1E074:
            case 0x77E16054:
#ifdef DEBUG
                Serial << "right" << endl;
#endif
                hsv_h += 10;
                if (hsv_h > 359)
                {
                    hsv_h = 0;
                    playTone(244);
                }
                else
                {
                    playTone(200);
                }
                color = hsvToRgb(hsv_h, hsv_s, hsv_v);
                break;
            case 0x77E1B074:
            case 0x77E13054:
#ifdef DEBUG
                Serial << "down" << endl;
#endif
                hsv_v -= 0.05;
                if (hsv_v < 0.05)
                {
                    hsv_v = 0.0;
                    playTone(244);
                }
                else
                {
                    playTone(122);
                }

                color = hsvToRgb(hsv_h, hsv_s, hsv_v);
                break;
            case 0x77E11074:
            case 0x77E19054:
#ifdef DEBUG
                Serial << "left" << endl;
#endif
                hsv_h -= 10;
                if (hsv_h < 1)
                {
                    hsv_h = 359;
                    playTone(244);
                }
                else
                {
                    playTone(200);
                }

                color = hsvToRgb(hsv_h, hsv_s, hsv_v);
                break;
            case 0x77E12074:
            case 0x77E1A054:
#ifdef DEBUG
                Serial << "play" << endl;
#endif
                color = presetColor[presetTracker];
                ++presetTracker;
                if (presetTracker > presetColorSize) presetTracker = 0;
                for (j = 0; j < 2; j++)
                {
                    playTone(122);
                    delay(20);
                    playTone(244);
                    delay(20);
                }
                // set current hsv values to the preset
                hsv_current = rgbToHsv(color.red, color.green, color.blue);
                hsv_h = hsv_current.h;
                hsv_s = hsv_current.s;
                hsv_v = hsv_current.v;
#ifdef DEBUG
                Serial << "hsv(a): " << hsv_h << ", " << hsv_s << ", " << hsv_v << endl << "----" << endl;
#endif
                break;
            case 0x77E14074:
            case 0x77E1C054:
#ifdef DEBUG
                Serial << "menu" << endl;
#endif
                if (colorWheel) 
                {
                    colorWheel = false;
                    for (j = 1; j <= 5; j++)
                    {
                        playTone(j * 20);
                    }
                }
                else 
                {
                    colorWheel = true;
                    for (j = 5; j > 0; j--)
                    {
                        playTone(j * 20);
                    }
                }
                break;
            }
            last_value = value;
        }
        writeLeds(color);
        My_Receiver.resume();         //Restart the receiver
    }
   
    delay(30);
   
    // break into color wheel mode with current s and v
    if (colorWheel)
    {
        if (++hsv_h > 359) hsv_h = 0;
        color = hsvToRgb(hsv_h, hsv_s, hsv_v);
        writeLeds(color);
        delay(20);
    }
}
   
void playTone(int value)
{
    digitalWrite(onboard_led, HIGH);   // turn the LED on (HIGH is the voltage level)
    for (long i = 0; i < 64 * 3; i++ ) 
    {
        // 1 / 2048Hz = 488uS, or 244uS high and 244uS low to create 50% duty cycle
        digitalWrite(buzzPin, HIGH);
        delayMicroseconds(value);
        digitalWrite(buzzPin, LOW);
        delayMicroseconds(value);
    }
    digitalWrite(onboard_led, LOW);    // turn the LED off by making the voltage LOW
}
   
void writeLeds(rgb_color c)
{
    for(int i = 0; i < LED_COUNT; i++)
    {
        colors[i].red = c.red;
        colors[i].green = c.green;
        colors[i].blue = c.blue;
    }
    ledStrip.write(colors, LED_COUNT);
}
   
// Convert HSV to corresponding RGB
rgb_color hsvToRgb(int h, double s, double v)
{
    rgb_color rgb;
   
    // Make sure our arguments stay in-range
    h = max(0, min(360, h));
    s = max(0, min(1.0, s));
    v = max(0, min(1.0, v));
    if(s == 0)
    {
        // Achromatic (grey)
        rgb.red = rgb.green = rgb.blue = round(v * 255);
        return rgb;
    }
    double hs = h / 60.0; // sector 0 to 5
    int i = floor(hs);
    double f = hs - i; // factorial part of h
    double p = v * (1 - s);
    double q = v * (1 - s * f);
    double t = v * (1 - s * (1 - f));
    double r, g, b;
    switch(i)
    {
    case 0:
        r = v;
        g = t;
        b = p;
        break;
    case 1:
        r = q;
        g = v;
        b = p;
        break;
    case 2:
        r = p;
        g = v;
        b = t;
        break;
    case 3:
        r = p;
        g = q;
        b = v;
        break;
    case 4:
        r = t;
        g = p;
        b = v;
        break;
    default: // case 5:
        r = v;
        g = p;
        b = q;
    }
    rgb.red = round(r * 255.0);
    rgb.green = round(g * 255.0);
    rgb.blue = round(b * 255.0);
    return rgb;
}
   
// convert RGB to the corresponding HSV
hsv_color rgbToHsv(unsigned char r, unsigned char g, unsigned char b) 
{
    double rd = (double) r/255;
    double gd = (double) g/255;
    double bd = (double) b/255;
    double d_h;
   
    double max = max(rd, max(gd, bd)), min = min(rd, min(gd, bd));
    hsv_color hsv;
    hsv.v = max;
   
    double d = max - min;
    hsv.s = max == 0 ? 0 : d / max;
   
    if (max == min) { 
        d_h = 0;
    } else {
        if (max == rd) {
            d_h = (gd - bd) / d + (gd < bd ? 6 : 0);
        } else if (max == gd) {
            d_h = (bd - rd) / d + 2;
        } else if (max == bd) {
            d_h = (rd - gd) / d + 4;
        }
        d_h /= 6;
    }
    hsv.h = d_h * 360;
    return hsv;
}

Step 4: Wiring the Arduino Pro Mini and Programming

Now it's time to hook everything together and put it in the mint tin.

I used a male AMP connector that was sitting around to connect the light strip to a corresponding female connector sticking out of the mint tin.

For cutting holes in the tin, I used a nibbler tool from Radio Shack (man, I'm gonna miss that place!). Lots of hot melt glue to secure everything.

All the components are wired directly to the board. The 100-ohm current-limiting resistor for the speaker is soldered inline with the speaker wire and covered in heat shrink tubing to keep it from shorting anything. The power wires from the light strip are connected straight to the barrel connector rather than going through the Arduino.

One note to avoid blowing out the Pro Mini (I might be speaking from experience). When you use more than about 7 volts, you must connect your input voltage to the RAW pin, not the VCC pin. Yeah, that cost a few bucks. Good thing I buy Pro Mini's by the dozen.

At first, I had the IR receiver connected directly to the board on pin 9. I used 9 because it was close to the V and G wires that the IR receiver needed. I ended up removing it from the board and attaching it to a pigtail so the IR receiver could be placed outside the tin for better visibility of the remote controls.

Finally, it's time to program the Arduino. If you breadboarded with the Uno, make sure you change the board type to the Pro Mini. You'll probably also have to change the COM port.

Use an FTDI programmer to connect to the Pro Mini. This is required because the Pro Mini doesn't have an onboard programmer. The FTDI board is available from the usual suspects. I got mine from SparkFun. This will not be part of the final build and can be used again and again for other projects.

Step 5: Final Construction and Installation

I ended up taking the IR receiver out of the box and stubbing it out the top with a long, thin wire. This way, I could mount the box in a hidden place, but have the IR receiver "peek" out so as to catch the remote control signal more reliably.

I put a Velcro patch on the back of the tin and the corresponding patch on the headboard. The idea was to make a semi-permanent installation that would stay in place, but could be removed in case I needed to change the programming.

Finally, the second remote is attached near the door.

Step 6: Light in Action

Colors can be changed quickly with the Play button, or set to slowly walk around the color wheel with the Menu button.

Here's what the color wheel looks like, followed by the presets.

This was a fun little automation project that costs just a couple dollars. I've entered this into the automation contest, so if you like it, please vote!