Pomodoro Timer: My Arduino based focus tool

Pomodoro Technique, a time management tool that involves splitting work tasks into 25-minute intervals, known as pomodoros, with breaks scheduled in between each interval.
Jun 20, 2025
embedded image

Time management is hard. And staying focused in a world full of distractions? Even harder. That’s why the Pomodoro Technique has become a go-to strategy for so many people (myself included) looking to get real work done without burning out.

If you’re not familiar, the Pomodoro Technique is simple: you work for 25 minutes (that’s one Pomodoro), then take a 5-minute break. After four Pomodoros, you take a longer 15-30 minute break. It’s all about maintaining sharp focus in short, powerful bursts — with rest built in to keep your brain fresh.

Now, while there are dozens of apps and timers out there that can do this, I wanted something more physical, interactive, and honestly... more fun. So I built my own Pomodoro timer — one that you can literally flip like an hourglass to start your session.

The Idea: Bring Pomodoro to Life

Instead of just tapping a screen or clicking a button, I wanted to simulate the feeling of flipping an hourglass to start your work session. That sense of intention — physically flipping something — makes a difference. It’s a subtle ritual that helps your brain say, “Okay, focus time.”

So I grabbed some components lying around — an Arduino Nano, an MPU6050 gyroscope, two MAX7219 LED matrices, and a buzzer — and got to work.

How It Works

Arduino Nano: It’s small, cheap, and perfect for prototyping. No need to go overkill with something like the ESP just yet, although that is something that I am looking forward to as I take this project even further.

MPU6050 (Gyroscope + Accelerometer): This is the brain behind the sensing. It detects the orientation of the device — so when you flip the timer, it knows. That flip starts a Pomodoro session. Flip again and it resets or pauses — depending on how we configure it.

MAX7219 LED Matrices: I used two of these, rotated at a 45° offset. This was to mimic the hourglass shape, with LED "particles" moving from one matrix to the other during a session. The result? A really cool visual effect that simulates sand falling — digital sand, of course.

Buzzer: A simple tone that lets you know when your 25-minute session (or 5-minute break) is up. Sometimes, that little beep is all you need to shake you out of a deep scroll session on social media.
### Code:

// A pomodoro timer Arduino 
#include "Arduino.h"
#include <MPU6050_tockn.h>
#include "LedControl.h"
#include "Delay.h"

// Matrix definitions
#define MATRIX_A  1
#define MATRIX_B  0

// Pin definitions
#define PIN_DATAIN 5
#define PIN_CLK 4                          
#define PIN_LOAD 6
#define PIN_BUZZER 12

// MPU6050 thresholds
#define ACC_THRESHOLD_LOW -25
#define ACC_THRESHOLD_HIGH 25

#define POMODORO_WORK_SECONDS 1500    // 25 minutes
#define POMODORO_BREAK_SECONDS 300    // 5 minutes
#define POMODORO_LONG_BREAK_SECONDS 900 // 15 minutes

#define ROTATION_OFFSET 90
#define DELAY_FRAME 100

// Global objects and variables
MPU6050 mpu6050(Wire);
LedControl lc = LedControl(PIN_DATAIN, PIN_CLK, PIN_LOAD, 2);
NonBlockDelay d;

int gravity;
int pomodoroCount = 0;
bool isWorkTime = true;
bool alarmWentOff = false;
int currentSeconds = 0;

// Particle movement structure
coord getDown(int x, int y) {
  coord xy;
  xy.x = x-1;
  xy.y = y+1;
  return xy;
}

coord getLeft(int x, int y) {
  coord xy;
  xy.x = x-1;
  xy.y = y;
  return xy;
}

coord getRight(int x, int y) {
  coord xy;
  xy.x = x;
  xy.y = y+1;
  return xy;
}

// Particle movement checks
bool canGoLeft(int addr, int x, int y) {
  if (x == 0) return false;
  return !lc.getXY(addr, getLeft(x, y));
}

bool canGoRight(int addr, int x, int y) {
  if (y == 7) return false;
  return !lc.getXY(addr, getRight(x, y));
}

bool canGoDown(int addr, int x, int y) {
  if (y == 7) return false;
  if (x == 0) return false;
  if (!canGoLeft(addr, x, y)) return false;
  if (!canGoRight(addr, x, y)) return false;
  return !lc.getXY(addr, getDown(x, y));
}

