Pomodoro Focus Timer
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.
// 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.