Pomodoro Timer for UNIHIKER K10

 

Why Everyone Needs a Pomodoro Timer

Sound familiar? Your to-do list is long, but concentration is fading – emails, social media, the coffee machine is calling. This is where the legendary Pomodoro Technique helps: It breaks work into productive 25-minute blocks ("Pomodoros"), followed by mini-breaks. This way you work more focused, less stressed, and celebrate small wins regularly. Whether studying, coding, office work, or creative projects – a smart Pomodoro timer makes a real difference!​

With the UNIHIKER K10 board, makers and tinkerers can implement this classic method in a modern and visually stunning way!

 

Project Idea: A Stylish Pomodoro Timer on the K10

Features:

Clean, modern display: Wine-red title, large bold time display, colored phase indicator, and a thick animated progress bar.

Intuitive controls: Start/Stop with button A, Reset with button B.

Fully Open Source and easy to replicate!

The Code, Step by Step

The entire timer runs on the UNIHIKER K10 with just a few libraries – no special hardware needed.
 

Key highlights:

All phases and states (work time, break, pause/resume, transition).

Flexible time adjustment (debug mode for demo purposes).

All colors, layouts, and speeds configurable via constants.

Cleanly separated display logic and control code.

 

 

How Your Pomodoro-K10 Works in Daily Life

 

Set the debug flag to your needs:

const bool DEBUG_MODE = true;   // true = 10s/5s for testing, false = 25min/5min

 

Compile project & upload to K10

 

Start timer with button A: The timer runs, progress bar grows, time ticks visibly.

Automatic switching: When time runs out, there's a brief "moment of reflection" – then the next phase automatically begins (Work ↔ Break).

Pause/Reset: Pause/resume with button A, reset everything with button B.

The Result: Complete overview, no rush, visually appealing timer app – and Pomodoro productivity on your maker board!

 

 

Live Demo Video


See the workflow directly on the real board – including all animated transitions and visual effects!

 

Conclusion & Download

With just a few lines of code, you turn your UNIHIKER K10 into the perfect everyday productivity machine – whether at work, studying, or during hackathons.
The complete source code is available below; customisations are super easy.

Have fun building and being productive!
Questions, feedback & your improvements are welcome in the comments.

🍅⏲️ #Pomodoro #UNIHIKER #MakerMindset

 

Download and Complete Code:
see embedded code below

 

Tip: Depending on feedback, elegant audio signals or additional features can easily be added. Happy Making!

CODE
// ====================================================================
// Pomodoro Timer for UNIHIKER K10
// 
// Features:
// - Centered, bold countdown display
// - Medium-thick progress bar (50px height)
// - Smooth transition between phases with 1-second pause
// - Button A: Start/Pause timer
// - Button B: Reset to idle
// - DEBUG_MODE: reduces times to 10s work / 5s break for testing
// ====================================================================

#include "unihiker_k10.h"

UNIHIKER_K10 k10;
const uint8_t screen_dir = 2;

// ====================================================================
// CONFIGURATION
// ====================================================================

const bool DEBUG_MODE = true;   // true = 10s/5s for testing, false = 25min/5min

// Timer durations (in seconds)
const unsigned long WORK_SEC  = 25UL * 60UL;   // 25 minutes
const unsigned long BREAK_SEC = 5UL * 60UL;    // 5 minutes
const unsigned long DEBUG_WORK_SEC  = 10UL;    // 10 seconds (debug)
const unsigned long DEBUG_BREAK_SEC = 5UL;     // 5 seconds (debug)

// Transition delay between phases (milliseconds)
const unsigned long PHASE_TRANSITION_DELAY = 1000;  // 1 second pause

// Display layout
const int MARGIN_LEFT = 5;
const int SCREEN_W = 240;
const int SCREEN_H = 320;
const int PROGRESS_BAR_HEIGHT = 50;  // 50px height

// Color palette (24-bit RGB)
const uint32_t COLOR_BG          = 0xFFFFFF;  // White background
const uint32_t COLOR_TEXT        = 0x000000;  // Black text
const uint32_t COLOR_WORK        = 0x800000;  // Maroon for work phase
const uint32_t COLOR_BREAK       = 0x0000CC;  // Blue for break phase
const uint32_t COLOR_BAR_BG      = 0xDDDDDD;  // Light gray bar background
const uint32_t COLOR_PAUSED      = 0xFF0000;  // Red for paused state
const uint32_t COLOR_TITLE       = 0x8B0000;  // Dark wine red for title

