Table of Contents

Smart NeoPixel LED Strip

Overview

General Idea

The idea for this project came as I wanted to add some backlight to my computer and desk so that it would be more cozy especially during the night. I wanted to be able to have more mods and have a finer control on the LEDs, so I chose a NeoPixels LED Strip, where I can control each LED of the Strip individually.

For the communication protocol between the ESP32 controller and my phone, I decided to use the Bluetooth LowEnergy protocol. I will use my phone as a remote control to send commands to the ESP32 controller and control the LED strip.

I used an OLED display as a way to better visualize the current status of the system, like if there is a device connected, if the lights are turned on or of, and what mode is currently selected.

Hardware Components Used

Software

Libraries needed

Architecture

Physical Architecture

Diagram

Real Hardware

Software Architecture

Setup de BLE Service

// BLE settings
#define SERVICE_UUID        "LED12345-1234-1234-1234-123456789LED"
#define CHARACTERISTIC_UUID "87654321-4321-4321-4321-LED987654321"
 
BLECharacteristic *pCharacteristic;
bool deviceConnected = false;
std::string receivedValue;
 
// BLE server callback
class MyServerCallbacks: public BLEServerCallbacks {
  void onConnect(BLEServer* pServer) {
    deviceConnected = true;
  };
 
  void onDisconnect(BLEServer* pServer) {
    deviceConnected = false;
  }
};
 
// BLE characteristic callback
class MyCallbacks: public BLECharacteristicCallbacks {
  void onWrite(BLECharacteristic *pCharacteristic) {
    receivedValue = pCharacteristic->getValue();
    if (receivedValue.length() > 0) {
      Serial.print("Received Value: ");
      Serial.println(receivedValue.c_str());
    }
  }
};
 
void setup() {
  Serial.begin(115200);
 
  // Initialize the NeoPixel library
  strip.begin();
  // Set all pixels to 'off'
  strip.show();
 
  // Initialize BLE
  BLEDevice::init("ESP32_LED_Control");
  BLEServer *pServer = BLEDevice::createServer();
  pServer->setCallbacks(new MyServerCallbacks());
 
  BLEService *pService = pServer->createService(SERVICE_UUID);
  pCharacteristic = pService->createCharacteristic(
                      CHARACTERISTIC_UUID,
                      BLECharacteristic::PROPERTY_READ |
                      BLECharacteristic::PROPERTY_WRITE
                    );
  pCharacteristic->setCallbacks(new MyCallbacks());
  pCharacteristic->setValue("Hello, World!");
  pService->start();
 
  BLEAdvertising *pAdvertising = pServer->getAdvertising();
  pAdvertising->addServiceUUID(SERVICE_UUID);
  pAdvertising->setScanResponse(true);
  pAdvertising->setMinPreferred(0x06);
  pAdvertising->setMinPreferred(0x12);
  BLEDevice::startAdvertising();
 
  Serial.println("Waiting for a client connection to notify...");
}

Get the commands from the phone via the created service

void loop() {
  // Check if a device was connected
  if (deviceConnected) {
    if (!receivedValue.empty()) {
      handleCommand(receivedValue);
      receivedValue = "";
    }
  }
}
 
// Function that handles the commands got from the Phone via BLE
void handleCommand(const std::string& command) {
  if (command == "off") {
    lightsOn = false;
    strip.clear();
    strip.show();
  } else {
    lightsOn = true;
    mode = command;
  }
}

Choose the lighting mode

void loop() {
  // Check if the lights are turned on or off
  if (lightsOn) {
    unsigned long currentMillis = millis();
    if (currentMillis - previousMillis >= interval) {
      previousMillis = currentMillis;
      updateLEDs();
    }
  }
}
 
// Function that handles the commands got from the Phone via BLE
void handleCommand(const std::string& command) {
  if (command == "off") {
    lightsOn = false;
    strip.clear();
    strip.show();
  } else {
    lightsOn = true;
    mode = command;
    if (mode == "red") {
      setAllPixels(strip.Color(255, 0, 0));
    } else if (mode == "blue") {
      setAllPixels(strip.Color(0, 0, 255));
    } else if (mode == "yellow") {
      setAllPixels(strip.Color(255, 255, 0));
    } else if (mode == "green") {
      setAllPixels(strip.Color(0, 255, 0));
    } else if (mode == "purple") {
      setAllPixels(strip.Color(128, 0, 128));
    } else if (mode == "orange") {
      setAllPixels(strip.Color(255, 100, 0)); // Adjusted towards red
    } else if (mode == "cyan") {
      setAllPixels(strip.Color(0, 255, 255));
    } else if (command.find("-") != std::string::npos) {
      mode = "split";
      std::string color1 = command.substr(0, command.find('-'));
      std::string color2 = command.substr(command.find('-') + 1);
      setSplitPixels(getColor(color1), getColor(color2));
    }
    strip.show();
  }
}
 
// Set all the pixels to one color
void setAllPixels(uint32_t color) {
  for (int i = 0; i < NUM_LEDS; i++) {
    strip.setPixelColor(i, color);
  }
}
 
// Set half the pixels to one color and the otehr half to the other
void setSplitPixels(uint32_t color1, uint32_t color2) {
  for (int i = 0; i < NUM_LEDS / 2; i++) {
    strip.setPixelColor(i, color1);
  }
  for (int i = NUM_LEDS / 2; i < NUM_LEDS; i++) {
    strip.setPixelColor(i, color2);
  }
  strip.show();
}
 
