FINAL PROJECT
MINUTES
FINAL VIDEO
Below i have explained the making of the both the modules seperately which could be used as a reference .
This productivity timer application will help users manage their time effectively by implementing the Pomodoro Technique. It will feature customizable work and break intervals, task tracking capabilities, and visual/audio notifications. The application will run on multiple platforms and integrate with existing task management systems.The submodule could be used as an addon for the Minutes pomodoro so that people could be aware that you are BUSY .
Feature | Main Module | Submodule (Add-on) | Description |
---|---|---|---|
Clock | ✔ | Always-on time display | |
Pomodoro Timer | ✔ | Customizable work/break intervals | |
Task Manager | ✔ | Add, track, and complete tasks | |
Alarm | ✔ | Audio/visual reminders | |
MP3 Player (AUX) | ✔ | Play music/audio files | |
Status Display | ✔ | Show current status to others | |
Integration | ✔ | ✔ | Syncs status and tasks across modules |
Customization | ✔ | ✔ | User-defined settings and messages |
Below i have listed the sources that helped me in the project for reference .
These are my componennts list that i have used .
Bill of Materials (BOM) Chart
Product | Cost (Rs) | Purchase Links | Supplier |
---|---|---|---|
Xiao ESP32 C6 | 555*2 | ROBU | FAB INVENTORY |
Rotary Encoder | 39 | ROBU | FAB INVENTORY |
Buzzer | 36 | ROBU | FAB INVENTORY |
E-ink Display | 1,899 | AMAZON | AMAZON |
OLED Display | 224 | ROBU | FAB INVENTORY |
LiPo Battery | 409 | ROBU | FAB INVENTORY |
MP3 Module | 112 | ROBU | ROBU |
Pla filament | 1300/KG | ROBU | FAB INVENTORY |
CAPACITOR | 1 | Robu | FAB INVENTORY |
4 PIN JST CONNECTOR | 1 *3 | ktron | FAB INVENTORY |
3 PIN JST CONNECTOR | 1 | ktron | FAB INVENTORY |
NEOPIXEL STRIP | 189 | HUBTRONICS | FAB INVENTORY |
Total | 4023 | ||
### COMPONENTS PLACEMENT |
In the below images i have shown how to integrate . To know more go to Project development page
TIP
FOLLOW THE STEPS MENTIONED IN THE IMAGE FOR A SMOOTH NAVIGATION EXPERIENCE
MINUTES TAG MODULE
This is the CAD Model that I made
Below I have attached the schematic that I used to design the small PCB for the display module that could be kept as an indicator.
Then I routed the PCB design so I could mill the PCB.
Then I checked the 3D view to check the alignment and I decided where to keep my OLED display.
Then I placed a hole in a way that my display module is in the center so it suits the circular design that I am going to make. For that, I used Fusion and exported the DXF with the OLED placing and aligned the through hole accordingly.
Then i have made the Pcb and soldered the componennts then i noticed there was an error (that i have forgotted about 2 via holes for the battery connection module . so for that my instructor saheen suggested an idea to vinyl print and took the connection out ) so we did that .
for that we have made jig for the pcb using cardboard .
Then i have pasted vinyl on back of pcb as shown below .
Then to engrave the traces i have used xtools .
Then i have removed the excess vinyl as shown below
This is how it looks after the removal
Then i have used uv glue to stick the vinyl .
since it was a uv glue , i applied some uv light with the help of uv torch which helped me in faster hardening of glue.
This week i have 3d printed some parts of my case .
Below i have shown the smoked acrylic that i have used as part of my display . its smoked so it is not opaque and is visible with a good looking effect .
Below i have attached the image for the laser cutted part .
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Fonts/FreeMono9pt7b.h>
#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels
// Declaration for SSD1306 display connected using I2C (SDA, SCL pins)
#define OLED_RESET -1 // Reset pin # (or -1 if sharing Arduino reset pin)
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
void setup() {
Serial.begin(115200); // Initialize Serial for debugging
// initialize with the I2C addr 0x3C (for most OLEDs)
if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.println(F("SSD1306 allocation failed"));
for(;;); // Don't proceed, loop forever
}
display.clearDisplay();
display.drawRect(10, 9, 106, 44, SSD1306_WHITE);
display.setTextColor(SSD1306_WHITE);
display.setTextWrap(false);
display.setFont(&FreeMono9pt7b);
display.setCursor(39, 29);
display.print("IN A");
display.setCursor(25, 43);
display.print("MEETING");
display.display();
}
void loop() {
// Nothing needed here for static display
}``
MINUTES PRODUCTIVITY TIMER MAIN MODULE
These are the buttons that i have 3d printed for the Minutes Module .
Below i have attached the 3d printed front face of the Module .
here i am checking the button with the front face .
This is the final back body that i have 3d printed
The final Assembly viw of the product from inside .
A more closeup shot . here you could see how i have placed the mp3 module . The mp3 module is placed on a 3D printed sliding mechanism where i screwed the module and the 3d print .
The below image shows the back cover which i have laser cutted .
This is the final photo of Minutes Module After Assembly .
#define pushButton D0
// ntp server code
#include <WiFi.h>
#include "time.h"
#include <Arduino.h>
#include <RotaryEncoder.h>
#define PIN_IN1 D2
#define PIN_IN2 D3
#define ROTARYSTEPS 2
#define ROTARYMIN 0
#define ROTARYMAX 16
RotaryEncoder encoder(PIN_IN1, PIN_IN2, RotaryEncoder::LatchMode::TWO03);
int lastPos = -1;
// Replace these with your Wi-Fi credentials
const char* ssid = "FabGuest";
const char* password = "4fabGUEST";
// NTP server and time zone settings (IST for Kochi, Kerala, India)
const char* ntpServer = "pool.ntp.org";
const long gmtOffset_sec = 19800; // GMT+5:30 = 5*3600 + 30*60 = 19800 seconds
const int daylightOffset_sec = 0;
int HOUR, MINUTE, SECOND;
// display code
#include <GxEPD2_BW.h>
#include <Fonts/FreeMonoBold24pt7b.h>
#include <SPI.h>
// Pin definitions
#define EPD_SCK_PIN 19
#define EPD_MOSI_PIN 18
#define EINK_CS 17
#define EINK_RST 23
#define EINK_DC 16
#define EINK_BUSY 22
// Instantiate display class for 2.7" GDEY027T91 (V2)
GxEPD2_BW<GxEPD2_270_GDEY027T91, GxEPD2_270_GDEY027T91::HEIGHT>
display(GxEPD2_270_GDEY027T91(EINK_CS, EINK_DC, EINK_RST, EINK_BUSY));
SPIClass spi = SPIClass(SPI); // Use VSPI for custom pins
void setup() {
pinMode(pushButton, INPUT);
Serial.begin(115200);
encoder.setPosition(10 / ROTARYSTEPS); // start with the value of 10.
// Initialize SPI with your custom pins
spi.begin(EPD_SCK_PIN, -1, EPD_MOSI_PIN, EINK_CS);
// Link SPI to display
display.epd2.selectSPI(spi, SPISettings(4000000, MSBFIRST, SPI_MODE0));
display.init(115200);
display.setRotation(1);
display.setFont(&FreeMonoBold24pt7b);
display.setTextColor(GxEPD_BLACK);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("\nWiFi connected.");
// Initialize and get the time from NTP
configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
}
int PrevMin;
void loop() {
int buttonState = digitalRead(pushButton);
Serial.println(buttonState);
delay(1);
static char timeStr[9]; // "HH:MM:SS" + null terminator
struct tm timeinfo;
if (getLocalTime(&timeinfo)) {
HOUR = timeinfo.tm_hour;
MINUTE = timeinfo.tm_min;
SECOND = timeinfo.tm_sec;
} else {
Serial.println("Failed to obtain time");
}
if (PrevMin != SECOND) {
display.setPartialWindow(19, 50, 223, 31);
display.firstPage();
do {
sprintf(timeStr, "%02d:%02d:%02d", HOUR, MINUTE, SECOND);
display.fillRect(19, 50, 223, 31, GxEPD_WHITE);
display.setCursor(19, 78);
display.print(timeStr);
} while (display.nextPage());
display.hibernate();
PrevMin = SECOND;
}
encoder.tick();
// get the current physical position and calc the logical position
int newPos = encoder.getPosition() * ROTARYSTEPS;
if (newPos < ROTARYMIN) {
encoder.setPosition(ROTARYMIN / ROTARYSTEPS);
newPos = ROTARYMIN;
} else if (newPos > ROTARYMAX) {
encoder.setPosition(ROTARYMAX / ROTARYSTEPS);
newPos = ROTARYMAX;
} // if
if (lastPos != newPos) {
Serial.print(newPos);
Serial.println();
lastPos = newPos;
} // if
}
#include <WiFi.h>
#include <WiFiUdp.h>
#include <NTPClient.h>
#include <GxEPD2_BW.h>
#include <Adafruit_NeoPixel.h>
#include <AiEsp32RotaryEncoder.h>
#include <SPI.h>
// ==== Pin Definitions (Adjusted to Reference) ====
#define ENCODER_CLK 4 // D2
#define ENCODER_DT 0 // D3
#define ENCODER_SW 16 // D0 (pushButton)
#define NEOPIXEL_PIN 5 // You can change if needed
#define BUZZER_PIN 21 // You can change if needed
#define EPD_SCK_PIN 19
#define EPD_MOSI_PIN 18
#define EPD_CS 17
#define EPD_DC 16
#define EPD_RST 23
#define EPD_BUSY 22
#define ROTARY_ENCODER_STEPS 2 // Adjust to your encoder type
// ==== Display Setup ====
GxEPD2_BW<GxEPD2_270_GDEY027T91, GxEPD2_270_GDEY027T91::HEIGHT>
display(GxEPD2_270_GDEY027T91(EPD_CS, EPD_DC, EPD_RST, EPD_BUSY));
SPIClass spi = SPIClass(VSPI); // Use VSPI for custom pins
// ==== NeoPixel Setup ====
Adafruit_NeoPixel pixels(1, NEOPIXEL_PIN, NEO_GRB + NEO_KHZ800);
// ==== Rotary Encoder Setup ====
AiEsp32RotaryEncoder rotaryEncoder(ENCODER_DT, ENCODER_CLK, ENCODER_SW, -1, ROTARY_ENCODER_STEPS);
// ==== WiFi and Time Setup ====
const char* ssid = "FabGuest";
const char* password = "4fabGUEST";
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, "pool.ntp.org", 19800, 60000); // IST offset
// ==== Modes ====
enum AppMode {CLOCK, POMODORO, TASKS, ALARM};
AppMode currentMode = CLOCK;
const int numModes = 4;
// ==== Pomodoro ====
unsigned long pomodoroStartTime = 0;
bool pomodoroActive = false;
const unsigned long pomodoroDuration = 25 * 60 * 1000UL; // 25 min in ms
// ==== Display Throttle ====
unsigned long lastDisplayUpdate = 0;
const unsigned long displayInterval = 1000; // ms
// ==== Rotary Encoder ISR Handler ====
void IRAM_ATTR readEncoderISR() {
rotaryEncoder.readEncoder_ISR();
}
// ==== Setup ====
void setup() {
Serial.begin(115200);
// --- Display SPI Init ---
spi.begin(EPD_SCK_PIN, -1, EPD_MOSI_PIN, EPD_CS);
display.epd2.selectSPI(spi, SPISettings(4000000, MSBFIRST, SPI_MODE0));
display.init(115200);
display.setRotation(1);
display.setTextColor(GxEPD_BLACK);
// --- NeoPixel ---
pixels.begin();
pixels.setPixelColor(0, 0, 0, 0);
pixels.show();
// --- Buzzer ---
pinMode(BUZZER_PIN, OUTPUT);
digitalWrite(BUZZER_PIN, LOW);
// --- Rotary Encoder ---
rotaryEncoder.begin();
rotaryEncoder.setup(readEncoderISR);
rotaryEncoder.setBoundaries(0, numModes - 1, false); // 0 to 3
rotaryEncoder.setAcceleration(0); // No acceleration
pinMode(ENCODER_SW, INPUT_PULLUP);
// --- WiFi ---
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("\nWiFi connected.");
// --- NTP Time ---
timeClient.begin();
displaySplashScreen();
updateDisplay();
}
// ==== Main Loop ====
void loop() {
rotaryEncoder.loop(); // Must be called regularly
// Handle rotation
static int lastEncoderPos = 0;
int encoderPos = rotaryEncoder.getEncoderValue();
if (encoderPos != lastEncoderPos) {
currentMode = static_cast<AppMode>(encoderPos);
updateDisplay();
lastEncoderPos = encoderPos;
}
// Handle button press (debounced)
if (rotaryEncoder.isEncoderButtonClicked()) {
handleModeAction();
}
// Pomodoro logic
handlePomodoro();
// Throttle e-ink updates (for Pomodoro countdown)
if (currentMode == POMODORO && pomodoroActive && millis() - lastDisplayUpdate > displayInterval) {
updateDisplay();
lastDisplayUpdate = millis();
}
}
// ==== Mode Actions ====
void handleModeAction() {
switch (currentMode) {
case POMODORO:
togglePomodoro();
break;
case CLOCK:
refreshClock();
break;
case TASKS:
// Add task management logic here
break;
case ALARM:
// Add alarm management logic here
break;
}
}
void togglePomodoro() {
if (!pomodoroActive) {
startPomodoro();
} else {
stopPomodoro();
}
}
void startPomodoro() {
pomodoroStartTime = millis();
pomodoroActive = true;
pixels.setPixelColor(0, pixels.Color(255, 0, 0)); // Red
pixels.show();
updateDisplay();
}
void stopPomodoro() {
pomodoroActive = false;
pomodoroStartTime = 0;
pixels.setPixelColor(0, pixels.Color(0, 255, 0)); // Green
pixels.show();
triggerBuzzer();
updateDisplay();
}
void handlePomodoro() {
if (pomodoroActive && (millis() - pomodoroStartTime >= pomodoroDuration)) {
stopPomodoro();
}
}
// ==== Display Functions ====
void updateDisplay() {
display.firstPage();
do {
switch (currentMode) {
case CLOCK: displayClock(); break;
case POMODORO: displayPomodoro(); break;
case TASKS: displayTasks(); break;
case ALARM: displayAlarms(); break;
}
} while (display.nextPage());
}
void displayClock() {
timeClient.update();
display.setCursor(0, 40);
display.setTextSize(2);
display.print("Time: ");
display.print(timeClient.getFormattedTime());
}
void displayPomodoro() {
unsigned long remaining = pomodoroActive ?
pomodoroDuration - (millis() - pomodoroStartTime) : pomodoroDuration;
int minutes = remaining / 60000;
int seconds = (remaining % 60000) / 1000;
display.setCursor(0, 40);
display.setTextSize(2);
display.printf("Pomodoro\n%02d:%02d", minutes, seconds);
}
void displayTasks() {
display.setCursor(0, 40);
display.setTextSize(2);
display.print("Tasks Feature");
}
void displayAlarms() {
display.setCursor(0, 40);
display.setTextSize(2);
display.print("Alarms Feature");
}
void displaySplashScreen() {
display.firstPage();
do {
display.setCursor(0, 40);
display.setTextSize(2);
display.print("Productivity Timer");
display.setCursor(0, 80);
display.setTextSize(1);
display.print("Initializing...");
} while (display.nextPage());
delay(2000);
}
// ==== Buzzer Function ====
void triggerBuzzer() {
for (int i = 0; i < 3; i++) {
digitalWrite(BUZZER_PIN, HIGH);
delay(100);
digitalWrite(BUZZER_PIN, LOW);
delay(100);
}
}
// ==== Helper ====
void refreshClock() {
updateDisplay();
}
WORKING
In the below video You could see the different functionalities that this module has as of now . The app should be integrated to this as of now its not working . see more
This is the video of Pomodoro timer , where i am making using of it and working on my daily schedule .
This is display tag video , where it shows the current status , currently its not integrated with the main module . I will be working on it in future so that my ideas is to add some prebuilt writingslike busy , donotdisturb , in a meeting and save in the main module and when choosed the tag displays those and with the help of espnow communicate with the two modules .
The implication of this project depends upon who the user actually is . It may sound unreal yah but its the fact ie if you are lacking some productivity issues or is trying to build a habit , this may become handy . As in the book Atomic habits you should start small , like organizing the things and working on it atleast 1 min (start it) , rather than keep on telling i would do it later .
On those aspects this would be helping someone getting into those habits of tsarting something and maintain a timer how long he has focused , this helps to build a habit along it which would help in the future where he no longer requires the device ,(it becomes a routine ) .
When evaluated on the aspects mentioned above its very good to build a routine .
FILES
Minutes a Pomodoro Timer © 2025 by Abin Mathew is licensed under CC BY-NC-SA 4.0