// ====================================================================
// STATE MANAGEMENT
// ====================================================================

enum TimerState { IDLE, WORK, BREAK, PAUSED, TRANSITIONING };
TimerState state = IDLE;

unsigned long startMillis = 0;       // When current phase started
unsigned long elapsedBefore = 0;     // Time elapsed before pause
unsigned long periodTotalMs = 0;     // Total duration of current phase
unsigned long transitionStartMs = 0; // When transition started

// Button edge detection
bool prevA = false;
bool prevB = false;

// ====================================================================
// HELPER FUNCTIONS
// ====================================================================

// Get work duration in milliseconds based on debug mode
unsigned long getWorkMs() { 
  return (DEBUG_MODE ? DEBUG_WORK_SEC : WORK_SEC) * 1000UL; 
}

// Get break duration in milliseconds based on debug mode
unsigned long getBreakMs() { 
  return (DEBUG_MODE ? DEBUG_BREAK_SEC : BREAK_SEC) * 1000UL; 
}

// Format milliseconds to MM:SS string
String fmtMSecToMMSS(unsigned long ms) {
  unsigned long s = ms / 1000UL;
  unsigned long m = s / 60UL;
  unsigned long sec = s % 60UL;
  char buf[10];
  sprintf(buf, "%02lu:%02lu", m, sec);
  return String(buf);
}

// ====================================================================
// DRAWING FUNCTIONS
// ====================================================================

// Draw bold, centered countdown text
// Uses multiple offset draws to simulate bold text effect
void drawBoldCenteredTime(const String &text, uint32_t color, int y) {
  // Clear larger area to prevent ghosting and pixel glitches
  int clearTop = y - 45;
  int clearLeft = 0;  // Clear from left edge
  if (clearTop < 0) clearTop = 0;
  k10.canvas->canvasRectangle(clearLeft, clearTop, SCREEN_W, 100, COLOR_BG, COLOR_BG, true);

  // Calculate inner area (respecting margins)
  int innerX = MARGIN_LEFT;
  int innerW = SCREEN_W - 2 * MARGIN_LEFT;

  // Draw text multiple times with slight offsets to create bold effect
  const int offsets[7][2] = {
    {0, 0},   // Center
    {-1, 0},  // Left
    {1, 0},   // Right
    {0, -1},  // Top
    {0, 1},   // Bottom
    {-1, -1}, // Top-left
    {1, 1}    // Bottom-right
  };

  for (int i = 0; i < 7; ++i) {
    int ox = offsets[i][0];
    int oy = offsets[i][1];
    k10.canvas->canvasText(text, innerX + ox, y + oy, color, 
                           k10.canvas->eCNAndENFont24, innerW, true);
  }
}

// Draw initial static screen (shown in IDLE state)
void drawScreenStatic() {
  k10.canvas->canvasRectangle(0, 0, SCREEN_W, SCREEN_H, COLOR_BG, COLOR_BG, true);

  // Title in wine red color
  k10.canvas->canvasText("Pomodoro Timer", MARGIN_LEFT, 4, COLOR_TITLE, 
                         k10.canvas->eCNAndENFont24, SCREEN_W - MARGIN_LEFT, true);

  // Initial duration display
  String totalStr = fmtMSecToMMSS(getWorkMs());
  k10.canvas->canvasText(String("Total: ") + totalStr, MARGIN_LEFT, 40, COLOR_TEXT, 
                         k10.canvas->eCNAndENFont16, SCREEN_W - MARGIN_LEFT, true);

  // Instructions
  k10.canvas->canvasText("Press A = Start/Pause, B = Reset", MARGIN_LEFT, SCREEN_H - 28, 
                         COLOR_TEXT, k10.canvas->eCNAndENFont16, SCREEN_W - MARGIN_LEFT, true);

  k10.canvas->updateCanvas();
}

