1. Home
  2. Projects
  3. ESP32 controlled lamp

Embedded · Fabrication

ESP32 controlled lamp

A desk lamp with a solid steel base and an addressable LED strip, driven over Wi-Fi by firmware and a desktop app I wrote.

Role
Solo — fabrication, electronics, software
Timeline
Sep 2026 – Sep 2026
Tools
ESP32, FastLED, C++, Dear ImGui, welding, 3D printing
Links
GitHub
The finished lamp on a desk with its LED strip lit
The finished lamp. Half an inch of steel keeps it planted.

Overview

I wanted a lamp that felt like it was built rather than bought: something heavy, made from real material, with lighting I could control down to the individual LED. The result is a 4.5-inch disc of half-inch steel plate, a welded conduit neck, and an ESP32 that drives a 60-LED WS2812B strip.

The software is split in an unusual way. The ESP32 runs a deliberately simple receiver, and every lighting pattern is generated on my PC and streamed to the lamp over the network. Adding a new effect means writing a C++ class on the desktop, never reflashing the microcontroller.

28lighting effects, all generated on the PC
60 FPSframes streamed over UDP
60individually addressable LEDs

The build

Base and neck

The base started as half-inch steel plate, which I cut into a 4.5-inch circle and cleaned up so it sits flat. The neck is a length of conduit welded to the top of the plate. At that thickness the plate is a big heat sink compared with the thin-wall conduit, so the weld is a balancing act: enough heat to wet into the plate without blowing through the tube.

Mounting the electronics

I drilled the plate and installed standoffs, then mounted a protoboard carrying the ESP32 on top of them. Standing the board off the steel keeps the solder joints from shorting to the base and leaves room for wiring underneath. Everything is enclosed in a 3D-printed housing, so no electronics are exposed.

Electronics

The strip is a BTF-LIGHTING WS2812B, one meter long with 60 LEDs, running at 5 V. Each LED can pull about 60 mA at full white, so the whole strip can draw close to 3.6 A. That shaped the wiring more than anything else.

ConnectionGoes toWhy
Strip 5 V5 V supply (+)The ESP32's regulator can't supply several amps
Strip GNDSupply (−) and ESP32 GNDData needs a shared ground reference
Strip DINGPIO 13 through ~330 ΩDamps ringing on the data line
1000 µF capacitorAcross the strip's powerAbsorbs the inrush when the strip turns on

The firmware also enforces a software current limit: FastLED scales brightness down whenever a frame would draw more than a set budget (2 A by default). That protects the supply even if I send a full-white frame by accident.

Software

A deliberately "dumb" receiver

The ESP32 does three things: joins Wi-Fi, listens for pixel frames, and answers discovery pings. It doesn't know what a rainbow or a fire effect is. It just copies RGB values onto the strip. Keeping all the logic on the PC means the firmware has almost nothing that can go wrong, and I iterate on effects at desktop speed.

The protocol

Everything runs over UDP on port 4210. UDP fits because a dropped frame doesn't matter: another arrives 16 ms later. If several frames queue up, the firmware drains them all and shows only the newest, so the lamp never lags behind the app.

MessageDirectionFormat
FramePC → ESP32PXL1, 2-byte LED count, then R, G, B per LED
DiscoverPC → ESP32PXLDISCOVER, sent directly or broadcast
ReplyESP32 → PCPXLHERE <num_leds> <name>

The desktop app

The controller is a C++ app built with Dear ImGui. It scans the network to find the lamp automatically, then renders effects at 60 FPS and streams them out. It has 28 effects across categories, from calm ones like breathing and color fade to simulations like fire, aurora, ocean, lightning, and bouncing balls. It also has a color wheel with hex entry and fine-tune sliders, brightness, speed and intensity controls, and a live preview of the strip. A running estimate of current draw sits at the bottom of the window. Settings save automatically between sessions.

Moving all the effect logic off the microcontroller was the best decision in the project. The firmware hasn't needed a change since, and a new effect is one small C++ class.

Code

The heart of the firmware: drain every waiting packet, keep only the newest frame, and answer discovery requests along the way.

firmware/esp32_led_strip/esp32_led_strip.ino
bool gotFrame = false;
// Drain every waiting packet; only the newest frame gets displayed.
while (udp.parsePacket() > 0) {
  int len = udp.read(packet, sizeof(packet));

  if (len >= 6 && memcmp(packet, "PXL1", 4) == 0) {
    int count = (packet[4] << 8) | packet[5];
    count = min(count, NUM_LEDS);
    count = min(count, (len - 6) / 3);
    for (int i = 0; i < count; i++) {
      const uint8_t* p = packet + 6 + i * 3;
      leds[i].setRGB(p[0], p[1], p[2]);
    }
    gotFrame = true;
    lastFrameMs = millis();
  }
  else if (len >= 11 && memcmp(packet, "PXLDISCOVER", 11) == 0) {
    char reply[80];
    int n = snprintf(reply, sizeof(reply), "PXLHERE %d %s", NUM_LEDS, DEVICE_NAME);
    udp.beginPacket(udp.remoteIP(), udp.remotePort());
    udp.write((const uint8_t*)reply, n);
    udp.endPacket();
  }
}

if (gotFrame) FastLED.show();

The full repository has the firmware, the desktop controller, and instructions for flashing and building both.

What I learned

  • Put the complexity where it's cheapest to change. A PC is far easier to iterate on than a microcontroller.
  • Budget power before you pick a supply. An LED strip that looks small can still pull several amps.
  • Turning off Wi-Fi power-saving on the ESP32 made a visible difference to streaming smoothness.

Next: [a dimmer knob on the base, a second lamp, or music-reactive effects].