Introduction: Indoor/Outdoor Expandable Wifi Light Strip (Arduino/NodeMCU)

About: My favorite bumper sticker of all time: Legalize Updoc.

Preamble

After tinkering for a while, entertaining my kids and myself with LEDs and Arduino coding, I decided put a collection of those pieces into one fun project. This is the latest iteration in a project that has its roots in a simple mood light created more than three years ago (I still use it). Over time that simple project grew into lamps and lighting, effects and signage.

This Instructable combines an Arduino Nano and a NodeMCU that interact through a combination of hardware interrupt and serial communication to control 300 WS2812B (NeoPixel) LEDs through a WiFi server. Oh, and it's made for indoor or outdoor use. Boom! Go put special effects in your flower bed.

Assumptions

That the reader has basic electrical skills is assumed. Electricity kills.

That the reader has the ability to use a power drill and a Dremel is assumed. Be safe with that stuff.

This project is not going to teach basic...um....anything. I'm going to assume you know how to drill holes and cut little things with a Dremel. I'll also assume you understand the basics of electrical wiring and simple electronics. The last assumption I'll make is in your understanding of the Arduino IDE. That said, I will provide as comprehensive a description of how everything works as I can.

Systems

This project can be divided into three distinct systems: power distribution, logic, and lighting. As such it is designed to be installed, configured, and broken down along those lines.

Power distribution is comprised of a 110VAC/5V10ADC power supply, switch, fuse, and terminal block. These items may stand alone for other projects.

Logic is the NodeMCU, Nano, and the bits that make them work together. The entire logic unit is removable using one quick-connect, making it simple to update code or replace the entire unit.

Lighting is just the lights. Nothing else. All lights are removable using waterproof connectors.

Software

This where evolution really led the way. I've done too many personal lighting projects and seen enough Instructables to know whether I have something worthy of posting. The key to this project is in how the Nano and the NodeMCU communicate and how they manage both the lights and WiFi server without lag.

Conveniently, both processors are coded using Arduino IDE. Inconveniently, you have to code for two processors. It's a drag but it makes everything so pretty and elegant in end that you'll understand.

Let's look at some parts...

Step 1: Parts List

Logic

(a) 1 - NodeMCU 1.0 (ESP8266 12e)

(b) 1 - Arduino Nano R3

(c) 1 - 2x3" prototyping/perf board

(d) 1 - I2C SPI OLED 128x64 display module

(e) 1 - 1000uf capacitor

(f) 1 - 2N3904 NPN transistor

(g) 1 - 470 ohm resistor

(h) 2 - 4k7 ohm resistors

(i) 3 - 1k ohm resistors

Power

(j) 4 - 3/8" rubber grommets

(k) 1 - fuse holder with fuse

(l) 1 - 110/220VAC Power switch

(m) 10 - 2 Pin Way 20-16 AWG Waterproof Connectors

(n) 1 - Terminal block

(o) 1 - Plastic cable gland joint

Box and lighting

1 - ABS Waterproof Box

1 - 3-prong indoor/outdoor power cord

1 - 5V 10A (50W) DC power supply

1 - 300 WS8212B LED strip

Tools

Wire

Solder

Wire stripper/cutter

Soldering iron

Power drill and bits

Dremel

The parts list is pretty straightforward, and it really doesn't matter whether you assemble your power unit, logic unit or lighting first. Since everything is designed to be modular, you can pick your poison.

Step 2: Getting the Box Ready for Power

The largest component to reside within my ABS box, which has a screw top and rubber gasket in the lid, is my DC power supply. At 6x4" it fits nicely inside my 8.5x5.5" box.

The next item to take into consideration is how to get power into and out of the box. On the AC side, I opted for a switch, fuse holder, and wire joint. I always use these; I have a handful of each sitting around. I used a power drill for the incoming AC wire/joint and the fuse holder. I used a Dremel to cut the rectangle for the switch.

I hate crimping wires, so I soldered the switch/fuse/incoming AC wire workings. They're still easily removable and are the only components within the box that are not removable by screw or quick-release. I don't mind, because really, how often is a fuse holder going to fail?

After cutting the opening for the AC side, I drilled four 9/32" hole for all the outgoing DC wiring: the power for each lighting strip and the signal wire(s). I certainly could have drilled more holes, but at the time my selection of grommets was limited.

