12 April 2026

IVergara - Final project prototype

 Productivity pal

This project is a small productivity device that uses RFID tags to trigger different modes on a screen. The idea is to make the interaction feel more physical and intentional: instead of opening features through regular buttons or menus, the user taps a specific tag to launch a mode. One mode is a focus timer for work sessions, and another is an active break mode where the user selects a short exercise and follows a countdown on screen.

I originally started the project with an ESP32, but I ran into technical issues while testing the display, which made development less reliable. Another factor was speed: every change on the ESP32 took noticeably longer to compile and upload, which slowed down testing and experimentation. Because of that, I moved the project to an Arduino Uno. The Uno felt easier to work with for this prototype because uploads were much faster and the overall setup was simpler to debug.

The device combines a display, an RFID reader, and a rotary encoder. The RFID reader and the screen share some communication pins, which keeps the wiring compact, while separate control pins let each part work correctly in the same system. The encoder is used to move through options and confirm selections, so once a mode is opened, the user can interact with it in a simple and clear way.

 Video



 

 

 

 Photos








 Schematic

 

 

Code 

#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;

// 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");
}

// 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");
      }
    }
  }
}

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(1);

  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 == "13 EA 00 2D") {
      currentMode = 1;
      lastAState = digitalRead(pinA);
      lastButtonState = HIGH;
      enterWorkMode();
    }
    else if (lastUID == "8C 50 FA 03") {
      currentMode = 2;
      lastAState = digitalRead(pinA);
      lastButtonState = HIGH;
      enterBreakMode();
    }
    else {
      currentMode = 0;
      runHomeMode();
    }
  }

  if (digitalRead(k0Pin) == LOW) {
    currentMode = 0;
    runHomeMode();
    delay(200);
  }

  if (currentMode == 1) {
    runWorkMode();
  }
  else if (currentMode == 2) {
    runBreakMode();
  }

  delay(50);
}