LED Ticker Build Log · 04
[IoT DIY Part 4] Designing an Interactive NeoPixel Mood Lamp with Multi-Sensor Fusion and Randomized Algorithms
![[IoT DIY Part 4] Designing an Interactive NeoPixel Mood Lamp with Multi-Sensor Fusion and Randomized Algorithms](/iot-lab/assets/led-ticker/hero-en-4.jpg)
The side-light project that began as a way to fill the empty left side of the vertical market ticker eventually became a core embedded device with more sensors and a more complex state-machine algorithm than the ticker itself.
Simply repeating RGB colors lowers the perceived quality and quickly becomes tiring. This article covers optical material choices, Doppler/sound/light sensor fusion, and software logic designed to reduce visual fatigue.
1. Hardware and optical design: light transmission and power engineering
Light diffusion differences by acrylic color
To match the ticker’s height and width(8 × 7 × 80cm), I first tested the same black translucent acrylic used for the ticker. That material worked well for clear price text, but it absorbed more than 70% of the internal LED light and created visible hot spots.
I switched to high-brightness white fluorescent translucent acrylic.
- Optical principle: White translucent material encourages Lambertian reflection and diffuse scattering, spreading the directional light from WS2812B LEDs more softly.
- Cost lesson: Ordering custom thick acrylic with enough durability cost about 240,000 KRW per unit including shipping. In embedded hardware projects, enclosure and custom fabrication costs can exceed the cost of the MCU or ICs.
Power safety for 80 NeoPixel(WS2812B) LEDs
If 80 WS2812B LEDs emit full white at 100% brightness, each pixel can draw about 60mA.
Supplying roughly 5V / 4.8A through the ESP32 board’s 5V pin can damage board traces or regulators. I applied the following hardware safety rules.
- Separate external power: LED strip VCC and GND are connected directly to a high-capacity 5V SMPS adapter, while only GND is shared with the ESP32(Common Ground).
- Surge support: A 1000µF or larger bypass capacitor is placed at the power input to reduce inrush current and first-pixel damage.
- Signal protection: A 330Ω ~ 470Ω resistor is placed in series between the ESP32 data pin and LED data input to reduce reflections and impedance mismatch.
2. Multi-sensor fusion and troubleshooting
For the lamp to act as more than an interior object, sensor data must work together. The device integrates three main sensors.
1) Microwave Doppler radar sensor(RCWL-0516)
- Purpose: When a user approaches, the lamp brightens naturally like a heartbeat pulse.
- Troubleshooting: PIR sensors did not work reliably inside the acrylic case. I switched to microwave Doppler sensing to detect motion through acrylic and wood without damaging the exterior.
2) Sound sensor(MAX4466 / INMP441)
- Purpose: When music exceeds a certain level, the lamp switches to an audio equalizer effect.
- Analog-noise issue: Raw analog sound values fluctuate sharply with tiny power noise. A simple threshold either reacts to keyboard noise or misses actual music peaks.
- Fix: I implemented an exponential moving average(EMA) filter to track the background-noise baseline and react only to peaks above that baseline.
3) Light sensor(LDR) and NTP time sync
Night mode needs to know whether it is actually night. I combined light sensing with Wi-Fi based NTP time synchronization so the device could switch logic at exactly midnight.
3. Randomized PRNG algorithm for visual-fatigue reduction
RGB lighting that changes in a fixed sequence becomes predictable within days. I used a pseudo-random number generator(PRNG) to reduce visual predictability.
- Dynamic effect branching: One of four main algorithms is selected randomly: rainbow flow, gradient transition, vertical swipe, and dual-layer blinking.
- Color and duration variation: Target palettes are generated randomly, and each effect lasts between 3 and 30 seconds with an asynchronous random timer.
- Duplicate prevention: The previous effect index is remembered and weighted to avoid repeated consecutive effects.
4. FastLED asynchronous state machine and night clock mode
To read multiple sensors while keeping LED animation smooth, the C++ delay() function must not be used. This boilerplate uses millis() and a state machine for the midnight one-second clock-hand mode.
#include <FastLED.h> #include <time.h> #define LED_PIN 18 #define NUM_LEDS 80 #define BRIGHTNESS 150 #define LED_TYPE WS2812B #define COLOR_ORDER GRB CRGB leds[NUM_LEDS]; enum LampState { STATE_NORMAL_RANDOM, STATE_SOUND_REACTIVE, STATE_NIGHT_CLOCK_MODE }; LampState currentState = STATE_NORMAL_RANDOM; unsigned long lastUpdate = 0; uint8_t clockHandIndex = 0; void setup() { Serial.begin(115200); FastLED.addLeds<LED_TYPE, LED_PIN, COLOR_ORDER>(leds, NUM_LEDS) .setCorrection(TypicalLEDStrip); FastLED.setBrightness(BRIGHTNESS); configTime(9 * 3600, 0, "pool.ntp.org", "time.nist.gov"); } void renderNightClockMode() { unsigned long currentMillis = millis(); if (currentMillis - lastUpdate >= 1000) { lastUpdate = currentMillis; FastLED.clear(); time_t now = time(nullptr); struct tm* timeinfo = localtime(&now); int currentSecond = timeinfo->tm_sec; clockHandIndex = map(currentSecond, 0, 59, 0, NUM_LEDS - 1); leds[clockHandIndex] = CRGB::DeepSkyBlue; if (clockHandIndex > 0) leds[clockHandIndex - 1] = CRGB(0, 10, 30); FastLED.show(); } } void checkTimeAndSwitchState() { time_t now = time(nullptr); struct tm* timeinfo = localtime(&now); int currentHour = timeinfo->tm_hour; if (currentHour >= 0 && currentHour < 6) { if (currentState != STATE_NIGHT_CLOCK_MODE) { currentState = STATE_NIGHT_CLOCK_MODE; FastLED.setBrightness(30); } } else { if (currentState == STATE_NIGHT_CLOCK_MODE) { currentState = STATE_NORMAL_RANDOM; FastLED.setBrightness(BRIGHTNESS); } } } void loop() { checkTimeAndSwitchState(); switch (currentState) { case STATE_NIGHT_CLOCK_MODE: renderNightClockMode(); break; case STATE_SOUND_REACTIVE: // renderSoundEqualizer(); break; case STATE_NORMAL_RANDOM: default: // renderRandomPaletteFlow(); break; } }
5. Spatial placement and system integration
The final placement considered spatial ergonomics.
- Placing the lamp horizontally consumed desk space and sent light directly into the eyes.
- Standing the 80cm column vertically placed the white acrylic mood lamp on the left and the black acrylic market ticker on the right, separating function while keeping a unified design.
- A 1×18 pixel LED strip was added to the right side of the ticker so time and indexes could also be read from the side.
This Part 4 verified a hardware design that reacts organically to the surrounding environment. Next, Part 5 covers the Wi-Fi hardcoding problem that appeared when giving the devices to others, and the Captive Portal / OTA update structure used to solve it.
FastLED.show() runs, microcontroller interrupts can be briefly blocked. If Wi-Fi drops or sound samples are missed, intentionally limit the FastLED.show() call rate to 30~50Hz to leave processing time for networking and analog conversion.