// Particle movement actions
void goDown(int addr, int x, int y) {
  lc.setXY(addr, x, y, false);
  lc.setXY(addr, getDown(x,y), true);
}

void goLeft(int addr, int x, int y) {
  lc.setXY(addr, x, y, false);
  lc.setXY(addr, getLeft(x,y), true);
}

void goRight(int addr, int x, int y) {
  lc.setXY(addr, x, y, false);
  lc.setXY(addr, getRight(x,y), true);
}

// Count particles in a matrix
int countParticles(int addr) {
  int c = 0;
  for (byte y=0; y<8; y++) {
    for (byte x=0; x<8; x++) {
      if (lc.getXY(addr, x, y)) {
        c++;
      }
    }
  }
  return c;
}

// Move a single particle
bool moveParticle(int addr, int x, int y) {
  if (!lc.getXY(addr,x,y)) {
    return false;
  }

  bool can_GoLeft = canGoLeft(addr, x, y);
  bool can_GoRight = canGoRight(addr, x, y);

  if (!can_GoLeft && !can_GoRight) {
    return false;
  }

  bool can_GoDown = canGoDown(addr, x, y);

  if (can_GoDown) {
    goDown(addr, x, y);
  } else if (can_GoLeft && !can_GoRight) {
    goLeft(addr, x, y);
  } else if (can_GoRight && !can_GoLeft) {
    goRight(addr, x, y);
  } else if (random(2) == 1) {
    goLeft(addr, x, y);
  } else {
    goRight(addr, x, y);
  }
  return true;
}

// Fill matrix with particles
void fill(int addr, int maxcount) {
  int n = 8;
  byte x,y;
  int count = 0;
  for (byte slice = 0; slice < 2*n-1; ++slice) {
    byte z = slice<n ? 0 : slice-n + 1;
    for (byte j = z; j <= slice-z; ++j) {
      y = 7-j;
      x = (slice-j);
      lc.setXY(addr, x, y, (++count <= maxcount));
    }
    if (slice % 3 == 0) {
      delay(10);
    }
  }
}

// Get gravity orientation
int getGravity() {
  int x = mpu6050.getAngleX();
  int y = mpu6050.getAngleY();
  
  if (y < -50) { return 0; }    // Upright
  if (y > 20)  { return 180; }  // Upside down
  if (x > ACC_THRESHOLD_HIGH) { return 90; }   
  if (x < ACC_THRESHOLD_LOW)  { return 270; }
  return 0; // Default return value
}

int getTopMatrix() {
  int g = getGravity();
  if (g == 0) return MATRIX_A;
  if (g == 180) return MATRIX_B;
  return (g == 90) ? MATRIX_A : MATRIX_B;
}

int getBottomMatrix() {
  int g = getGravity();
  if (g == 0) return MATRIX_B;
  if (g == 180) return MATRIX_A;
  return (g != 90) ? MATRIX_A : MATRIX_B;
}

// Get delay between particle drops
long getDelayDrop() {
  if (currentSeconds == 0) return 1;
  long dropDelay = (currentSeconds * 1000L) / 60;
  
  // Ensure minimum delay of 100ms for smooth animation
  if (dropDelay < 100) dropDelay = 100;
  
  // Return delay in milliseconds (not seconds!)
  return dropDelay;
}

// Alarm patterns
void workCompleteAlarm() {
  Serial.println("WORK SESSION COMPLETE! Time for a break!");
  
  // Cheerful completion pattern
  for (int i = 0; i < 3; i++) {
    tone(PIN_BUZZER, 523, 150);  // C5
    delay(200);
    tone(PIN_BUZZER, 659, 150);  // E5
    delay(200);
    tone(PIN_BUZZER, 784, 150);  // G5
    delay(200);
    tone(PIN_BUZZER, 1047, 300); // C6
    delay(400);
  }
  noTone(PIN_BUZZER);
}

void breakCompleteAlarm() {
  Serial.println("BREAK TIME OVER! Back to work!");
  
  // Urgent pattern
  for (int i = 0; i < 5; i++) {
    tone(PIN_BUZZER, 1000, 100);
    delay(150);
    tone(PIN_BUZZER, 500, 100);
    delay(150);
  }
  for (int i = 0; i < 3; i++) {
    tone(PIN_BUZZER, 1500, 300);
    delay(400);
  }
  noTone(PIN_BUZZER);
}

