FluxBench.AIShared project · read-only

Pomodoro Focus Timer

ESP32 DevKit (WROOM-32)·Arduino C++·Updated Jul 27, 2026·by FluxBench TeamIncludes Lab + Schematic

A productivity timer that leans on the DS3231’s real hardware alarm registers instead of millis() math: setAlarm1() arms alarm 1, the sketch polls the latched flag with rtc.alarmFired(1) and resets it with clearAlarm(1) — the exact battery-backed pattern used on real hardware. Press the bench button (GPIO 14) to start a 25-minute focus block (demo speed: 1 s = 1 min, so it rings after 25 s with three buzzer beeps on GPIO 4), then a 5-minute break follows automatically and the cycle repeats; press the button mid-phase to skip ahead. The OLED shows the phase, a live countdown from the RTC clock and a tally of completed focus blocks, and the green LED on GPIO 26 is lit while you should be focusing.

sketch.ino
// Pomodoro Focus Timer - driven by REAL DS3231 hardware alarms
// ESP32 DevKit v1. DS3231 @0x68 + SSD1306 @0x3C on I2C (SDA=21, SCL=22),
// piezo buzzer on GPIO 4, start/skip button on GPIO 14, focus LED on GPIO 26.
//
// Instead of counting millis(), this sketch programs the DS3231's alarm 1
// register with setAlarm1(), polls the latched flag with rtc.alarmFired(1)
// and resets it with clearAlarm(1) - the exact pattern you would use on
// battery-backed hardware, where the alarm keeps working through resets.
// Demo speed: 1 second = 1 "minute", so a 25-minute focus block rings 25 s
// after you press the button, then a 5-minute break, and so on. Press the
// button mid-phase to skip ahead.

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

#define BUZZER_PIN 4
#define BUTTON_PIN 14
#define LED_PIN 26

// Demo speed: these "minutes" are armed as seconds (see armAlarm)
#define FOCUS_MIN 25
#define BREAK_MIN 5

RTC_DS3231 rtc;
Adafruit_SSD1306 display(128, 64, &Wire, -1);

int mode = 0;              // 0 = ready, 1 = focus, 2 = break
int sessions = 0;          // completed focus blocks
long phaseEnd = 0;         // unix seconds when the current phase ends
int lastBtn = 1;
unsigned long lastPress = 0;
int beepsLeft = 0;         // on/off beep half-cycles still to play
unsigned long nextBeep = 0;
int beepOn = 0;

void armAlarm(int minutes) {
  DateTime now = rtc.now();
  // demo speed: arm the alarm 'minutes' SECONDS ahead; on real hardware use
  // TimeSpan(0, 0, minutes, 0) for true minutes
  DateTime target = now + TimeSpan(0, 0, 0, minutes);
  rtc.clearAlarm(1);
  rtc.setAlarm1(target, DS3231_A1_Date);   // one-shot: full date + time match
  phaseEnd = target.unixtime();
}

void startFocus() {
  mode = 1;
  digitalWrite(LED_PIN, HIGH);
  armAlarm(FOCUS_MIN);
}

void startBreak() {
  mode = 2;
  sessions = sessions + 1;
  digitalWrite(LED_PIN, LOW);
  armAlarm(BREAK_MIN);
}

void ringAndAdvance() {
  rtc.clearAlarm(1);          // reset the latched alarm flag
  beepsLeft = 6;              // three short beeps
  nextBeep = millis();
  beepOn = 0;
  if (mode == 1) startBreak();
  else startFocus();
}

void drawScreen() {
  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);
  display.setTextSize(2);
  display.setCursor(0, 0);
  if (mode == 1) display.println("FOCUS");
  else if (mode == 2) display.println("BREAK");
  else display.println("READY");
  display.setTextSize(1);
  display.setCursor(98, 4);
  display.print("#");
  display.print(sessions);

  if (mode == 0) {
    display.setCursor(0, 30);
    display.println("Press the button to");
    display.println("start a 25 min focus");
    display.println("block.");
  } else {
    DateTime nowDt = rtc.now();
    long left = phaseEnd - nowDt.unixtime();
    if (left < 0) left = 0;
    display.setTextSize(3);
    display.setCursor(24, 30);
    display.print(left);
    display.setTextSize(1);
    display.setCursor(96, 44);
    display.print("min");
  }
  display.display();
}

void setup() {
  pinMode(BUZZER_PIN, OUTPUT);
  pinMode(LED_PIN, OUTPUT);
  pinMode(BUTTON_PIN, INPUT_PULLUP);
  Serial.begin(115200);
  Wire.begin();
  rtc.begin();
  if (rtc.lostPower()) {
    rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));  // sync clock on first boot
  }
  rtc.disableAlarm(2);
  rtc.clearAlarm(1);
  rtc.writeSqwPinMode(DS3231_OFF);  // free the INT/SQW pin for alarm duty
  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
  display.clearDisplay();
  display.display();
  Serial.println("Pomodoro ready - press the button to start focusing");
}

void loop() {
  // button: start when ready, skip the current phase otherwise
  int btn = digitalRead(BUTTON_PIN);
  if (btn == LOW && lastBtn == HIGH && millis() - lastPress > 200) {
    lastPress = millis();
    if (mode == 0) {
      Serial.println("Focus block started");
      startFocus();
    } else {
      Serial.println("Phase skipped");
      ringAndAdvance();
    }
  }
  lastBtn = btn;

  // poll the DS3231's latched alarm flag (no interrupt wiring needed)
  if (mode != 0 && rtc.alarmFired(1)) {
    DateTime now = rtc.now();
    Serial.print("ALARM fired at ");
    Serial.print(now.hour());
    Serial.print(":");
    Serial.print(now.minute());
    Serial.print(":");
    Serial.println(now.second());
    ringAndAdvance();
  }

  // non-blocking triple beep
  if (beepsLeft > 0 && millis() >= nextBeep) {
    if (beepOn == 0) {
      tone(BUZZER_PIN, 1800);
      beepOn = 1;
    } else {
      noTone(BUZZER_PIN);
      beepOn = 0;
    }
    beepsLeft = beepsLeft - 1;
    nextBeep = millis() + 160;
    if (beepsLeft == 0) noTone(BUZZER_PIN);
  }

  drawScreen();
  delay(40);
}

Built with FluxBench.AI — the AI workbench for embedded projects.