Build a Physical Pomodoro Timer (Arduino + 3D Print)

March 25, 2026 · PATRICKGENSEL

A simple, distraction-free focus tool designed and built from scratch.

Turn focus into a physical system—not another app you forget to open.


The Problem

Focus is harder than it should be.

Between notifications, apps, and constant distractions, it’s easy to lose entire days without doing meaningful work.

I’ve tried:

  • productivity apps
  • timers on my phone
  • different systems and techniques

And the result is always the same:
I either forget to use them… or they’re too complex to stick with.

So instead of downloading another app, I decided to build something.


The Idea

Create a physical Pomodoro timer that lives on my desk.

No screens.
No menus.
No distractions.

Just:

  • one button
  • one purpose
  • one system

A tool that encourages focus simply by existing.


What It Does

This timer follows a standard Pomodoro cycle:

  • 25-minute focus session
  • 5-minute break session

Behavior

  1. Press the button
  2. Timer begins
  3. LED ring gradually fills over 25 minutes
  4. Audible chime signals the end of focus
  5. LEDs count back down over 5 minutes
  6. Final chime signals reset

That’s it.

No extra features.
No overengineering.

Just a system that works.


Components Used

Core Components

  • Arduino Nano
  • 24-LED ring (NeoPixel)
  • Momentary push button
  • Piezo buzzer

Supporting Components

  • Resistor (for LED data stability)
  • Capacitor (to prevent flicker / voltage spikes)
  • Breadboard + jumper wires

Build Components

  • 3D printed enclosure
  • Laser-cut acrylic lens
  • Optional base (angled stand)

Electronics Overview

The system is intentionally simple:

  • Button → triggers cycle
  • LED ring → visual progress indicator
  • Buzzer → audio feedback
  • Arduino Nano → controls logic

Key Learnings

  • Capacitor placement helped eliminate LED flickering
  • Shorter data lines improved signal stability
  • Resistor placement near the LED ring made a noticeable difference
  • Buzzer needed a resistor to smooth out tone

The Code

I approached this from a desired behavior first mindset.

Instead of writing everything from scratch, I:

  1. Defined exactly how I wanted it to behave
  2. Used ChatGPT to generate a starting point
  3. Iterated and debugged until it worked

Core Logic

  • Idle state
  • Focus cycle (increment LEDs)
  • Break cycle (decrement LEDs)
  • Audio cues at transitions
#include <Adafruit_NeoPixel.h>

// ================== HARDWARE ==================
#define LED_PIN     6
#define LED_COUNT   24

#define BUTTON_PIN  2
#define BUZZER_PIN  9

Adafruit_NeoPixel strip(LED_COUNT, LED_PIN, NEO_GRB + NEO_KHZ800);

// ================== DEMO MODE ==================
const bool DEMO_MODE = false; // false = real minutes
const unsigned long TICK_MS = DEMO_MODE ? 1000UL : 60000UL;

// ================== TIMING ==================
const int FOCUS_MINUTES = 25;
const int COOLDOWN_MINUTES = 5;

// ================== LED SETTINGS ==================
const uint8_t BRIGHTNESS = 40;

// ================== BUTTON DEBOUNCE ==================
bool buttonStableState = HIGH;
bool lastButtonReading = HIGH;
unsigned long lastDebounceMs = 0;
const unsigned long DEBOUNCE_MS = 30;

// ================== STATE MACHINE ==================
enum Mode { IDLE, FOCUS, COOLDOWN, COMPLETE };
Mode mode = IDLE;

unsigned long modeStartMs = 0;

// Focus state
int focusLit = 0;

// Cooldown state
int cooldownRemoved = 0;
int cooldownLit = 24;
unsigned long cooldownNextTickMs = 0;

// Complete animation
int breatheCyclesDone = 0;
bool breatheIncreasing = true;
int breatheLevel = 0;
unsigned long breatheLastStepMs = 0;
const unsigned long BREATHE_STEP_MS = 20;
const int BREATHE_CYCLES = 3;