// This is just a test block,, might be useful as i was running on Matrix displaying issues
void testMatrices() {
  Serial.println("Testing Matrix A (addr 1)...");
  for(int row = 0; row < 8; row++) {
    lc.setRow(MATRIX_A, row, B11111111);
    delay(100);
  }
  delay(500);
  lc.clearDisplay(MATRIX_A);
  
  Serial.println("Testing Matrix B (addr 0)...");
  for(int row = 0; row < 8; row++) {
    lc.setRow(MATRIX_B, row, B11111111);
    delay(100);
  }
  delay(500);
  lc.clearDisplay(MATRIX_B);
  
  Serial.println("Testing both matrices simultaneously...");
  for(int i = 0; i < 8; i++) {
    lc.setRow(MATRIX_A, i, B10101010);
    lc.setRow(MATRIX_B, i, B01010101);
  }
  delay(1000);
  lc.clearDisplay(MATRIX_A);
  lc.clearDisplay(MATRIX_B);
}

// Reset timer based on orientation
void resetTime() {
  for (byte i=0; i<2; i++) {
    lc.clearDisplay(i);
  }
  
  if (gravity == 0) {  // Upright = Work time
    isWorkTime = true;
    currentSeconds = POMODORO_WORK_SECONDS;
    Serial.println("STARTING WORK SESSION - FOCUS TIME!");
    Serial.print("Duration: ");
    Serial.print(currentSeconds);
    Serial.println(" seconds");
  } else if (gravity == 180) {  // Upside down = Break time
    isWorkTime = false;
    
    if (pomodoroCount % 4 == 0 && pomodoroCount > 0) {
      currentSeconds = POMODORO_LONG_BREAK_SECONDS;
      Serial.print("LONG BREAK TIME! You've completed ");
      Serial.print(pomodoroCount);
      Serial.println(" pomodoros!");
    } else {
      currentSeconds = POMODORO_BREAK_SECONDS;
      Serial.println("SHORT BREAK TIME - RELAX!");
    }
    Serial.print("Duration: ");
    Serial.print(currentSeconds);
    Serial.println(" seconds");
  }
  
  int topMatrix = getTopMatrix();
  Serial.print("Filling top matrix: ");
  Serial.println(topMatrix == MATRIX_A ? "A" : "B");
  
  fill(topMatrix, 60);
  d.Delay(getDelayDrop());
}

// Update all particles in matrices
bool updateMatrix() {
  int n = 8;
  bool somethingMoved = false;
  byte x,y;
  bool direction;
  
  for (byte slice = 0; slice < 2*n-1; ++slice) {
    direction = (random(2) == 1);
    byte z = slice<n ? 0 : slice-n + 1;
    for (byte j = z; j <= slice-z; ++j) {
      y = direction ? (7-j) : (7-(slice-j));
      x = direction ? (slice-j) : j;
      
      if (moveParticle(MATRIX_B, x, y)) {
        somethingMoved = true;
      };
      if (moveParticle(MATRIX_A, x, y)) {
        somethingMoved = true;
      }
    }
  }
  return somethingMoved;
}

// Drop particle between matrices
boolean dropParticle() {
  if (d.Timeout()) {
    d.Delay(getDelayDrop()); 
    if (gravity == 0 || gravity == 180) {
      if ((lc.getRawXY(MATRIX_A, 0, 0) && !lc.getRawXY(MATRIX_B, 7, 7)) ||
          (!lc.getRawXY(MATRIX_A, 0, 0) && lc.getRawXY(MATRIX_B, 7, 7))
      ) {
        lc.invertRawXY(MATRIX_A, 0, 0);
        lc.invertRawXY(MATRIX_B, 7, 7);
        tone(PIN_BUZZER, 440, 10);
        return true;
      }
    }
  }
  return false;
}

