wong_o
Published © GPL3+

OLED Pomodoro Study Timer

This is a study timer powered by Arduino IDE. It features a 0.96" OLED screen, rotary encoder, and a custom 3D-printed enclosure.

IntermediateFull instructions provided179
OLED Pomodoro Study Timer

Things used in this project

Hardware components

Arduino Nano R3
Arduino Nano R3
×1
Rotary Encoder with Push-Button
Rotary Encoder with Push-Button
×1
SSD1315 0.96 Inch OLED Module
×1
Male/Female Jumper Wires
Male/Female Jumper Wires
×9
M7 x 0.75x2 mm Hex Nut
×1

Software apps and online services

Arduino IDE
Arduino IDE
Tinkercad
Autodesk Tinkercad

Hand tools and fabrication machines

3D Printer (generic)
3D Printer (generic)

Story

Read more

Custom parts and enclosures

Pomodoro Timer Enclosure

Schematics

Pomodoro Timer Circuit Diagram

Code

Pomodoro Timer Software

C/C++
    //Pomodoro Timer Software

    //Created: 6/5/26
    //Author: wong_o

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


#define SCREEN_WIDTH 128     //OLED display width, in pixels
#define SCREEN_HEIGHT 64     //OLED display height, in pixels
#define OLED_RESET -1        //Reset pin for OLED (no reset in this program)
#define SCREEN_ADDRESS 0x3C  //I2C address for the OLED display


Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);


//Identify encoder pins
#define CLK 2
#define DT 3
#define SW 4


uint8_t minuteCount = 25;  //handles minutes, uses uint8_t to take up less memory
uint8_t secondCount = 1;   //handles seconds


uint8_t lastStateClick;  //compared to CLK to detect encoder turns


//detects button presses
unsigned long lastButtonPress = 0;
uint8_t buttonPushCount = 0;
bool btnDown = false;


bool zeroNotReached = true;  //maintains countdown loop when the countdown has not reached 0 minutes and 0 seconds


const unsigned long interval = 1;  //define the interval (1 ms)

//variables compared to millis() to allow select functions to run at certain intervals
//uses unsigned long to prevent integer overflow
unsigned long previousCountdownMillis = 0;
unsigned long previousTimersetMillis = 0;
unsigned long previousEncoderturnMillis = 0;


void setup() {
  Serial.begin(9600);  //initialize serial monitor

  pinMode(CLK, INPUT);
  pinMode(DT, INPUT);
  pinMode(SW, INPUT_PULLUP);

  //reads the initial state of CLK
  lastStateClick = digitalRead(CLK);

  //OLED failure check
  if (!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
    Serial.println(F("SSD1306 allocation failed"));
    for (;;)
      ;
  }

  //display title screen
  display.setTextSize(2);
  display.setCursor(2, 1);
  display.setTextColor(WHITE);
  display.clearDisplay();
  display.print("Il timer apomodoro  By wong_o"); //spaces included to format text on OLED screen
  display.display();

  delay(3000);
}