// ================== HELPERS ==================
void clearStrip() {
  strip.clear();
  strip.show();
}

void flashConfirm() {
  for (int i = 0; i < LED_COUNT; i++) strip.setPixelColor(i, strip.Color(255, 255, 255));
  strip.show(); delay(80);
  clearStrip(); delay(80);
  for (int i = 0; i < LED_COUNT; i++) strip.setPixelColor(i, strip.Color(255, 255, 255));
  strip.show(); delay(80);
  clearStrip();
}

bool buttonPressedEvent() {
  bool reading = digitalRead(BUTTON_PIN);

  if (reading != lastButtonReading) {
    lastDebounceMs = millis();
    lastButtonReading = reading;
  }

  if ((millis() - lastDebounceMs) > DEBOUNCE_MS) {
    if (reading != buttonStableState) {
      buttonStableState = reading;
      if (buttonStableState == LOW) return true;
    }
  }
  return false;
}

// ================== SOUND (LOWER OCTAVE) ==================
// End of focus: gentle ascending chime (one octave lower)
void playFocusDoneJingle() {
  tone(BUZZER_PIN, 494, 180);  delay(230); // B4
  tone(BUZZER_PIN, 587, 200);  delay(250); // D5
  tone(BUZZER_PIN, 784, 240);  delay(290); // G5
  noTone(BUZZER_PIN);
}

// End of cooldown / completion: gentle descending chime (one octave lower)
void playCompleteJingle() {
  tone(BUZZER_PIN, 784, 200);  delay(250); // G5
  tone(BUZZER_PIN, 587, 220);  delay(270); // D5
  tone(BUZZER_PIN, 494, 260);  delay(320); // B4
  noTone(BUZZER_PIN);
}

// ================== RENDER ==================
void renderLitCount(int litCount, uint32_t color) {
  strip.clear();
  for (int i = 0; i < litCount; i++) strip.setPixelColor(i, color);
  strip.show();
}

void renderFocus(unsigned long elapsedMs) {
  int minute = (int)(elapsedMs / TICK_MS) + 1;

  int targetLit;
  if (minute <= 1) targetLit = 0;
  else targetLit = constrain(minute - 1, 0, 24);

  if (targetLit != focusLit) {
    focusLit = targetLit;
    renderLitCount(focusLit, strip.Color(0, 255, 0));
  }
}

void renderCooldownLit() {
  renderLitCount(cooldownLit, strip.Color(0, 120, 255));
}

// ================== TRANSITIONS ==================
void startFocus() {
  mode = FOCUS;
  modeStartMs = millis();
  focusLit = 0;
  clearStrip();
}

void startCooldown() {
  mode = COOLDOWN;
  modeStartMs = millis();

  cooldownRemoved = 0;
  cooldownLit = 24;
  renderCooldownLit();

  cooldownNextTickMs = millis() + TICK_MS;
}

void startComplete() {
  mode = COMPLETE;
  modeStartMs = millis();

  breatheCyclesDone = 0;
  breatheIncreasing = true;
  breatheLevel = 0;
  breatheLastStepMs = millis();
}

void backToIdle() {
  mode = IDLE;
  clearStrip();
}

// ================== SETUP / LOOP ==================
void setup() {
  pinMode(BUTTON_PIN, INPUT_PULLUP);
  pinMode(BUZZER_PIN, OUTPUT);

  strip.begin();
  strip.setBrightness(BRIGHTNESS);
  strip.show();
}