Once all the box openings were created, sized properly, and cleaned up, all the AC components were fitted and soldered in place, leaving three leads for the input to the DC power supply. The rubber grommets were inserted.

The AC leads are connected to the power supply. The switch is tested. And power is good!

For power out, I decided to go with a terminal block. DC power will be fed to two systems: lights and logic. I'm going to feed the lights from the terminal block. All but one terminal will be used for lighting power, the last will be isolated for the signal line coming from the Nano. (Pictured, lower right: power for the LEDs in red and black wires, signal line at right edge of terminal block in yellow.) So, pretty simple: DC out from the power supply into the terminal block, DC from the terminal block to the lights. We'll come back to the signal line in more detail.

So now we have a box with AC going in and up to 50W of DC coming out.

Step 3: Preparing the Lights

The lighting option being used in this Instructable is a 300-pixel strand of WS2812B LEDs, which really is nothing more than five connected one-meter strands of 60 LEDs each, encased in a silicone waterproof covering.

The problem with trying to power 300 of these LEDs in one long chain, is that the cumulative voltage drop will make using anything other than primary colors at the end of the chain unusable and anything beyond about 80 LEDs questionable in color consistency, so I decided to chop my five-meter strand into five one-meter segments, each consisting of 60 LEDs. These strands have solder joints every meter, so using a razor knife I cut the strands right through the middle of the solder joints, trying to leave solder on each segment. That didn't always work to plan.

These LED strands are clearly marked with the direction of power flow*. The input side is where the new quick-connect leads will be soldered. Piece of cake. Just make sure your leads are positive to positive and are tinned. There is limited solder to work with on the LED strip, so give yourself every advantage.

For each quick-connect lead that is soldered to an LED strand, the opposite half of the lead must be connected through the grommets to the terminal block. Positive to positive, negative to negative. I have five strands, so I have five DC power leads leads that extend from my box.

Once all five segments have had their power leads attached, the signal line must be attached. *THE SIGNAL LINE CANNOT BE ATTACHED THE SAME WAY THE POWER LEADS ARE ATTACHED, I.E. IT MUST RUN TAIL-TO-HEAD FROM FIRST SEGMENT TO LAST IN A ZIGZAG PATTERN.

To attach the LED signal line properly, attach a wire (22 gauge is fine, unless your LEDs really should be 12v) from the terminal block to beginning of the first LED segment (see "Preparing for power" step regarding attaching signal line to terminal block), then attach a wire from the end of the first segment to the beginning of the second LED segment, then attach a wire from the end of the second segment to the beginning of the third LED segment, etc. until you have connect the last line to the beginning of the last segment in your run. The signal wire will form a zigzag pattern. (If your signal wires are cut long enough, you can run them behind the LED strips inside the waterproof covering.)

If you want to avoid the zigzag signal line, you can reverse the direction of every other LED segment and run power rails at each end of your segments, but this will severely impair any sort of mobility you may want. This is the ideal arrangement for a large matrix of LEDs being used for signage, as opposed to strips being used for effect.

Also regarding your signal line, if you start losing signal at the end of long runs of LEDs (or long signal wires), you can always add a second signal line, using another digital pin on your Arduino, but you'll have to code to accommodate.

Step 4: Preparing the Logic Unit

I'm sure the first question to come to mind is why use two logic control units, rather than just using the NodeMCU standalone? The first answer is that this is proof-of-concept for a much larger use case. In that scenario, the ESP8266 just doesn't have the speed to manage its WiFi chores and handle the rest of its duties.

The full story is one of taking one step at a time, analyzing each piece in light of the bigger picture and moving on. I will not go into how many combinations of hardware and approaches to software I tested until finally I thought to scrap my original approach of going so cheap I could do what I wanted on an ESP8266-01. So to make a long story boring, I wound up increasing my LCU budget from $3 to $20. In flat dollars, not bad. Percentage-wise, lousy.

So here's the deal.

