Schematik build

How to Build an ESP32 Desk Presence Busy Light

Motion sensing, manual busy mode, OLED status, and a colour LED ring

Schematik

Published May 27, 2026 · Updated July 22, 2026

ESP32Beginner35 minutes9 components4 assembly steps
Remix this project
Photo of How to Build an ESP32 Desk Presence Busy Light

What you'll build

This guide walks you through building a small ESP32 desk presence light that makes your workspace status obvious at a glance. A PIR motion sensor marks the desk as recently active, a tactile button toggles manual busy mode, an SSD1306 OLED shows the current state in plain text, and a WS2812B LED ring changes colour: green for free, orange for recently active, and red when you have set focus mode deliberately.

The build is intentionally practical. You wire a common HC-SR501 PIR module to a digital input, use the ESP32 internal pull-up for the button, share the OLED over I²C, and drive the addressable LED ring from one data pin. No Wi-Fi, no phone app, and no account is required.

By the end you have a useful desk sign for shared studios, home offices, or classrooms where people need a clear signal before interrupting you.

What you are building

This guide produces a standalone presence indicator. The firmware has four jobs:

  1. poll the HC-SR501 PIR sensor and mark the desk as occupied for occupiedHoldMs milliseconds after the last detected motion (default 120 seconds),
  2. debounce the tactile button and toggle manual busy mode on each press,
  3. update the WS2812B LED ring colour: green (free), orange (recently active), red (manual busy),
  4. update the SSD1306 OLED with a two-line status: a large label (FREE / HERE / BUSY) and a short detail string.

Out of scope: Wi-Fi reporting, MQTT, Home Assistant integration, and multi-room synchronisation. The code is structured to make those additions straightforward once the physical device works reliably.

Upload and calibrate

Flash the starter sketch from Schematik. There are no Wi-Fi credentials to enter. The only runtime constant worth tuning is occupiedHoldMs, which controls how long the desk stays in the HERE (orange) state after the last detected movement:

  • occupiedHoldMs — default 120000 (2 minutes). Reduce it if the status takes too long to clear after you leave; increase it if it flickers back to FREE during normal desk use.

After flashing, open Serial Monitor at 115200 baud and verify the board starts. Wave a hand near the PIR; the ring should turn orange and the OLED should show HERE. Press the button; the ring should turn red and the OLED should show BUSY. Press again to return to the automatic state.

If the ring flickers or shows wrong colours, lower FastLED.setBrightness() from 80 to 40 to reduce current draw.

Troubleshooting

  • PIR triggers constantly with no movement: the HC-SR501 needs a 30–60 second warm-up after power-on before readings are reliable. Wait a minute after boot before testing.
  • PIR never triggers: confirm OUT is wired to GPIO 27, not to the VCC or GND rail. The dome side of the sensor should face the area you want to monitor, not the wall.
  • Button toggles twice per press: the firmware debounces with a 350 ms guard on lastButtonMs. If double-triggering persists, increase that value in the code.
  • OLED blank, no startup text: check SDA on GPIO 21 and SCL on GPIO 22. Most SSD1306 modules use I²C address 0x3C; confirm with an I²C scanner if the display does not respond.
  • LED ring shows wrong colour or stays off: confirm GND is shared between the ring and the ESP32. A loose data wire on GPIO 4 is the most common cause of missing colour output.
  • All three states show the same colour: FastLED requires the NEOPIXEL type and correct NUM_LEDS count. Confirm the ring pixel count matches NUM_LEDS 12 in the code, or adjust it to match your ring.

Going further

The three-state model maps naturally onto MQTT: publish the current state string on each transition and subscribe to a "set busy" topic from another device. That opens up integration with calendar apps, meeting room displays, or a simple Home Assistant automation. On the physical side, replacing the LED ring with a larger diffused enclosure — a frosted ping-pong ball, a lamp shade, or a 3D-printed diffuser — makes the colour visible from across a room without being distracting at close range.

Wiring diagram

Interactive · read-only

Pan and zoom to explore the wiring. Remix the project to edit it in your own workspace.

Parts list

Bill of materials
ComponentQtyNotes
Tactile Push Button1Toggles manual busy mode.
470–1000 µF electrolytic capacitor assortment1Bulk decoupling across the 5 V addressable LED supply.
220 Ω resistor1Series resistor at the first addressable LED data input.
74AHCT125 logic level shifter1Buffers 3.3 V GPIO data to a 5 V WS2812 input.
5 V, 4 A USB-C wall power supply with switch1Sized for the LED load; grounds must be common with the controller.
USB-C socket breakout for 5 V and GND1Breaks the supply output out to safe wired 5 V and GND rails.
WS2812B LED Ring1Shows free, present, or busy status in colour.
SSD1306 OLED Display1Shows the current desk status as text.
HC-SR501 PIR Motion Sensor1Detects recent movement near the desk.

Assembly