void loop() {
  if (buttonPressedEvent() && mode == IDLE) {
    flashConfirm();
    startFocus();
  }

  unsigned long now = millis();

  if (mode == FOCUS) {
    unsigned long elapsed = now - modeStartMs;
    renderFocus(elapsed);

    if (elapsed >= (unsigned long)FOCUS_MINUTES * TICK_MS) {
      playFocusDoneJingle();
      startCooldown();
    }
  }

  if (mode == COOLDOWN) {
    if (cooldownRemoved < COOLDOWN_MINUTES && now >= cooldownNextTickMs) {
      cooldownRemoved++;
      cooldownLit = 24 - cooldownRemoved;
      renderCooldownLit();
      cooldownNextTickMs += TICK_MS;
    }

    if (cooldownRemoved >= COOLDOWN_MINUTES) {
      playCompleteJingle();
      startComplete();
    }
  }

  if (mode == COMPLETE) {
    if ((now - breatheLastStepMs) >= BREATHE_STEP_MS) {
      breatheLastStepMs = now;

      if (breatheIncreasing) breatheLevel += 5;
      else breatheLevel -= 5;

      if (breatheLevel >= 255) {
        breatheLevel = 255;
        breatheIncreasing = false;
      } else if (breatheLevel <= 0) {
        breatheLevel = 0;
        breatheIncreasing = true;
        breatheCyclesDone++;
      }

      strip.clear();
      uint32_t c = strip.Color(0, breatheLevel, breatheLevel);
      for (int i = 0; i < 19; i++) strip.setPixelColor(i, c);
      strip.show();

      if (breatheCyclesDone >= BREATHE_CYCLES) {
        backToIdle();
      }
    }
  }
}


Prototyping

The first version was built on a breadboard.

Goals

  • Validate logic
  • Test LED behavior
  • Identify electrical issues

Issues Encountered

  • LED flickering
  • Incorrect transition to break cycle
  • Wiring inefficiencies

All solved through:

  • component placement adjustments
  • code tweaks

Enclosure Design (Fusion 360)

Once the electronics worked, I moved into enclosure design.

Workflow

  • Imported component models (STEP files from GrabCAD)
  • Arranged layout in Fusion 360
  • Designed around real geometry (not guessing dimensions)

Key Design Decisions

  • Separate back plate for easier assembly
  • Peg alignment system for clean construction
  • Front plate to hold LED ring + lens
  • Internal cavities for components

👉 [OPTIONAL: INSERT FUSION SCREENSHOT OR IMAGE HERE]


3D Printing Iteration

First print revealed a few issues:

  • Button hole too tight
  • USB port clearance too small
  • Mounting holes slightly misaligned

Adjusted and reprinted.

👉 [OPTIONAL: INSERT PROTOTYPE PHOTO HERE]


Lens + Lighting

The front lens was:

  • laser cut from acrylic
  • engraved for light diffusion

This helped:

  • soften LED brightness
  • create a cleaner visual output

👉 [OPTIONAL: INSERT IMAGE OF LENS / LIGHT DIFFUSION HERE]


Final Design Details

  • Circular lens with trim ring for a more finished look
  • Angled base for better visibility on a desk
  • Internal layout optimized for cleaner wiring
  • Terminal adapter used for easier assembly and maintenance

👉 [INSERT FINAL PRODUCT PHOTOS HERE]


Why This Matters

This isn’t just a timer.

It’s a system.

A physical object that:

  • removes friction
  • builds a habit
  • improves focus

Instead of relying on discipline, it creates an environment that encourages it.


Files & Resources

All files for this project are available here:

👉 [INSERT PATREON / DOWNLOAD LINK HERE]

Includes:

  • STL files
  • Fusion 360 files
  • Arduino code

What I’d Improve

  • Battery-powered version
  • Smaller footprint
  • Multiple modes (deep work, short sessions)
  • Cleaner internal wiring system

Build Your Own

If you’ve ever struggled with focus, try building your own version.
or take this idea and push it further.

That’s the whole point.


Project Details

  • Build Time: 5-6 hours
  • Skill Level: intermediate
  • Tools Used: 3D Printer, Soldering Iron, Laser Cutter
  • Materials Cost: $20-$30