
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!
// ====================================================================
// 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
}









