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

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:
- poll the HC-SR501 PIR sensor and mark the desk as occupied for
occupiedHoldMsmilliseconds after the last detected motion (default 120 seconds), - debounce the tactile button and toggle manual busy mode on each press,
- update the WS2812B LED ring colour: green (free), orange (recently active), red (manual busy),
- 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
NEOPIXELtype and correctNUM_LEDScount. Confirm the ring pixel count matchesNUM_LEDS 12in 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-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 |
|---|---|---|
| Tactile Push Button | 1 | Toggles manual busy mode. |
| 470–1000 µF electrolytic capacitor assortment | 1 | Bulk decoupling across the 5 V addressable LED supply. |
| 220 Ω resistor | 1 | Series resistor at the first addressable LED data input. |
| 74AHCT125 logic level shifter | 1 | Buffers 3.3 V GPIO data to a 5 V WS2812 input. |
| 5 V, 4 A USB-C wall power supply with switch | 1 | Sized for the LED load; grounds must be common with the controller. |
| USB-C socket breakout for 5 V and GND | 1 | Breaks the supply output out to safe wired 5 V and GND rails. |
| WS2812B LED Ring | 1 | Shows free, present, or busy status in colour. |
| SSD1306 OLED Display | 1 | Shows the current desk status as text. |
| HC-SR501 PIR Motion Sensor | 1 | Detects recent movement near the desk. |
Assembly
4 stepsWire 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.
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.
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.
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| Pin | Connection | Type |
|---|---|---|
| GPIO 26 | button-1 SIG | digital |
| GND | button-1 GND | ground |
| EXT | desk-presence-busy-light-bulk-capacitor-1 + → regulated-5v-supply:5V_OUT | power |
| EXT | desk-presence-busy-light-bulk-capacitor-1 − → regulated-5v-supply:GND | ground |
| EXT | desk-presence-busy-light-led-data-resistor-1 IN → logic-level-shifter:1Y | digital |
| EXT | desk-presence-busy-light-led-data-resistor-1 OUT → ws2812b-ring:DIN | digital |
| EXT | desk-presence-busy-light-logic-level-shifter-1 → regulated-5v-supply:5V_OUT | power |
| EXT | desk-presence-busy-light-logic-level-shifter-1 → regulated-5v-supply:GND | ground |
| EXT | desk-presence-busy-light-logic-level-shifter-1 → regulated-5v-supply:GND | enable-low |
| GPIO 4 | desk-presence-busy-light-logic-level-shifter-1 | data-in |
| EXT | desk-presence-busy-light-logic-level-shifter-1 → led-data-resistor:IN | data-out |
| EXT | desk-presence-busy-light-logic-level-shifter-1 → regulated-5v-supply:GND | unused-input-low |
| EXT | desk-presence-busy-light-logic-level-shifter-1 → regulated-5v-supply:GND | unused-input-low |
| EXT | desk-presence-busy-light-logic-level-shifter-1 → regulated-5v-supply:GND | unused-input-low |
| EXT | desk-presence-busy-light-logic-level-shifter-1 | disable-high |
| EXT | desk-presence-busy-light-logic-level-shifter-1 | disable-high |
| EXT | desk-presence-busy-light-logic-level-shifter-1 | disable-high |
| EXT | desk-presence-busy-light-regulated-5v-supply-1 5V_OUT → usb-c-power-breakout:VBUS | power |
| EXT | desk-presence-busy-light-regulated-5v-supply-1 GND → usb-c-power-breakout:GND | ground |
| 5V | desk-presence-busy-light-usb-c-power-breakout-1 VBUS | power |
| GND | desk-presence-busy-light-usb-c-power-breakout-1 GND | ground |
| EXT | led-ring-1 VCC → regulated-5v-supply:5V_OUT | power |
| EXT | led-ring-1 GND → regulated-5v-supply:GND | ground |
| EXT | led-ring-1 DIN → led-data-resistor:OUT | digital |
| 3V3 | oled-1 VCC | power |
| GND | oled-1 GND | ground |
| GPIO 21 | oled-1 SDA | i2c |
| GPIO 22 | oled-1 SCL | i2c |
| 3V3 | pir-1 VCC | power |
| GND | pir-1 GND | ground |
| GPIO 27 | pir-1 OUT | digital |
Firmware
ESP32#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.