The NodeMCU is tasked with the following:

  • running a small web server;
  • toggling a pin (that is connected to the Nano) upon receipt of an HTTP request to serve a known page (see "Uploading The Sketches" step;
  • sending a serial notification to the Nano;
  • optionally updating a small I2C OLED display (too cool to not be standard).

The Nano handles the following:

  • handling a hardware interrupt triggered by the NodeMCU;
  • monitoring serial input associated with a hardware interrupt;
  • executing LED effect code associated with serial input.

A description of the wiring

The idea behind this exercise is to take any load off the NodeMCU that can be removed and provide the end user a smooth experience with anticipated results. I have a similar system built with only a NodeMCU, but trying to manage the timing of the lighting effects without degrading - if not outright killing - WiFi performance became a challenge. The main challenge was in using NodeMCU in the Arduino environment; so far I have not found a good asynchronous WiFi server. I know the facility exists in LUA, but at the time I had the idea split the processing, I realized I could expand the idea well beyond my original expectations.

When I started this project I intended to plug a prototype board with a NodeMCU on it directly into an Arduino Duo. Alas, my last Duo died, so I went with a Nano. I like the Nano. Pretty much the same as a Duo with a USB mini port. So now I have a NodeMCU soldered to a Duo prototype board? What to do? Just start soldering wires directly to the header-less Nano. Total meatball job, but it's a prototype, and honestly it would be a fairly simple PCB to etch and make a super slick compact package.

The wiring between the two units is fairly straightforward...until the realization that the Nano runs on 5V, and while NodeMCU will run on a VCC of 5V, its digital I/O is strictly 3.3V.

How we want to connect them:

NodeMCU <--------> Arduino Uno/Nano

TX ------------------------> D4

RX <----------------------- D5

D0 ------------------------> D3

Unfortunately what we want does not mesh with reality. The general pattern is the same - in fact, the connection between the NodeMCU TX pin and the Nano D4 pin can be made with no modification - but the connection between the NodeMCU RX pin and the Nano D5 must be made using a voltage divider, and the connection between the NodeMCU D0 pin and the Nano D3 pin must be made using a simple logic level shifter (in other words a transistor that is rigged to allow 5v to pass to Nano D3 when NodeMCU D0 GOES LOW.

How they are connected is detailed in the schematic.

The LED strip(s) must be controlled from a PWM pin on the Nano. The Nano and Uno both have six PWM pins: these are digital pins 3, 5, 6, 9, 10, and 11. Pins D3 and D5 are already in use, so I have four PWM pins to choose from. I chose to connect pin 10 from the Nano to the input side of the terminal block using a 470 ohm resistor to protect the first LED on the signal line. I have a three-pin quick-connect between my logic assembly and the box that provides 5V, ground and the signal line connection from the logic unit to the terminal block. This makes code updates really quick, and it allows interchange with another circuit (such as an ESP8266-12E based system or a simple Arduino-only circuit). Three wires and you're the boss.

The rest of the wiring is what you'd expect. Both boards take 5V VCC from a common source when connected to the main power. The NodeMCU also needs 5V on the EN pin. Both boards are grounded to a common ground. It's important to note that you probably shouldn't plug either unit into your computer USB without disconnecting it from the main power when the power is off. The current draw may damage your computer. If you want to safeguard against this, add a diode to the 5V IN of your logic unit to prevent voltage flowing from it to your power supply and out to your LED strips.

A little display for debugging

Not shown in the schematic diagram is the I2C OLED display wired to the NodeMCU. It's used mainly for debugging, but it has a valuable purpose on WiFi networks using DHCP in that it displays the address of the web server. The only other way to know the address you're serving is to use a USB connection to the NodeMCU to get the info at time of boot. If you're on a network that allows static addresses, the IP information is irrelevant, but the display is still very cool at around $8. Connecting it is a breeze. Four wires. In addition to power and ground, make these connections:

NodeMCU <-----> I2C display

D1 ------------- SCL

D2 ------------- SDA

Step 5: Upload the Sketches

Two units, two sketches. See the files attached.

NodeMCU

Sketch name:

instructable_nodemcu_web_server.ino

Libraries required:

SPI

Wire

Adafruit_GFX

Adafruit_SSD1306

ESP8266WiFi

WiFiClient

ESP8266WebServer

ESP8266mDNS

Highlights:

void triggerInterrupt(String mode) {
// triggered from each http handler
  // when we get a request to serve http response, make sure to toggle the interrupt pin
  // interrupt pin is initialized high to hold arduino pin low
  // but here we toggle twice with a brief delay between each change
  // the Arduino is loking for the RISING edge
  // the process ends when the HTMLout() function sets interruptPin HIGH
  char* modeprompt = "Current mode: ";

// toggle interruot pin to signal the Arduino to stop what it's doing
// a new serial request is imminent
  digitalWrite(interruptPin, LOW);
  delay(10);
  digitalWrite(interruptPin, HIGH);
  delay(10);
  digitalWrite(interruptPin, LOW);  

// refresh the ip address
  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(WHITE);
  display.setCursor(0,0);
  display.println(WiFi.localIP());

// display the mode requested
  display.setCursor(0,8);
  display.print(modeprompt);
  display.setCursor(0,17);
  display.print(mode);
  display.display();
}
void setup () {
// set up some display messages
  char* trying = "Connecting...";
  char* gotip = "Connected.";
  
//set up serial
  Serial.begin (4800);

//set up interrupt pin to output; set it high
  pinMode(interruptPin, OUTPUT);
  digitalWrite(interruptPin, HIGH);  // start with pin high to hold Nano interrupt pin low

// initialize i2c display
  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);  // initialize with the I2C addr 0x3C

// say hello on display
  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(WHITE);
  display.setCursor(0,0);
  display.println(trying);
  display.display();    

// try to connect to wifi
  WiFi.begin ( ssid, password );
  while ( WiFi.status() != WL_CONNECTED ) {
      // keep a minimal delay here to avoid crash at startup
    delay(1);
  }

// display connect message
  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(WHITE);
  display.setCursor(0,0);
  display.println(gotip);
  display.display();

// set up http handlers
  server.on("/",HTMLout);
  server.on("/red",red);
  server.on("/green",green);
  server.on("/blue",blue);
  server.on("/cyan",cyan);
  server.on("/yellow",yellow);
  server.on("/magenta",magenta);
  server.on("/purple",purple);
  server.on("/orange",orange);
  server.on("/cycle",rainbow);
  server.on("/rwipe",rwipe);
  server.on("/tchase",tchase);
  server.on("/tchaser",tchaser);
  server.on("/worain",worain);
  server.on("/pulse",pulse);
  server.on("/rfade",rfade);
  server.on("/fwhite",fwhite);
  server.on("/off",lightoff);
  server.onNotFound(HTMLout);

// start wifi server
  server.begin();

// some serial debug messages if preferred
//  Serial.println(".server started");
//  Serial.print("Use this URL to connect: ");
//  Serial.print("http://");
//  Serial.print(WiFi.localIP());
//  Serial.println("/");

// display IP address of web server once connected
  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(WHITE);
  display.setCursor(0,0);
  display.println(WiFi.localIP());
  display.display();
}
void loop () {
// look for http requests and direct program flow accordingly
  server.handleClient();
}

Aduino Nano:

Sketch name:

instructable_serial_strip_2812b.ino

Libraries required:

SoftwareSerial
Adafruit_NeoPixel

Highlights:

void pardon() {
// function called whenever interrupt pin is seen rising
// if current function is a long runner, set stop action bit
  if (longRunner)
    stopCurrentAction=true;
}

void setup() {
// initialize interrupt pin for input
  pinMode(interruptPin, INPUT_PULLUP);
  // look for interrupt pin's rising edge, indicating a signal from the NodeMCU
  // on rising, run the pardon() function
  attachInterrupt(digitalPinToInterrupt(interruptPin), pardon, RISING);
  // intitialize serial for debugging
  Serial.begin(115200);
  // initialize second serial for communication with Nano
  mySerial.begin(4800);
  // set up strip pin for output and initialize strip
  numP = strip.numPixels();
  pinMode(PIN,OUTPUT);
  strip.begin();
  // set strip to all green if everything's ok to this point
  colorSet(strip.Color(0, 64, 0)); // Green
  strip.show();
  // send a debug message
  Serial.println("Ready to receive");
}
void loop() {
  color = "";
  if (mySerial.available()) {
// if we get some serial input, parse it
    color = mySerial.readString();
    Serial.print(color);
    for (int x=0; x<color.length(); x++)="" {
      if (color[x] == '/') {
        color[x+1] = '\0';
        break;
      } 
    }
// check the request and run the right routine
    if (color == "rainbowc/") rainbowCycleMS(0);
      else if (color == "tchase/") theaterChase(strip.Color(0,127,0),200);
        else if (color == "tchaser/") theaterChaseRainbow(200);
          else if (color == "red/") colorWipeMS(strip.Color(127,0,0),10,false);
            else if (color == "green/") colorWipeMS(strip.Color(0,64,0),10,false);
              else if (color == "blue/") colorWipeMS(strip.Color(0,0,127),10,false);
                else if (color == "cyan/") colorWipeMS(strip.Color(0,64,127),10,false);
                  else if (color == "yellow/") colorWipeMS(strip.Color(96,48,0),10,false);
                    else if (color == "magenta/") colorWipeMS(strip.Color(64,0,64),10,false);
                      else if (color == "purple/") colorWipeMS(strip.Color(48,0,96),10,false);
                        else if (color == "orange/") colorWipeMS(strip.Color(127,32,0),10,false);
                          else if (color == "worain/") whiteOverRainbow(10,10,2);
                            else if (color == "rwipe/") rainbowWipe(20);
                              else if (color == "pulse/") pulseWhite(100);
                                else if (color == "rfade/") rainbowFade2White(20,3,3);
                                  else if (color == "fwhite/") colorWipeMS(strip.Color(50,40,64),10,false);
                                    else if (color == "lightoff/") colorSet(0);
  }
  //this is a little sparkle routine
  randomSparkle(7500);
  delay(100);
}
void resetPin() {
  // resets long running bit and stop action bit
  // must be called at the end of any long-running animation
  stopCurrentAction = false;
  longRunner = false;
}

void rainbowCycleMS(uint8_t wait) {
// Example of long running routine that is broken by a hardware interrupt
  uint16_t i, j;
  uint16_t numstrands = NUM_STRANDS;
// this is a long running function  
  longRunner = true;
  for(;;) {
// check for stop action bit to be set at the top of every loop
    if (stopCurrentAction) break;
    for(j=0; j<256*5; j++) { // 5 cycles of all colors on wheel
// check for stop action bit to be set at the top of every loop      
    if (stopCurrentAction) break;
      for (uint16_t y=0;y<=ledsPerStrand;y++) {
// check for stop action bit to be set at the top of every loop
        if (stopCurrentAction) break;
        for (uint16_t x=0;x<numstrands;x++) {
	  if (stopCurrentAction) break;
            i = x*ledsPerStrand+y;
            if (i < 256)
              strip.setPixelColor(i, Wheel(((i * 256/ledsPerStrand) + j) & 255));
            else
              strip.setPixelColor(i, Wheel((((i-60) * 256/ledsPerStrand) + j) & 255));
        }
      }
      strip.show();
      delay(wait);
    }
  }
// call resetPin() to make sure stop action and long running bits are cleared
  resetPin();
}

Step 6: Putting It All Together

When everything is wired up and started using the supplied code, the Arduino should initialize the LED strip to solid green, and the NodeMCU should connect to the WiFi network specified in its sketch.

Accessing the IP address the web server is hosting with a browser should result in a simple web page displaying all modes available (see image). Clicking one of the buttons should (making an HTTP request) should:

  • force NodeMCU to toggle the interrupt pin connected to the Nano, which in turn forces the Nano to interrupt any running process and return to monitoring for serial input,
  • and cause the NodeMCU to send a serial request to the Nano, which starts the new animation to begin.

So, clicking "rainbow cycle" from the web page should trigger a display of "rainbowc" on the I2C display as the mode (see image), and the rainbow cycle animation will begin on the LED strip (see video). Given that the "rainbowCycleMS" function will run forever without some interruption (the function begins with a "for (;;)" loop), a good test would be to start rainbow cycle, then hit stop to make sure the interrupt function works.

Of course using a display or connecting a USB cable to either controller will allow for snappy debugging.

Overall the entire system is comprised of several smaller systems that can be broken down into components that make for easy debugging. If everything is wired correctly, the software should run without a hitch.

If you decide to build something like this yourself, keep it modular. The power supply and LED strips can be upgraded to 12V. Of course more LEDs can be added. And the entire "logic unit" can be reworked. Enjoy.