// Draw dynamic elements (progress bar, countdown, etc.)
void drawDynamic(unsigned long remainingMs, float progress) {
  // Clamp and threshold progress to prevent visual artifacts
  if (progress < 0.003f) progress = 0.0f;  // Remove tiny start ghost pixels
  if (progress > 1.0f) progress = 1.0f;

  // Clear screen
  k10.canvas->canvasRectangle(0, 0, SCREEN_W, SCREEN_H, COLOR_BG, COLOR_BG, true);

  // ----------------------------------------------------------------
  // Header: Mode indicator (Work/Break/Paused)
  // ----------------------------------------------------------------
  uint32_t modeColor = (state == WORK) ? COLOR_WORK : COLOR_BREAK;
  String modeStr;
  
  if (state == WORK) {
    modeStr = "Work";
  } else if (state == BREAK) {
    modeStr = "Break";
  } else if (state == PAUSED) {
    modeStr = (periodTotalMs == getWorkMs()) ? "Work" : "Break";
    modeColor = COLOR_PAUSED;
  }
  
  k10.canvas->canvasText(modeStr, MARGIN_LEFT, 4, modeColor, 
                         k10.canvas->eCNAndENFont24, SCREEN_W - MARGIN_LEFT, true);

  // Total duration for current phase
  String totalStr = fmtMSecToMMSS(periodTotalMs);
  k10.canvas->canvasText(String("Total: ") + totalStr, MARGIN_LEFT, 40, COLOR_TEXT, 
                         k10.canvas->eCNAndENFont16, SCREEN_W - MARGIN_LEFT, true);

  // ----------------------------------------------------------------
  // Progress Bar (50px height)
  // ----------------------------------------------------------------
  const int barX = MARGIN_LEFT;
  const int barY = 80;
  const int barW = SCREEN_W - 2 * MARGIN_LEFT;
  const int barH = PROGRESS_BAR_HEIGHT;

  // Draw background track
  k10.canvas->canvasRectangle(barX, barY, barW, barH, COLOR_BAR_BG, COLOR_BAR_BG, true);

  // Calculate filled portion
  int filledW = (int)round(barW * progress);
  if (filledW < 0) filledW = 0;
  if (filledW > barW) filledW = barW;

  // Only draw fill if it's visible enough (prevents ghost pixels)
  const int MIN_FILL_PIXELS = 3;
  if (filledW >= MIN_FILL_PIXELS) {
    uint32_t barColor = (periodTotalMs == getWorkMs()) ? COLOR_WORK : COLOR_BREAK;
    k10.canvas->canvasRectangle(barX, barY, filledW, barH, barColor, barColor, true);
  }

  // ----------------------------------------------------------------
  // Countdown Display (Bold, Centered)
  // ----------------------------------------------------------------
  String timeStr = fmtMSecToMMSS(remainingMs);
  int timeY = barY + barH + 30;  // Position below progress bar
  drawBoldCenteredTime(timeStr, COLOR_TEXT, timeY);

  // ----------------------------------------------------------------
  // Paused Indicator
  // ----------------------------------------------------------------
  if (state == PAUSED) {
    k10.canvas->canvasText("PAUSED", MARGIN_LEFT, timeY + 50, COLOR_PAUSED, 
                           k10.canvas->eCNAndENFont16, SCREEN_W - MARGIN_LEFT, true);
  }

  // ----------------------------------------------------------------
  // Footer: Button instructions
  // ----------------------------------------------------------------
  k10.canvas->canvasText("Press A = Start/Pause, B = Reset", MARGIN_LEFT, SCREEN_H - 28, 
                         COLOR_TEXT, k10.canvas->eCNAndENFont16, SCREEN_W - MARGIN_LEFT, true);

  k10.canvas->updateCanvas();
}

// Draw transition screen (clean state between phases)
void drawTransition(const String &nextPhase) {
  k10.canvas->canvasRectangle(0, 0, SCREEN_W, SCREEN_H, COLOR_BG, COLOR_BG, true);
  
  uint32_t phaseColor = (nextPhase == "Work") ? COLOR_WORK : COLOR_BREAK;
  
  // Show next phase
  String msg = "Next: " + nextPhase;
  k10.canvas->canvasText(msg, MARGIN_LEFT, SCREEN_H / 2 - 20, phaseColor, 
                         k10.canvas->eCNAndENFont24, SCREEN_W - MARGIN_LEFT, true);
  
  k10.canvas->updateCanvas();
}

