How to Build a Gesture-Controlled Mood Lamp with ESP32
Wave to cycle colors, hold to dim, and save your favorite lighting scenes
Updated

What you'll build
Build a gesture-controlled mood lamp using an ESP32, an APDS-9960 Gesture Sensor, and a WS2812B LED Strip. Swipe left or right to shift the colour, swipe up or down to adjust brightness. The current colour and brightness save automatically to flash five seconds after your last gesture, so the lamp restores its state after a power cycle.
The APDS-9960 combines gesture detection, proximity sensing, and ambient light measurement in a single I2C breakout. The ESP32 reads gesture interrupts in the main loop and passes the result to FastLED. There is no app, no Wi-Fi setup, and no buttons — the lamp responds only to hand movements held 5–10 cm above the sensor.
The firmware has four jobs: initialise the APDS-9960 over I2C and enable gesture detection; translate left/right gestures to hue changes and up/down gestures to brightness changes; drive the WS2812B LED Strip with FastLED using the current hue and brightness; and save colour and brightness to the ESP32's non-volatile Preferences storage after five seconds of inactivity.
Upload and calibrate
Flash the sketch from Schematik and open Serial Monitor at 115200 baud. On boot you should see Mood Lamp ready. The LEDs light at the last saved colour and brightness, or a default colour on first boot.
Swipe left or right horizontally over the sensor to shift the hue. Swipe up or down to adjust brightness. Each swipe shifts hue by 24 steps on a 256-step wheel, and brightness by 15 steps on a 255-step scale. After five seconds of no gesture the current state saves to Preferences. Cycle the power and confirm the lamp restores the same colour.
The default strip length is 16 LEDs. If your strip is longer or shorter, update NUM_LEDS in the firmware and re-flash.
Troubleshooting
- APDS-9960 init fails on boot (
APDS-9960 init failedin Serial). Confirm SDA is GPIO 21 and SCL is GPIO 22, and that the sensor VCC is on 3.3 V. The default I2C address for APDS-9960 is 0x39 — some breakout versions differ, though this is uncommon. - Gestures not detected or misfiring. Hold your hand 5–10 cm above the sensor. Bright direct light from above can swamp the photodiodes; shade the sensor or reduce ambient light. Confirm INT is connected to GPIO 27 with
INPUT_PULLUP. - LEDs flicker or show wrong colours. Check that GND is shared between the ESP32 and the 5 V supply. A missing common ground is the most common cause of WS2812B data errors. Also verify the 330 Ω series resistor is on the DIN line.
- First LED burns out. Power-on inrush without the 470 µF capacitor can damage the first LED in a strip. Replace the LED and add the capacitor before powering up again.
- State does not save. Preferences writes happen five seconds after the last gesture. If you power off immediately after a gesture, the new colour has not saved yet. Wait for the five-second window before cycling power.
Going further
With gesture control and persistent state in place, a straightforward next step is adding Wi-Fi so you can also set colour from a browser. The Voice-Controlled Robot Face shows the pattern for hosting a small web page on the ESP32 that accepts GET requests to change state. For a multi-lamp setup, each ESP32 could subscribe to an MQTT topic and change colour together, turning the gesture on one lamp into a group scene change across a room.
Wiring diagram
Components needed
| Component | Type | Qty | Buy |
|---|---|---|---|
| SparkFun RGB and Gesture Sensor - APDS-9960 | sensor | 1 | $7.11 |
| SMD LED - RGB WS2812B (Strip of 50) | actuator | 1 | $25.95 |
Supplier links, prices, and availability are shown as a guide and may change. Schematik may earn a commission from purchases made through affiliate links.
Assembly
Connect the gesture sensor
Wire APDS-9960 VCC to 3.3V, GND to ground, SDA to GPIO21, SCL to GPIO22, and INT to GPIO27.
- Use 3.3V for the sensor — most APDS-9960 breakouts have an onboard regulator.
Wire the LED strip
Connect WS2812B 5V and GND to a stable 5V power source, and DIN to GPIO4. Share ground with the ESP32.
- Add a 330 Ω resistor on the DIN line and a 470 µF capacitor across LED power for stability.
- Power the LED strip from a separate 5V source when using more than 8 LEDs.
Upload and test gestures
Flash the sketch, open Serial Monitor at 115200 baud. Swipe left/right to change color, up/down to adjust brightness. Settings save automatically after 5 seconds of inactivity.
- Hold your hand 5–10 cm above the APDS-9960 for reliable gesture detection.
Pin assignments
| Pin | Connection | Type |
|---|---|---|
| 3V3 | apds9960-1 VCC | POWER |
| GND | apds9960-1 GND | GROUND |
| GPIO 21 | apds9960-1 SDA | I2C |
| GPIO 22 | apds9960-1 SCL | I2C |
| GPIO 27 | apds9960-1 INT | DIGITAL |
| 5V | ws2812b-1 5V | POWER |
| GND | ws2812b-1 GND | GROUND |
| GPIO 4 | ws2812b-1 DIN | DATA |
Code
#include <Wire.h>
#include <Adafruit_APDS9960.h>
#include <FastLED.h>
#include <Preferences.h>
#define APDS_INT_PIN 27
#define LED_PIN 4
#define NUM_LEDS 16
#define SDA_PIN 21
#define SCL_PIN 22
Adafruit_APDS9960 apds;
CRGB leds[NUM_LEDS];
Preferences prefs;
uint8_t hue = 0;
uint8_t brightness = 140;
uint8_t savedHue = 0;
uint8_t savedBrightness = 140;
unsigned long lastGestureMs = 0;
void setup() {
Serial.begin(115200);
delay(100);
prefs.begin("moodlamp", false);
hue = prefs.getUChar("hue", 0);
brightness = prefs.getUChar("bright", 140);
savedHue = hue;
savedBrightness = brightness;
Wire.begin(SDA_PIN, SCL_PIN);
pinMode(APDS_INT_PIN, INPUT_PULLUP);
FastLED.addLeds<NEOPIXEL, LED_PIN>(leds, NUM_LEDS);
FastLED.setBrightness(brightness);
if (!apds.begin()) {
Serial.println("APDS-9960 init failed");
}
apds.enableProximity(true);
apds.enableGesture(true);
Serial.println("Mood Lamp ready");
}
void loop() {
if (apds.gestureValid()) {
int g = apds.readGesture();
if (g == APDS9960_LEFT) hue += 24;
if (g == APDS9960_RIGHT) hue -= 24;
if (g == APDS9960_UP && brightness < 245) brightness += 15;
if (g == APDS9960_DOWN && brightness > 20) brightness -= 15;
FastLED.setBrightness(brightness);
lastGestureMs = millis();
}
if (millis() - lastGestureMs > 5000 && lastGestureMs > 0) {
if (hue != savedHue || brightness != savedBrightness) {
prefs.putUChar("hue", hue);
prefs.putUChar("bright", brightness);
savedHue = hue;
savedBrightness = brightness;
}
lastGestureMs = 0;
}
fill_rainbow(leds, NUM_LEDS, hue, 4);
FastLED.show();
delay(20);
}
// Run this and build other cool things at schematik.ioReady to build this?
Open this project in Schematik to get the full wiring diagram, pin assignments, and deployable code for the Gesture-Controlled Mood Lamp.
Open in Schematik →