The goal of the project is to create a distraction free tool for managing productivity. While there are many apps and websites that offer similar features, they often come with distractions of their own, such as animations, notifications, ads, or extra features competing for the user’s attention. Productivity Pal keeps the experience simple and physical, using RFID tags and a small screen to access each mode without needing to connect to the web.
#include <Adafruit_GFX.h>
#include <Adafruit_ST7789.h>
#include <SPI.h>
#include <MFRC522.h>
#define TFT_CS 10
#define TFT_DC 8
#define TFT_RST 9
#define RFID_SS 7
#define RFID_RST 2
const int pinA = 3;
const int pinB = 4;
const int pushPin = 5;
const int k0Pin = 6;
int currentMode = 0;
Adafruit_ST7789 tft = Adafruit_ST7789(TFT_CS, TFT_DC, TFT_RST);
MFRC522 rfid(RFID_SS, RFID_RST);
String lastUID = "";
int lastAState;
bool lastButtonState = HIGH;
// Work mode options
int timerOptions[3] = {5, 10, 20};
int selectedIndex = 0;
bool workTimerStarted = false;
unsigned long workStartTime = 0;
unsigned long workDuration = 0;
int lastWorkSecond = -1;
// Break Mode options
String breakOptions[3] = {"Stretch", "Jumping Jacks", "Pushups"};
int breakSelectedIndex = 0;
bool breakTimerStarted = false;
unsigned long breakStartTime = 0;
const unsigned long breakDuration = 2UL * 60UL * 1000UL;
int lastBreakSecond = -1;
// Todo Mode options
const int TODO_COUNT = 5;
const char* todoOptions[TODO_COUNT] = {"Homework", "Work", "Clean", "Chores", "Groceries"};
bool todoSelected[TODO_COUNT] = {false, false, false, false, false};
bool todoCompleted[TODO_COUNT] = {false, false, false, false, false};
int todoSelectedIndex = 0;
bool todoListStarted = false;
// Home screen
void runHomeMode() {
tft.fillScreen(ST77XX_BLACK);
tft.setTextSize(2);
tft.setTextColor(ST77XX_GREEN);
tft.setCursor(20, 20);
tft.println("Home");
tft.setTextSize(1);
tft.setCursor(20, 60);
tft.println("Tap an RFID tag");
tft.setCursor(20, 80);
tft.println("Blue tag -> Work");
tft.setCursor(20, 95);
tft.println("Card -> Break");
tft.setCursor(20, 110);
tft.println("White tag -> Todo");
}
// Work screen
void drawWorkSelectionScreen() {
tft.fillScreen(ST77XX_BLACK);
tft.setTextSize(2);
tft.setTextColor(ST77XX_YELLOW);
tft.setCursor(20, 20);
tft.println("Work Mode");
tft.setTextSize(1);
tft.setTextColor(ST77XX_WHITE);
tft.setCursor(20, 50);
tft.println("Choose timer:");
for (int i = 0; i < 3; i++) {
int y = 80 + i * 25;
if (i == selectedIndex) {
tft.fillRect(20, y - 2, 140, 18, ST77XX_YELLOW);
tft.setTextColor(ST77XX_BLACK);
} else {
tft.fillRect(20, y - 2, 140, 18, ST77XX_BLACK);
tft.setTextColor(ST77XX_WHITE);
}
tft.setTextSize(2);
tft.setCursor(30, y);
tft.print(timerOptions[i]);
tft.print(" min");
}
tft.setTextSize(1);
tft.setTextColor(ST77XX_CYAN);
tft.setCursor(20, 170);
tft.println("Press knob to start");
}
void enterWorkMode() {
workTimerStarted = false;
selectedIndex = 0;
lastWorkSecond = -1;
drawWorkSelectionScreen();
}
void drawWorkTimerScreen() {
tft.fillScreen(ST77XX_BLACK);
tft.setTextSize(2);
tft.setTextColor(ST77XX_YELLOW);
tft.setCursor(20, 20);
tft.println("Work Mode");
tft.setTextSize(1);
tft.setTextColor(ST77XX_WHITE);
tft.setCursor(20, 50);
tft.print("Pomodoro ");
tft.print(timerOptions[selectedIndex]);
tft.println(" min");
}
void startWorkTimer() {
workTimerStarted = true;
workStartTime = millis();
workDuration = (unsigned long)timerOptions[selectedIndex] * 60UL * 1000UL;
lastWorkSecond = -1;
drawWorkTimerScreen();
}
void runWorkMode() {
if (!workTimerStarted) {
int currentAState = digitalRead(pinA);
if (currentAState != lastAState) {
if (digitalRead(pinB) != currentAState) {
selectedIndex++;
} else {
selectedIndex--;
}
if (selectedIndex > 2) selectedIndex = 0;
if (selectedIndex < 0) selectedIndex = 2;
drawWorkSelectionScreen();
delay(5);
}
lastAState = currentAState;
bool currentButtonState = digitalRead(pushPin);
if (currentButtonState == LOW && lastButtonState == HIGH) {
startWorkTimer();
delay(200);
}
lastButtonState = currentButtonState;
} else {
unsigned long elapsed = millis() - workStartTime;
if (elapsed > workDuration) {
elapsed = workDuration;
}
int remainingSeconds = (workDuration - elapsed) / 1000;
int minutes = remainingSeconds / 60;
int seconds = remainingSeconds % 60;
if (remainingSeconds != lastWorkSecond) {
lastWorkSecond = remainingSeconds;
char timeText[6];
sprintf(timeText, "%02d:%02d", minutes, seconds);
tft.fillRect(20, 80, 160, 40, ST77XX_BLACK);
tft.setTextSize(3);
tft.setTextColor(ST77XX_RED);
tft.setCursor(20, 80);
tft.print(timeText);
if (remainingSeconds == 0) {
tft.fillRect(20, 140, 140, 30, ST77XX_BLACK);
tft.setTextSize(2);
tft.setTextColor(ST77XX_GREEN);
tft.setCursor(20, 140);
tft.println("Done");
}
}
}
}
// Break screen
void drawBreakSelectionScreen() {
tft.fillScreen(ST77XX_BLACK);
tft.setTextSize(2);
tft.setTextColor(ST77XX_CYAN);
tft.setCursor(20, 20);
tft.println("Break Mode");
tft.setTextSize(1);
tft.setTextColor(ST77XX_WHITE);
tft.setCursor(20, 50);
tft.println("Choose exercise:");
for (int i = 0; i < 3; i++) {
int y = 80 + i * 25;
if (i == breakSelectedIndex) {
tft.fillRect(20, y - 2, 180, 18, ST77XX_CYAN);
tft.setTextColor(ST77XX_BLACK);
} else {
tft.fillRect(20, y - 2, 180, 18, ST77XX_BLACK);
tft.setTextColor(ST77XX_WHITE);
}
tft.setTextSize(1);
tft.setCursor(30, y);
tft.print(breakOptions[i]);
}
tft.setTextSize(1);
tft.setTextColor(ST77XX_YELLOW);
tft.setCursor(20, 170);
tft.println("Press knob to start");
}
void enterBreakMode() {
breakTimerStarted = false;
breakSelectedIndex = 0;
lastBreakSecond = -1;
drawBreakSelectionScreen();
}
void drawBreakTimerScreen() {
tft.fillScreen(ST77XX_BLACK);
tft.setTextSize(2);
tft.setTextColor(ST77XX_CYAN);
tft.setCursor(20, 20);
tft.println("Break Mode");
tft.setTextSize(1);
tft.setTextColor(ST77XX_WHITE);
tft.setCursor(20, 50);
tft.print("Exercise: ");
tft.println(breakOptions[breakSelectedIndex]);
}
void startBreakTimer() {
breakTimerStarted = true;
breakStartTime = millis();
lastBreakSecond = -1;
drawBreakTimerScreen();
}
void runBreakMode() {
if (!breakTimerStarted) {
int currentAState = digitalRead(pinA);
if (currentAState != lastAState) {
if (digitalRead(pinB) != currentAState) {
breakSelectedIndex++;
} else {
breakSelectedIndex--;
}
if (breakSelectedIndex > 2) breakSelectedIndex = 0;
if (breakSelectedIndex < 0) breakSelectedIndex = 2;
drawBreakSelectionScreen();
delay(5);
}
lastAState = currentAState;
bool currentButtonState = digitalRead(pushPin);
if (currentButtonState == LOW && lastButtonState == HIGH) {
startBreakTimer();
delay(200);
}
lastButtonState = currentButtonState;
} else {
unsigned long elapsed = millis() - breakStartTime;
if (elapsed > breakDuration) {
elapsed = breakDuration;
}
int remainingSeconds = (breakDuration - elapsed) / 1000;
int minutes = remainingSeconds / 60;
int seconds = remainingSeconds % 60;
if (remainingSeconds != lastBreakSecond) {
lastBreakSecond = remainingSeconds;
char timeText[6];
sprintf(timeText, "%02d:%02d", minutes, seconds);
tft.fillRect(20, 85, 160, 40, ST77XX_BLACK);
tft.setTextSize(3);
tft.setTextColor(ST77XX_GREEN);
tft.setCursor(20, 85);
tft.print(timeText);
if (remainingSeconds == 0) {
tft.fillRect(20, 140, 220, 50, ST77XX_BLACK);
tft.setTextSize(2);
tft.setTextColor(ST77XX_YELLOW);
tft.setCursor(20, 140);
tft.println("Congrats on");
tft.setCursor(20, 165);
tft.println("staying active");
}
}
}
}
// Todo screen
int getTodoChosenCount() {
int count = 0;
for (int i = 0; i < TODO_COUNT; i++) {
if (todoSelected[i]) {
count++;
}
}
return count;
}
void drawTodoSelectionScreen() {
tft.fillScreen(ST77XX_BLACK);
tft.setTextSize(2);
tft.setTextColor(ST77XX_MAGENTA);
tft.setCursor(20, 15);
tft.println("Todo Mode");
tft.setTextSize(1);
tft.setTextColor(ST77XX_WHITE);
tft.setCursor(20, 45);
tft.println("Press knob to select");
for (int i = 0; i < TODO_COUNT; i++) {
int y = 70 + i * 25;
if (i == todoSelectedIndex) {
tft.fillRect(20, y - 3, 200, 19, ST77XX_MAGENTA);
tft.setTextColor(ST77XX_BLACK);
} else {
tft.fillRect(20, y - 3, 200, 19, ST77XX_BLACK);
tft.setTextColor(todoSelected[i] ? ST77XX_GREEN : ST77XX_WHITE);
}
tft.setTextSize(1);
tft.setCursor(30, y);
tft.print(todoSelected[i] ? "[x] " : "[ ] ");
tft.print(todoOptions[i]);
}
tft.setTextSize(1);
tft.setTextColor(ST77XX_YELLOW);
tft.setCursor(20, 210);
tft.println("Side button starts list");
}
void drawTodoListScreen() {
tft.fillScreen(ST77XX_BLACK);
tft.setTextSize(2);
tft.setTextColor(ST77XX_MAGENTA);
tft.setCursor(20, 15);
tft.println("Today's List");
tft.setTextSize(1);
tft.setTextColor(ST77XX_WHITE);
tft.setCursor(20, 45);
tft.println("Press knob when done");
int shown = 0;
for (int i = 0; i < TODO_COUNT; i++) {
if (!todoSelected[i]) {
continue;
}
int y = 75 + shown * 28;
if (todoCompleted[i]) {
tft.fillRect(20, y - 4, 220, 21, ST77XX_GREEN);
tft.setTextColor(ST77XX_BLACK);
} else if (i == todoSelectedIndex) {
tft.fillRect(20, y - 4, 220, 21, ST77XX_MAGENTA);
tft.setTextColor(ST77XX_BLACK);
} else {
tft.fillRect(20, y - 4, 220, 21, ST77XX_BLACK);
tft.setTextColor(ST77XX_WHITE);
}
tft.setTextSize(2);
tft.setCursor(30, y);
tft.print(todoOptions[i]);
shown++;
}
if (shown == 0) {
tft.setTextSize(2);
tft.setTextColor(ST77XX_YELLOW);
tft.setCursor(20, 95);
tft.println("No tasks");
tft.setTextSize(1);
tft.setCursor(20, 125);
tft.println("Use side button to go back");
}
}
void moveTodoSelection(int direction) {
if (!todoListStarted) {
todoSelectedIndex += direction;
if (todoSelectedIndex >= TODO_COUNT) todoSelectedIndex = 0;
if (todoSelectedIndex < 0) todoSelectedIndex = TODO_COUNT - 1;
drawTodoSelectionScreen();
return;
}
if (getTodoChosenCount() == 0) {
return;
}
int nextIndex = todoSelectedIndex;
do {
nextIndex += direction;
if (nextIndex >= TODO_COUNT) nextIndex = 0;
if (nextIndex < 0) nextIndex = TODO_COUNT - 1;
} while (!todoSelected[nextIndex]);
todoSelectedIndex = nextIndex;
drawTodoListScreen();
}
void enterTodoMode() {
todoListStarted = false;
todoSelectedIndex = 0;
for (int i = 0; i < TODO_COUNT; i++) {
todoSelected[i] = false;
todoCompleted[i] = false;
}
drawTodoSelectionScreen();
}
void startTodoList() {
todoListStarted = true;
if (!todoSelected[todoSelectedIndex]) {
for (int i = 0; i < TODO_COUNT; i++) {
if (todoSelected[i]) {
todoSelectedIndex = i;
break;
}
}
}
drawTodoListScreen();
}
void handleTodoSideButton() {
if (!todoListStarted) {
startTodoList();
} else {
currentMode = 0;
runHomeMode();
}
}
void runTodoMode() {
int currentAState = digitalRead(pinA);
if (currentAState != lastAState) {
if (digitalRead(pinB) != currentAState) {
moveTodoSelection(1);
} else {
moveTodoSelection(-1);
}
delay(5);
}
lastAState = currentAState;
bool currentButtonState = digitalRead(pushPin);
if (currentButtonState == LOW && lastButtonState == HIGH) {
if (!todoListStarted) {
todoSelected[todoSelectedIndex] = !todoSelected[todoSelectedIndex];
drawTodoSelectionScreen();
} else if (todoSelected[todoSelectedIndex]) {
todoCompleted[todoSelectedIndex] = !todoCompleted[todoSelectedIndex];
drawTodoListScreen();
}
delay(200);
}
lastButtonState = currentButtonState;
}
void setup() {
Serial.begin(9600);
delay(2000);
pinMode(pinA, INPUT_PULLUP);
pinMode(pinB, INPUT_PULLUP);
pinMode(pushPin, INPUT_PULLUP);
pinMode(k0Pin, INPUT_PULLUP);
SPI.begin();
tft.init(240, 320);
tft.invertDisplay(false);
tft.setRotation(3);
rfid.PCD_Init();
lastAState = digitalRead(pinA);
runHomeMode();
}
void loop() {
if (rfid.PICC_IsNewCardPresent() && rfid.PICC_ReadCardSerial()) {
String uidString = "";
for (byte i = 0; i < rfid.uid.size; i++) {
if (rfid.uid.uidByte[i] < 0x10) {
uidString += "0";
}
uidString += String(rfid.uid.uidByte[i], HEX);
if (i < rfid.uid.size - 1) {
uidString += " ";
}
}
uidString.toUpperCase();
lastUID = uidString;
Serial.print("UID: ");
Serial.println(lastUID);
rfid.PICC_HaltA();
rfid.PCD_StopCrypto1();
if (lastUID == "04 88 F0 32 DA 61 80") {
currentMode = 1;
lastAState = digitalRead(pinA);
lastButtonState = HIGH;
enterWorkMode();
}
else if (lastUID == "04 16 36 3A DA 61 81") {
currentMode = 2;
lastAState = digitalRead(pinA);
lastButtonState = HIGH;
enterBreakMode();
}
else if (lastUID == "04 4F E1 7A 2A 62 81") {
currentMode = 3;
lastAState = digitalRead(pinA);
lastButtonState = HIGH;
enterTodoMode();
}
else {
currentMode = 0;
runHomeMode();
}
}
if (digitalRead(k0Pin) == LOW) {
if (currentMode == 3) {
handleTodoSideButton();
} else {
currentMode = 0;
runHomeMode();
}
delay(200);
}
if (currentMode == 1) {
runWorkMode();
}
else if (currentMode == 2) {
runBreakMode();
}
else if (currentMode == 3) {
runTodoMode();
}
delay(50);
}