void setup() {
  // Initialize Serial first and wait
  Serial.begin(9600);
  while (!Serial) {
    ; // Might be useful too, I was experiencing serial connection problems , and this seems to have fixed it 
  }
  delay(3000);  // Give more time to open Serial Monitor
  
  Serial.println("\n\n=== POMODORO HOURGLASS TIMER ===");
  Serial.println("UPRIGHT: Work time (15 sec test / 25 min production)");
  Serial.println("FLIPPED: Break time (5 sec test / 5 min production)");
  Serial.println("================================\n");
  
  // Initialize LED matrices first (before MPU6050)
  Serial.println("Initializing LED matrices...");
  
  // Initialize each matrix individually with explicit settings
  for (byte i = 0; i < 2; i++) {
    lc.shutdown(i, false);       // Wake up the display
    delay(100);                  // Longer delay between commands
    lc.setIntensity(i, 4);       // REDUCED brightness to save power (was 8)
    delay(100);
    lc.clearDisplay(i);          // Clear display
    delay(100);
    
    Serial.print("Matrix ");
    Serial.print(i);
    Serial.println(" initialized");
  }
  
  // Skip the comprehensive test to save power during startup, but feel free to run it if needed to
  // testMatrices();
  
  // Simple quick test instead
  Serial.println("Quick matrix test...");
  lc.setLed(MATRIX_A, 0, 0, true);
  lc.setLed(MATRIX_B, 7, 7, true);
  delay(500);
  lc.clearDisplay(MATRIX_A);
  lc.clearDisplay(MATRIX_B);
  
  // Startup beep
  tone(PIN_BUZZER, 1000, 200);
  delay(300);
  noTone(PIN_BUZZER);
  
  // Initialize MPU6050 last
  Serial.println("Initializing MPU6050...");
  Wire.begin();
  mpu6050.begin();
  Serial.println("Calibrating gyro (keep still)...");
  mpu6050.calcGyroOffsets(true); 
  Serial.println("MPU6050 ready!");
  
  // Get initial gravity and start
  mpu6050.update();
  gravity = getGravity();
  Serial.print("Initial orientation detected: ");
  Serial.println(gravity);
  
  resetTime();
}

void loop() {
  mpu6050.update();
  delay(DELAY_FRAME);
  
  // Update gravity and rotation
  gravity = getGravity();
  lc.setRotation((ROTATION_OFFSET + gravity) % 360);
  
  // Update particle physics
  bool moved = updateMatrix();
  bool dropped = dropParticle();
  
  // Check if all particles have fallen
  if (!moved && !dropped && !alarmWentOff && (countParticles(getTopMatrix()) == 0)) {
    alarmWentOff = true;
    
    if (isWorkTime) {
      workCompleteAlarm();
      pomodoroCount++;
      Serial.print("Work session completed! Total pomodoros: ");
      Serial.println(pomodoroCount);
      
      // Flash LEDs with lower intensity
      for (int i = 0; i < 3; i++) {
        lc.setIntensity(0, 8);  // Reduced from 15
        lc.setIntensity(1, 8);  // Reduced from 15
        delay(200);
        lc.setIntensity(0, 0);
        lc.setIntensity(1, 0);
        delay(200);
      }
      lc.setIntensity(0, 4);  // Back to low brightness
      lc.setIntensity(1, 4);
    } else {
      breakCompleteAlarm();
      Serial.println("Break completed! Flip to start working.");
    }
  }
  
  // Reset when flipped
  if (dropped || (countParticles(getTopMatrix()) > 0)) {
    static int lastGravity = gravity;
    if (lastGravity != gravity && (gravity == 0 || gravity == 180)) {
      Serial.println("Hourglass flipped - resetting timer");
      resetTime();
      lastGravity = gravity;
    }
    alarmWentOff = false;
    noTone(PIN_BUZZER);
  }
  
  // This section was only necessary for my debugging ,, does not affect the flow of the program, can be eliminated or commented out if needed to
  static unsigned long lastDebugTime = 0;
  if (millis() - lastDebugTime > 3000) {
    Serial.print("Top matrix (");
    Serial.print(getTopMatrix() == MATRIX_A ? "A" : "B");
    Serial.print("): ");
    Serial.print(countParticles(getTopMatrix()));
    Serial.print(" particles | Bottom matrix (");
    Serial.print(getBottomMatrix() == MATRIX_A ? "A" : "B");
    Serial.print("): ");
    Serial.print(countParticles(getBottomMatrix()));
    Serial.println(" particles");
    
    // Additional debug for Matrix B
    Serial.print("Matrix B direct test - Row 0: ");
    for(int col = 0; col < 8; col++) {
      Serial.print(lc.getXY(MATRIX_B, 0, col) ? "1" : "0");
    }
    Serial.println();
    
    lastDebugTime = millis();
  }
}