4 steps
  1. Wire the low-voltage sensor and controls

    Power the OLED from ESP32 3V3 and connect SDA to GPIO21 and SCL to GPIO22. Power the HC-SR501 from the 5 V breakout, connect OUT to GPIO27, and connect the button between GPIO26 and GND. All grounds must be common.

  2. Build the regulated 5 V LED rail

    Plug the 5 V USB-C supply into the socket breakout. Connect breakout VBUS/5V to WS2812B LED ring 5V and 74AHCT125 VCC. Connect breakout GND, ESP32 GND, WS2812B LED ring GND, and 74AHCT125 GND together. Place the bulk capacitor across 5 V and GND at the LEDs, observing polarity. Tie unused inputs 2A, 3A, and 4A to GND and unused enables 2OE, 3OE, and 4OE to 5 V.

    • Unplug the supply while wiring. Verify 5 V polarity with a multimeter before connecting the LEDs or ESP32.
  3. Buffer the LED data signal

    Connect ESP32 GPIO4 to 74AHCT125 input 1A, tie 1OE to GND, and connect output 1Y through the 220 Ω resistor to WS2812B LED ring DIN. Do not connect GPIO4 directly to DIN. Tie unused inputs 2A, 3A, and 4A to GND and unused enables 2OE, 3OE, and 4OE to 5 V.

  4. Upload, test, and calibrate

    Upload the sketch, open Serial Monitor at 115200 baud, verify every input and output, then perform the calibration described in the guide before enclosing the project.

Pin assignments

Board wiring reference
PinConnectionType
GPIO 26button-1 SIGdigital
GNDbutton-1 GNDground
EXTdesk-presence-busy-light-bulk-capacitor-1 +regulated-5v-supply:5V_OUTpower
EXTdesk-presence-busy-light-bulk-capacitor-1 regulated-5v-supply:GNDground
EXTdesk-presence-busy-light-led-data-resistor-1 INlogic-level-shifter:1Ydigital
EXTdesk-presence-busy-light-led-data-resistor-1 OUTws2812b-ring:DINdigital
EXTdesk-presence-busy-light-logic-level-shifter-1 regulated-5v-supply:5V_OUTpower
EXTdesk-presence-busy-light-logic-level-shifter-1 regulated-5v-supply:GNDground
EXTdesk-presence-busy-light-logic-level-shifter-1 regulated-5v-supply:GNDenable-low
GPIO 4desk-presence-busy-light-logic-level-shifter-1 data-in
EXTdesk-presence-busy-light-logic-level-shifter-1 led-data-resistor:INdata-out
EXTdesk-presence-busy-light-logic-level-shifter-1 regulated-5v-supply:GNDunused-input-low
EXTdesk-presence-busy-light-logic-level-shifter-1 regulated-5v-supply:GNDunused-input-low
EXTdesk-presence-busy-light-logic-level-shifter-1 regulated-5v-supply:GNDunused-input-low
EXTdesk-presence-busy-light-logic-level-shifter-1 disable-high
EXTdesk-presence-busy-light-logic-level-shifter-1 disable-high
EXTdesk-presence-busy-light-logic-level-shifter-1 disable-high
EXTdesk-presence-busy-light-regulated-5v-supply-1 5V_OUTusb-c-power-breakout:VBUSpower
EXTdesk-presence-busy-light-regulated-5v-supply-1 GNDusb-c-power-breakout:GNDground
5Vdesk-presence-busy-light-usb-c-power-breakout-1 VBUSpower
GNDdesk-presence-busy-light-usb-c-power-breakout-1 GNDground
EXTled-ring-1 VCCregulated-5v-supply:5V_OUTpower
EXTled-ring-1 GNDregulated-5v-supply:GNDground
EXTled-ring-1 DINled-data-resistor:OUTdigital
3V3oled-1 VCCpower
GNDoled-1 GNDground
GPIO 21oled-1 SDAi2c
GPIO 22oled-1 SCLi2c
3V3pir-1 VCCpower
GNDpir-1 GNDground
GPIO 27pir-1 OUTdigital

Firmware

ESP32
schematik_esp32.inoDeploy to device
#include <Wire.h>
#include <FastLED.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_GFX.h>

#define PIR_PIN 27
#define BUTTON_PIN 26
#define LED_PIN 4
#define NUM_LEDS 16
#define SDA_PIN 21
#define SCL_PIN 22

Adafruit_SSD1306 display(128, 64, &Wire, -1);
CRGB leds[NUM_LEDS];

bool manualBusy = false;
bool occupied = false;
unsigned long lastMotionMs = 0;
unsigned long lastButtonMs = 0;
const unsigned long occupiedHoldMs = 120000;

void setRing(CRGB color) {
  fill_solid(leds, NUM_LEDS, color);
  FastLED.show();
}

void drawStatus(const char* status, const char* detail) {
  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);
  display.setTextSize(2);
  display.setCursor(0, 6);
  display.println(status);
  display.setTextSize(1);
  display.setCursor(0, 40);
  display.println(detail);
  display.display();
}

void setup() {
  pinMode(PIR_PIN, INPUT);
  pinMode(BUTTON_PIN, INPUT_PULLUP);
  Wire.begin(SDA_PIN, SCL_PIN);
  FastLED.addLeds<NEOPIXEL, LED_PIN>(leds, NUM_LEDS);
  FastLED.setBrightness(80);
  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
  drawStatus("FREE", "Wave nearby or press button");
  setRing(CRGB::Green);
}

void loop() {
  if (digitalRead(PIR_PIN) == HIGH) {
    occupied = true;
    lastMotionMs = millis();
  }

  if (digitalRead(BUTTON_PIN) == LOW && millis() - lastButtonMs > 350) {
    manualBusy = !manualBusy;
    lastButtonMs = millis();
  }

  if (occupied && millis() - lastMotionMs > occupiedHoldMs) {
    occupied = false;
  }

  if (manualBusy) {
    setRing(CRGB::Red);
    drawStatus("BUSY", "Manual focus mode");
  } else if (occupied) {
    setRing(CRGB::Orange);
    drawStatus("HERE", "Motion seen at desk");
  } else {
    setRing(CRGB::Green);
    drawStatus("FREE", "No recent motion");
  }

  delay(200);
}

“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.

Open in Schematik