// ====================================================================
// STATE TRANSITION FUNCTIONS
// ====================================================================

void startWork() {
  state = WORK;
  periodTotalMs = getWorkMs();
  startMillis = millis();
  elapsedBefore = 0;
}

void startBreak() {
  state = BREAK;
  periodTotalMs = getBreakMs();
  startMillis = millis();
  elapsedBefore = 0;
}

void pauseTimer() {
  elapsedBefore += (millis() - startMillis);
  state = PAUSED;
}

void resumeTimer() {
  startMillis = millis();
  state = (periodTotalMs == getWorkMs()) ? WORK : BREAK;
}

void resetAll() {
  state = IDLE;
  startMillis = 0;
  elapsedBefore = 0;
  periodTotalMs = 0;
  transitionStartMs = 0;
  drawScreenStatic();
}

void startTransition(bool workToBreak) {
  state = TRANSITIONING;
  transitionStartMs = millis();
  
  // Show transition screen
  if (workToBreak) {
    drawTransition("Break");
  } else {
    drawTransition("Work");
  }
}

// ====================================================================
// ARDUINO SETUP & LOOP
// ====================================================================

void setup() {
  k10.begin();
  k10.initScreen(screen_dir);
  k10.creatCanvas();
  k10.setScreenBackground(COLOR_BG);
  drawScreenStatic();
}

void loop() {
  // ----------------------------------------------------------------
  // Button Input Handling (Edge Detection)
  // ----------------------------------------------------------------
  bool a = k10.buttonA->isPressed();
  bool b = k10.buttonB->isPressed();

  // Button A: Start/Pause (not during transition)
  if (a && !prevA && state != TRANSITIONING) {
    if (state == IDLE) {
      startWork();
    } else if (state == WORK || state == BREAK) {
      pauseTimer();
    } else if (state == PAUSED) {
      resumeTimer();
    }
    delay(40);  // Debounce
  }

  // Button B: Reset
  if (b && !prevB) {
    resetAll();
    delay(40);  // Debounce
  }

  prevA = a;
  prevB = b;

  // ----------------------------------------------------------------
  // Timer Update Logic
  // ----------------------------------------------------------------
  
  if (state == TRANSITIONING) {
    // Wait for transition delay to complete
    unsigned long elapsed = millis() - transitionStartMs;
    if (elapsed >= PHASE_TRANSITION_DELAY) {
      // Transition complete - start next phase
      // Determine which phase to start based on periodTotalMs
      if (periodTotalMs == getWorkMs()) {
        // Was work phase, now start break
        startBreak();
      } else {
        // Was break phase, now start work
        startWork();
      }
    }
    // Stay in transition screen
    
  } else if (state == WORK || state == BREAK) {
    // Calculate elapsed and remaining time
    unsigned long now = millis();
    unsigned long runMs = elapsedBefore + (now - startMillis);
    unsigned long remainingMs = (runMs < periodTotalMs) ? (periodTotalMs - runMs) : 0;

    if (remainingMs == 0) {
      // Phase complete - start transition
      bool workToBreak = (state == WORK);
      startTransition(workToBreak);
    } else {
      // Update display with current progress
      float progress = (float)runMs / (float)periodTotalMs;
      if (progress < 0.0f) progress = 0.0f;
      if (progress > 1.0f) progress = 1.0f;
      drawDynamic(remainingMs, progress);
    }
    
  } else if (state == PAUSED) {
    // Show paused state
    unsigned long remainingMs = (elapsedBefore < periodTotalMs) 
                                ? (periodTotalMs - elapsedBefore) : 0;
    float progress = (float)elapsedBefore / (float)periodTotalMs;
    if (progress < 0.0f) progress = 0.0f;
    if (progress > 1.0f) progress = 1.0f;
    drawDynamic(remainingMs, progress);
  }
  // IDLE state: keep showing static screen

  delay(150);  // Update rate ~6-7 times per second
}
License
All Rights
Reserved
licensBg
0