void loop() {

  while (zeroNotReached) {

    unsigned long currentMillis = millis();
    if (currentMillis - previousCountdownMillis >= 1000) {
      previousCountdownMillis = currentMillis;
      //the “>= 1000” in the comparison between currentMillis and previousCountdownMillis allows the countdown to decrease
      //only after an interval of 1000 milliseconds, or 1 second
      //one millisecond is 1/1000th of a second

      if (secondCount == 0 and minuteCount == 0) {
        zeroNotReached = false;
      } else if (secondCount == 0 and minuteCount > 0) {
        secondCount = 59;
        minuteCount -= 1;
      } else {
        secondCount--;
      }
      CountAdjust();
    }


    //outside of the interval, this button press detection code runs in an interval of 1 millisecond
    //this is to ensure button presses are detected without delay
    if (digitalRead(SW) == LOW) {
      if (!btnDown && millis() - lastButtonPress > 50) {
        buttonPushCount++;  // increase buttonPushCount by 1
        Serial.println("button pressed, while loop active");
        Serial.println(buttonPushCount);
        zeroNotReached = false;  //leave the while loop
        lastButtonPress = millis();
      }
      btnDown = true;
    } else {
      btnDown = false;
    }
  }

  unsigned long currentMillis = millis();

  //detect button press, repeats each 1 ms
  if (digitalRead(SW) == LOW) {
    if (!btnDown && millis() - lastButtonPress > 50) {
      buttonPushCount++;
      Serial.println("button pressed, while loop inactive");
      Serial.println(buttonPushCount);
      lastButtonPress = millis();
    }
    btnDown = true;
  } else {
    btnDown = false;
  }

  if (buttonPushCount == 0) {
    if (currentMillis - previousTimersetMillis >= 100) {  //this interval of 100 ms, or 1/10th of a second, is to protect the OLED screen from pixel burnout
      previousTimersetMillis = currentMillis;
      display.clearDisplay();
      display.setTextSize(2);
      display.setCursor(2, 2);
      display.print("Study time complete!");
      display.display();
    }
  }

  if (buttonPushCount == 1) {
    if (currentMillis - previousTimersetMillis >= 100) {
      previousTimersetMillis = currentMillis;
      display.clearDisplay();
      display.setTextSize(2);
      display.setCursor(2, 2);
      display.print("  Paused             Press to  set time");
      display.display();
    }
  }

  if (buttonPushCount == 2) {
    minuteTurnSensing();  //call interrupt minuteTurnSensing()
  }

  if (buttonPushCount == 3) {
    secondTurnSensing();  //call interrupt secondTurnSensing()
  }

  if (buttonPushCount == 4) {
    if (currentMillis - previousTimersetMillis >= 100) {
      previousTimersetMillis = currentMillis;
      display.clearDisplay();
      display.setTextSize(2);
      display.setCursor(2, 2);
      display.print("  Paused             Press to  resume");
      display.display();
    }
  }

  //if statement to return user to while loop
  if (buttonPushCount == 5) {
    if (currentMillis - previousTimersetMillis >= 250) {
      display.clearDisplay();
      display.setTextSize(2);
      display.setCursor(2, 2);
      display.print("  Paused             Press to  resume");
      display.display();
    }
  }

  if (buttonPushCount >= 5) {
    zeroNotReached = true;
    buttonPushCount = 0;
    secondCount += 1;
  }
}

// display minutes and seconds during time setting and countdown
void CountAdjust() {
  display.clearDisplay();
  display.setTextSize(4);
  display.setCursor(5, 22);
  display.print(minuteCount);
  display.print(":");
  display.print(secondCount);
  display.display();
}




void minuteTurnSensing() {
  unsigned long currentMillis = millis();
  if (currentMillis - previousEncoderturnMillis >= 2) {
    previousEncoderturnMillis = currentMillis;

    //read current state of CLK
    uint8_t currentStateClick = digitalRead(CLK);

    //if the current state of CLK is different than the previous state of CLK, a change has occurred
    if (currentStateClick != lastStateClick && currentStateClick == 1) {

      //if the DT state is different from the CLK state, then a turn occurred
      if (digitalRead(DT) != currentStateClick) {  //if encoder turns to the right
        if (minuteCount > 0) {
          minuteCount--;
        }

      } else { //if encoder turns to the left
        minuteCount++;
      }

      //call the formatting
      CountAdjust();
    }
    //remember the last CLK state
    //if nothing occurs, this prevents it from increasing minuteCount forever
    lastStateClick = currentStateClick;
  }
}


void secondTurnSensing() {
  unsigned long currentMillis = millis();
  if (currentMillis - previousEncoderturnMillis >= 2) {
    previousEncoderturnMillis = currentMillis;


    //read current state of CLK
    uint8_t currentStateClick = digitalRead(CLK);

    //  //if the current state of CLK is different than the previous state of CLK, a change has occurred
    if (currentStateClick != lastStateClick && currentStateClick == 1) {


      //if the DT state is different from the CLK state, then a turn occurred
      if (digitalRead(DT) != currentStateClick) {  //if encoder turns to the right
        if (secondCount > 0) {
          secondCount--;
        }

      } else {  //if encoder turns to the left
        if (secondCount < 59) { //cap to prevent secondCount from being set to values greater than 60 seconds per minute
          secondCount++;
        }
      }

      //call the formatting
      CountAdjust();
    }
    //remember the last CLK state
    //if nothing occurs, this prevents it from increasing secondCount forever
    lastStateClick = currentStateClick;
  }
}

Credits

wong_o
1 project • 0 followers

Comments