LED strip controller on an ESP32-C3
Sixteen WS2812B pixels with on, off, brightness, color and two effects, faded between, from two buttons and a knob on the strip or from the Web tab. The whole build, an ESP32-C3 and 4 more parts, runs here in your browser on the firmware below; open it in the editor to change the wiring or the code and run it again.
Teaching? Assign it to your class in one click.
Intermediate Runs in your browser. Free, and no account needed.
The code
The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.
// Home Assistant LED strip: on, off, brightness, color and two effects, with a fade between them.
//
// WS2812B strip on GPIO 18 sixteen pixels, from the 5 V rail
// button on GPIO 9 press to turn the strip on or off
// button on GPIO 10 press to step through the effects
// knob on GPIO 0 brightness, read when you turn it
//
// This is the job ESPHome's light component does, written as a sketch. A light in
// Home Assistant has a state, a brightness, a color and an effect, and a good
// one fades between them instead of jumping. The buttons and the knob work on
// the strip itself; the Web tab drives it through /set, the way Home Assistant
// would through its integration.
//
// For a real strip: power the LEDs from their own 5 V supply sized for the strip,
// not from the board, inject power along long runs, share the ground with the
// ESP32, and put a level shifter on the data line, because 3.3 V data is below
// what many 5 V strips promise to read. A fuse near the supply is cheap insurance.
//
// Try /set?state=on&r=255&g=80&b=0&brightness=200 or /set?effect=rainbow in the Web tab.
#include <Adafruit_NeoPixel.h>
#include <WebServer.h>
#include <WiFi.h>
const int STRIP_PIN = 18;
const int POWER_BUTTON_PIN = 9;
const int EFFECT_BUTTON_PIN = 10;
const int KNOB_PIN = 0;
const int PIXELS = 16;
const unsigned long FADE_MS = 600;
enum Effect { SOLID, RAINBOW, CANDLE, EFFECTS };
const char *const EFFECT_NAMES[] = {"solid", "rainbow", "candle"};
Adafruit_NeoPixel strip(PIXELS, STRIP_PIN, NEO_GRB + NEO_KHZ800);
WebServer server(80);
bool on = true;
uint8_t red = 255, green = 160, blue = 60; // a warm white to start
int brightness = 128; // what was asked for, 0 to 255
float shown = 0; // what the strip shows now, fading toward it
Effect effect = SOLID;
int lastKnob = -1;
bool powerWas = false, effectWas = false;
unsigned long powerAt = 0, effectAt = 0;
unsigned long lastFrame = 0;
uint16_t hue = 0;
void publishState() {
Serial.print("publish home/strip/state {\"state\":\"");
Serial.print(on ? "ON" : "OFF");
Serial.print("\",\"brightness\":");
Serial.print(brightness);
Serial.print(",\"color\":{\"r\":");
Serial.print(red);
Serial.print(",\"g\":");
Serial.print(green);
Serial.print(",\"b\":");
Serial.print(blue);
Serial.print("},\"effect\":\"");
Serial.print(EFFECT_NAMES[effect]);
Serial.println("\"}");
}
uint32_t scaled(uint8_t r, uint8_t g, uint8_t b, int level) {
return strip.Color(r * level / 255, g * level / 255, b * level / 255);
}
void drawFrame() {
int level = (int)(shown + 0.5f);
for (int i = 0; i < PIXELS; i++) {
uint32_t c;
if (effect == RAINBOW) {
uint32_t wheel = strip.ColorHSV(hue + i * 65536L / PIXELS);
c = scaled((wheel >> 16) & 255, (wheel >> 8) & 255, wheel & 255, level);
} else if (effect == CANDLE) {
int flicker = 160 + (int)random(96); // each pixel wavers on its own
c = scaled(255, 120, 20, level * flicker / 255);
} else {
c = scaled(red, green, blue, level);
}
strip.setPixelColor(i, c);
}
strip.show();
}
void sendState() {
String json = "{\"state\":\"";
json += on ? "ON" : "OFF";
json += "\",\"brightness\":";
json += String(brightness);
json += ",\"r\":";
json += String(red);
json += ",\"g\":";
json += String(green);
json += ",\"b\":";
json += String(blue);
json += ",\"effect\":\"";
json += EFFECT_NAMES[effect];
json += "\"}";
server.send(200, "application/json", json);
}
void handleSet() {
if (server.hasArg("state")) on = server.arg("state") != "off";
if (server.hasArg("brightness")) brightness = constrain(server.arg("brightness").toInt(), 0, 255);
if (server.hasArg("r")) red = constrain(server.arg("r").toInt(), 0, 255);
if (server.hasArg("g")) green = constrain(server.arg("g").toInt(), 0, 255);
if (server.hasArg("b")) blue = constrain(server.arg("b").toInt(), 0, 255);
if (server.hasArg("effect")) {
String name = server.arg("effect");
for (int e = 0; e < EFFECTS; e++) {
if (name == EFFECT_NAMES[e]) effect = (Effect)e;
}
}
publishState();
sendState();
}
// True once per press: a level that changed and has not bounced in the last 30 ms.
bool pressedNow(int pin, bool &was, unsigned long &at) {
bool down = digitalRead(pin) == LOW;
unsigned long now = millis();
if (down == was || now - at < 30) return false;
was = down;
at = now;
return down;
}
void setup() {
Serial.begin(115200);
pinMode(POWER_BUTTON_PIN, INPUT_PULLUP);
pinMode(EFFECT_BUTTON_PIN, INPUT_PULLUP);
strip.begin();
strip.show();
Serial.println("LED strip: simulated WiFi, no MQTT broker in Mokxi.");
WiFi.begin("Mokxi-Lab", "breadboard");
while (WiFi.status() != WL_CONNECTED) {
delay(250);
}
Serial.print("online at http://");
Serial.print(WiFi.localIP());
Serial.println("/state, change it with /set");
server.on("/", sendState);
server.on("/state", sendState);
server.on("/set", handleSet);
server.begin();
publishState();
}
void loop() {
server.handleClient();
if (pressedNow(POWER_BUTTON_PIN, powerWas, powerAt)) {
on = !on;
publishState();
}
if (pressedNow(EFFECT_BUTTON_PIN, effectWas, effectAt)) {
effect = (Effect)((effect + 1) % EFFECTS);
publishState();
}
// The knob only takes over when it is turned, so the network's brightness sticks.
int knob = analogRead(KNOB_PIN) / 16; // 0 to 255
if (lastKnob < 0) {
lastKnob = knob;
} else if (abs(knob - lastKnob) > 4) {
lastKnob = knob;
brightness = knob;
publishState();
}
// About 50 frames a second, the shown level gliding toward the target over FADE_MS.
unsigned long now = millis();
if (now - lastFrame >= 20) {
lastFrame = now;
float target = on ? brightness : 0;
float step = 255.0f * 20 / FADE_MS;
if (shown < target) shown = (shown + step < target) ? shown + step : target;
else if (shown > target) shown = (shown - step > target) ? shown - step : target;
hue += 400;
drawFrame();
}
delay(2);
}
Parts list
6 parts, plus the jumper wires. Every one is in the editor's parts bin.
How it is wired
8 connections, pin by pin, read from the circuit itself. Each line is one set of pins joined together, by a jumper wire or a breadboard strip.
- Ground: ESP32-C3-DevKitM-1 pin GND; Potentiometer pin 1; Pushbutton (2) pin 2a; Pushbutton (2) pin 2b
- ESP32-C3-DevKitM-1 pin 3V3; Potentiometer pin 3
- ESP32-C3-DevKitM-1 pin 0; Potentiometer pin 2
- ESP32-C3-DevKitM-1 pin 10; Pushbutton (2) pin 1a; Pushbutton (2) pin 1b
- ESP32-C3-DevKitM-1 pin 5V; Addressable RGB LEDs pin VDD
- Ground: ESP32-C3-DevKitM-1 pin GND; Pushbutton (1) pin 2a; Pushbutton (1) pin 2b; Addressable RGB LEDs pin VSS
- ESP32-C3-DevKitM-1 pin 9; Pushbutton (1) pin 1a; Pushbutton (1) pin 1b
- ESP32-C3-DevKitM-1 pin 18; Addressable RGB LEDs pin DIN
Change it and keep it
Open it in the editor, change the circuit or the code, and keep your version in a free account.