uint32_t getColor(const std::string& color) {
  if (color == "red") return strip.Color(255, 0, 0);
  if (color == "blue") return strip.Color(0, 0, 255);
  if (color == "yellow") return strip.Color(255, 255, 0);
  if (color == "green") return strip.Color(0, 255, 0);
  if (color == "purple") return strip.Color(128, 0, 128);
  if (color == "orange") return strip.Color(242, 133, 0);
  if (color == "cyan") return strip.Color(0, 255, 255);
  return strip.Color(0, 0, 0); // Default to black if color not recognized
}
 
// Run the selected mode
void updateLEDs() {
  if (mode == "cycle") {
    cycleColors();
  } else if (mode == "rainbow") {
    rainbowCycle();
  } else if (mode == "snake") {
    snakeMode();
  } else if (mode == "random_blink") {
    randomBlink();
  }
}
 
// Function to cycle through colors
void cycleColors() {
  static int currentColor = 0;
  uint32_t colors[] = {
    strip.Color(255, 0, 0),    // Red
    strip.Color(0, 0, 255),    // Blue
    strip.Color(255, 255, 0),  // Yellow
    strip.Color(0, 255, 0),    // Green
    strip.Color(128, 0, 128),  // Purple
    strip.Color(242, 132, 0),  // Orange
    strip.Color(0, 255, 255)   // Cyan
  };
  setAllPixels(colors[currentColor]);
  currentColor = (currentColor + 1) % 7;
  strip.show();
}
 
// Function that lights the LED strip in an evolving rainbow
void rainbowCycle() {
  static uint16_t j = 0;
  for (int i = 0; i < strip.numPixels(); i++) {
    strip.setPixelColor(i, Wheel((i + j) & 255));
  }
  strip.show();
  j++;
  if (j >= 256) j = 0;
}
 
// Auxilary function that helps the rainbow function
uint32_t Wheel(byte WheelPos) {
  WheelPos = 255 - WheelPos;
  if (WheelPos < 85) {
    return strip.Color(255 - WheelPos * 3, 0, WheelPos * 3);
  }
  if (WheelPos < 170) {
    WheelPos -= 85;
    return strip.Color(0, WheelPos * 3, 255 - WheelPos * 3);
  }
  WheelPos -= 170;
  return strip.Color(WheelPos * 3, 255 - WheelPos * 3, 0);
}
 
// Build a snake that slitters through the light strip
void snakeMode() {
  static int head = 0;
  strip.clear();
  for (int i = 0; i < 10; i++) {
    int pos = (head - i + NUM_LEDS) % NUM_LEDS;
    strip.setPixelColor(pos, strip.Color(0, 255 - i * 25, 0)); // Body is darker green
  }
  strip.show();
  head = (head + 1) % NUM_LEDS;
}
 
// Function that makes the light strip to blink random LEDs with random colors
void randomBlink() {
  strip.clear();
  for (int i = 0; i < NUM_LEDS; i++) {
    if (random(10) < 2) {
      strip.setPixelColor(i, strip.Color(random(256), random(256), random(256)));
    }
  }
  strip.show();
}

Display setup and printing

// OLED display settings
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 32
#define OLED_RESET -1
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
 
void setup() {
  // Initialize the OLED display
  // Address 0x3C for 128x32 OLED display
  if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
    Serial.println(F("SSD1306 allocation failed"));
    for(;;);
  }
  display.display();
  delay(2000); // Pause for 2 seconds
  display.clearDisplay();
}
 
void loop() {
  // Update the display with the current status
  updateDisplay();
}
 
// Update the OLED display with the current status
void updateDisplay() {
  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(SSD1306_WHITE);
 
  // Show if any device is connected
  display.setCursor(0, 0);
  display.print("Device: ");
  display.print(deviceConnected ? "Connected" : "Disconnected");
 
  // Show if the lights are on or off
  display.setCursor(0, 10);
  display.print("Lights: ");
  display.print(lightsOn ? "On" : "Off");
 
  // Show the current lighting mode
  display.setCursor(0, 20);
  display.print("Mode: ");
  display.print(mode.c_str());
 
  display.display();
}

How it works

Main service page

Command for a static color

Command for splitting the strip in 2 colors

Command for cycling through colors

Command for having a rainbow show

Command for having a snake-like moving

Command for lighting-up random LEDs with random colors

Challenges

Initially, I wanted to use both WiFi and BLE to control the LED Strip, but I had to limit myself to just one of them, so I chose BLE. The issue was that the WiFi library was just too big in size, taking over 59% of the text memory on my development board, so I dropped it.

By dropping the WiFi features, I had to drop the automatic time triggering as I could not set the time. To set the time to the Local Time, i would have to use a NTP server, but without the WiFi library, I could not connect to one. One other option was to pass the time as a parameter when the code is flashed on the board, but this is not optimal. Other option is to pass it as a command through my phone, but this just destroys the purpose of it being automatic.

Lastly, I found to my surprise that the ESP32 development board I have bought has not a LTR308 module on it, so I was unable to set an automatic detection of light in the room and trigger the light when needed.

Next Steps

First of all, buy a better development board, one that has at leas a LTR308 module on it. After that, I will implement the automatic light detection function and have the lights start automatically when it becomes dark in the room. I also have to actually fix the connection that I screwed up and had to glue together. Lastly, I have to find a way to mount this thing.

Resources