How to Build an ESP32 Pomodoro Focus Timer

OLED countdown, rotary controls, buzzer alerts, and LED progress

ESP32HealthBeginner45 minutes4 components

Updated

How to Build an ESP32 Pomodoro Focus Timer
For illustrative purposes only
On this page

What you'll build

This guide walks through a desk-sized ESP32 focus timer for Pomodoro-style work sessions. The OLED shows the current mode and countdown, the rotary encoder gives you one physical control for start, pause, and reset, the buzzer gives short alerts, and the LED ring shows progress at a glance.

The build uses common parts: an ESP32 development board, SSD1306 I2C OLED, KY-040 rotary encoder, piezo buzzer, and a 16-pixel WS2812B ring. The starter code keeps the behaviour intentionally simple: 25 minutes of focus, 5 minutes of break, pause and resume from the encoder button, and a reset shortcut while paused.

It is deliberately offline: no app account, no Wi-Fi credentials, and no cloud service. Once it is flashed, the timer lives on your desk and works from the encoder alone.

Wiring diagram

Wiring diagram

Interactive wiring diagram

Components needed

ComponentTypeQtyBuy
SSD1306 OLEDdisplay1€4.30
KY-040 Rotary Encodersensor1€8.35
Piezo Buzzeractuator1€4.75
WS2812B LED Ringactuator1

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

Connect the OLED display

Wire the SSD1306 OLED VCC to 3.3V, GND to ground, SDA to GPIO21, and SCL to GPIO22.

2

Wire the rotary encoder

Connect encoder VCC to 3.3V, GND to ground, CLK to GPIO32, DT to GPIO33, and SW to GPIO25.

3

Add the buzzer and LED ring

Connect the piezo buzzer signal to GPIO26 and its other lead to GND. Connect the WS2812B ring DIN to GPIO4, GND to common ground, and 5V to a suitable 5V rail.

4

Upload and try a session

Install the Adafruit SSD1306, Adafruit GFX, and FastLED libraries, then deploy the sketch. Press the encoder button to start, press again to pause, and turn the encoder while paused to reset to a focus or break session.

Pin assignments

PinConnectionType
3V3focus-oled-1 VCCPOWER
GNDfocus-oled-1 GNDGROUND
GPIO 21focus-oled-1 SDAI2C
GPIO 22focus-oled-1 SCLI2C
3V3focus-encoder-1 VCCPOWER
GNDfocus-encoder-1 GNDGROUND
GPIO 32focus-encoder-1 CLKDIGITAL
GPIO 33focus-encoder-1 DTDIGITAL
GPIO 25focus-encoder-1 SWDIGITAL
GNDfocus-buzzer-1 GNDGROUND
GPIO 26focus-buzzer-1 SIGPWM
5Vfocus-led-ring-1 5VPOWER
GNDfocus-led-ring-1 GNDGROUND
GPIO 4focus-led-ring-1 DINDATA

Code

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <FastLED.h>

#define OLED_SDA 21
#define OLED_SCL 22
#define OLED_RESET -1
#define ENCODER_CLK 32
#define ENCODER_DT 33
#define ENCODER_SW 25
#define BUZZER_PIN 26
#define LED_PIN 4
#define NUM_LEDS 16

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

const unsigned long WORK_MINUTES = 25;
const unsigned long BREAK_MINUTES = 5;

enum TimerMode { SET_WORK, RUN_WORK, RUN_BREAK, PAUSED };
TimerMode mode = SET_WORK;
TimerMode pausedFrom = RUN_WORK;

unsigned long remainingSeconds = WORK_MINUTES * 60;
unsigned long lastTick = 0;
int lastClk = HIGH;

void beep(int frequency, int durationMs) {
  tone(BUZZER_PIN, frequency, durationMs);
  delay(durationMs + 20);
}

void showProgress() {
  unsigned long total = mode == RUN_BREAK ? BREAK_MINUTES * 60 : WORK_MINUTES * 60;
  int lit = map(remainingSeconds, 0, total, 0, NUM_LEDS);
  for (int i = 0; i < NUM_LEDS; i++) {
    if (i < lit) {
      leds[i] = mode == RUN_BREAK ? CRGB::Green : CRGB::DeepSkyBlue;
    } else {
      leds[i] = CRGB::Black;
    }
  }
  FastLED.show();
}

void drawScreen() {
  unsigned long minutes = remainingSeconds / 60;
  unsigned long seconds = remainingSeconds % 60;

  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);
  display.setTextSize(1);
  display.setCursor(0, 0);
  display.println(mode == RUN_BREAK ? "Break" : mode == PAUSED ? "Paused" : "Focus");

  display.setTextSize(3);
  display.setCursor(16, 22);
  if (minutes < 10) display.print('0');
  display.print(minutes);
  display.print(':');
  if (seconds < 10) display.print('0');
  display.print(seconds);

  display.setTextSize(1);
  display.setCursor(0, 56);
  display.println("Press: start/pause  Turn: reset");
  display.display();
}

void startWork() {
  mode = RUN_WORK;
  remainingSeconds = WORK_MINUTES * 60;
  lastTick = millis();
  beep(1200, 90);
}

void startBreak() {
  mode = RUN_BREAK;
  remainingSeconds = BREAK_MINUTES * 60;
  lastTick = millis();
  beep(900, 100);
  beep(1400, 120);
}

void handleButton() {
  static int lastButton = HIGH;
  int now = digitalRead(ENCODER_SW);
  if (lastButton == HIGH && now == LOW) {
    if (mode == SET_WORK) {
      startWork();
    } else if (mode == RUN_WORK || mode == RUN_BREAK) {
      pausedFrom = mode;
      mode = PAUSED;
      beep(600, 80);
    } else if (mode == PAUSED) {
      mode = pausedFrom;
      lastTick = millis();
      beep(1000, 80);
    }
  }
  lastButton = now;
}

void handleEncoder() {
  int clk = digitalRead(ENCODER_CLK);
  if (clk != lastClk && clk == LOW) {
    bool clockwise = digitalRead(ENCODER_DT) != clk;
    if (mode == SET_WORK || mode == PAUSED) {
      remainingSeconds = clockwise ? WORK_MINUTES * 60 : BREAK_MINUTES * 60;
      mode = SET_WORK;
      beep(clockwise ? 1500 : 800, 40);
    }
  }
  lastClk = clk;
}

void setup() {
  pinMode(ENCODER_CLK, INPUT_PULLUP);
  pinMode(ENCODER_DT, INPUT_PULLUP);
  pinMode(ENCODER_SW, INPUT_PULLUP);
  pinMode(BUZZER_PIN, OUTPUT);

  Wire.begin(OLED_SDA, OLED_SCL);
  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
  FastLED.addLeds<NEOPIXEL, LED_PIN>(leds, NUM_LEDS);
  FastLED.setBrightness(80);
  drawScreen();
}

void loop() {
  handleButton();
  handleEncoder();

  if ((mode == RUN_WORK || mode == RUN_BREAK) && millis() - lastTick >= 1000) {
    lastTick += 1000;
    if (remainingSeconds > 0) remainingSeconds--;
    if (remainingSeconds == 0) {
      if (mode == RUN_WORK) startBreak();
      else startWork();
    }
  }

  showProgress();
  drawScreen();
  delay(80);
}

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