Schematik build
How to Build a Gesture-Controlled Mood Lamp with ESP32
Wave to cycle colors, hold to dim, and save your favorite lighting scenes
Schematik
Published April 9, 2026 · Updated June 12, 2026

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
Interactive · read-onlyPan and zoom to explore the wiring. Remix the project to edit it in your own workspace.
Parts list
Bill of materials| Component | Qty | Notes |
|---|---|---|
| APDS-9960 Gesture Sensor | 1 | Gesture sensor for swipe/hold interaction. |
| WS2812B LED Strip | 1 | Addressable LEDs for animated mood scenes. |
Assembly
3 stepsConnect the gesture sensor
Wire APDS-9960 VCC to 3.3V, GND to ground, SDA to GPIO21, SCL to GPIO22, and INT to GPIO27.
- Tip: 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.
- Tip: 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.
- Tip: Hold your hand 5–10 cm above the APDS-9960 for reliable gesture detection.
Pin assignments
Board wiring reference| 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 |
Firmware
ESP32#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);
}“Deploy to device” opens this project in Schematik, where you can flash it to your board over USB.
Remix this project
Make it yours in one click
Open a full copy of this project in your own Schematik workspace — diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.



