How to Build an ESP32 Desk Presence Busy Light

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

ESP32Smart HomeBeginner35 minutes4 components

Updated

How to Build an ESP32 Desk Presence Busy Light
For illustrative purposes only
On this page

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 deliberately set focus mode.

The build is intentionally practical rather than flashy. You will wire a common PIR module to a digital input, use the ESP32 internal pull-up for the button, share the OLED over I2C, and drive the addressable LED ring from one data pin. The firmware keeps the motion state alive for a short window after movement, debounces the button, and updates both the display and lights without needing Wi-Fi, accounts, or a phone app.

By the end you will have a useful desk sign for shared studios, home offices, livestream setups, or classrooms where people need a simple signal before interrupting you. The code is easy to adapt: change the timeout, swap the LED colours, add a quiet mode for evenings, or send the state over MQTT later once the physical version works reliably.

Wiring diagram

Wiring diagram

Interactive wiring diagram

Components needed

ComponentTypeQtyBuy
HC-SR501 PIR Motion Sensorsensor1€1.85
Tactile Push Buttoninput1€8.30
WS2812B LED Ringactuator1
SSD1306 OLED Displaydisplay1€4.30

Prices and availability are indicative and may have been updated by the supplier. Schematik may earn a commission from purchases made through affiliate links.

Assembly

1

Wire the shared power rails

Connect each module VCC to the ESP32 3V3 rail where supported, and connect all grounds together.

2

Connect the PIR sensor

Wire PIR OUT to GPIO 27, then place the sensor so it sees the desk rather than the whole room.

3

Add the busy button

Connect the button signal side to GPIO 26 and the other side to ground. The firmware uses the internal pull-up.

4

Connect the LED ring and OLED

Wire the LED ring DIN to GPIO 4. Connect the OLED SDA to GPIO 21 and SCL to GPIO 22.

5

Upload and tune

Upload the sketch, check the three states, then adjust occupiedHoldMs if the status clears too quickly or too slowly.

Pin assignments

PinConnectionType
3V3pir-1 VCCPOWER
GNDpir-1 GNDGROUND
GPIO 27pir-1 OUTDIGITAL
GPIO 26button-1 SIGDIGITAL
GNDbutton-1 GNDGROUND
3V3led-ring-1 VCCPOWER
GNDled-ring-1 GNDGROUND
GPIO 4led-ring-1 DINDIGITAL
3V3oled-1 VCCPOWER
GNDoled-1 GNDGROUND
GPIO 21oled-1 SDAI2C
GPIO 22oled-1 SCLI2C

Code

Arduino C++
#include <Wire.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_GFX.h>
#include <FastLED.h>

#define PIR_PIN 27
#define BUTTON_PIN 26
#define LED_PIN 4
#define NUM_LEDS 12
#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);
}

// Run this and build other cool things at schematik.io
Libraries: Adafruit SSD1306, Adafruit GFX Library, FastLED

Ready to build this?

Open this project in Schematik to get the full wiring diagram, pin assignments, and deployable code for the ESP32 Desk Presence Busy Light.

Open in Schematik →

Related guides