/*
* Pomodoro Stick - Pomodoro Timer for M5StickC Plus2
*
* Button Controls:
* - Menu Mode: A=Next program, A Long=Select, B=Prev program
* - Timer Running: A=Pause, A Long=Skip step, B Long=Exit dialog
* - Timer Paused: A=Resume, A Long=Skip step, B Long=Exit dialog
* - Exit Dialog: A=Confirm exit, B=Cancel
*/
#include <M5Unified.h>
// ============================================================================
// Constants
// ============================================================================
#define LONG_PRESS_MS 800
#define DEBOUNCE_MS 50
// Colors
#define COLOR_BG TFT_BLACK
#define COLOR_TEXT TFT_WHITE
#define COLOR_WORK 0xFD20 // Orange
#define COLOR_BREAK 0x07E0 // Green
#define COLOR_ACCENT 0x07FF // Cyan
#define COLOR_DIM 0x7BEF // Gray
// Screen dimensions (M5StickC Plus2 in landscape: 240x135)
#define SCREEN_W 240
#define SCREEN_H 135
// Top margin to account for LCD offset from case window
#define TOP_MARGIN 2
// ============================================================================
// Data Structures
// ============================================================================
struct PomodoroStep {
uint16_t duration; // Duration in seconds
uint16_t beepFrequency; // Beep frequency (0 = no beep)
const char* label; // Step label
};
struct PomodoroProgram {
const char* name;
PomodoroStep* steps;
uint8_t stepCount;
int8_t loopCount; // -1 = infinite, 1+ = n times
};
enum AppState {
STATE_MENU,
STATE_TIMER_RUNNING,
STATE_TIMER_PAUSED,
STATE_CONFIRM_EXIT
};
struct ButtonEvent {
bool pressed;
bool longPressed;
bool released;
};
// ============================================================================
// Program Definitions
// ============================================================================
// Classic Pomodoro 25/5
PomodoroStep pomodoro25_5_steps[] = {
{25 * 60, 1000, "Work"},
{5 * 60, 800, "Break"}
};
// Short Focus 15/3
PomodoroStep shortFocus_steps[] = {
{15 * 60, 1000, "Work"},
{3 * 60, 800, "Break"}
};
// Long Session 50/10
PomodoroStep longSession_steps[] = {
{50 * 60, 1000, "Work"},
{10 * 60, 800, "Break"}
};
// Quick Test 10s/5s
PomodoroStep quickTest_steps[] = {
{10, 1000, "Work"},
{5, 800, "Break"}
};
// Programs array
PomodoroProgram programs[] = {
{"Pomodoro 25/5", pomodoro25_5_steps, 2, 4},
{"Short Focus", shortFocus_steps, 2, 6},
{"Long Session", longSession_steps, 2, 2},
{"Quick Test", quickTest_steps, 2, 2}
};
const uint8_t PROGRAM_COUNT = sizeof(programs) / sizeof(programs[0]);
// ============================================================================
// Global State
// ============================================================================
AppState currentState = STATE_MENU;
uint8_t currentProgramIndex = 0;
// Timer state
uint32_t remainingSeconds = 0;
uint8_t currentStepIndex = 0;
uint8_t currentLoop = 0;
uint32_t lastTickMillis = 0;
// Button state
bool btnA_wasPressed = false;
bool btnB_wasPressed = false;
uint32_t btnA_pressStart = 0;
uint32_t btnB_pressStart = 0;
bool btnA_longHandled = false;
bool btnB_longHandled = false;
// UI state
bool needsRedraw = true;
// Double buffer canvas (sprite) to prevent flickering
M5Canvas canvas(&M5.Lcd);
// ============================================================================
// Helper Functions
// ============================================================================
void formatTime(uint32_t seconds, char* buffer) {
uint16_t mins = seconds / 60;
uint16_t secs = seconds % 60;
sprintf(buffer, "%02d:%02d", mins, secs);
}
uint32_t calculateTotalTime(PomodoroProgram* prog) {
uint32_t total = 0;
for (uint8_t i = 0; i < prog->stepCount; i++) {
total += prog->steps[i].duration;
}
return total * prog->loopCount;
}
uint16_t getStepColor(const char* label) {
if (strcmp(label, "Work") == 0) return COLOR_WORK;
if (strcmp(label, "Break") == 0) return COLOR_BREAK;
return COLOR_TEXT;
}
void playBeep(uint16_t frequency) {
if (frequency > 0) {
M5.Speaker.tone(frequency, 200);
delay(250);
M5.Speaker.tone(frequency, 200);
}
}
// ============================================================================
// Button Handling
// ============================================================================
ButtonEvent checkButton(m5::Button_Class& btn, bool& wasPressed,
uint32_t& pressStart, bool& longHandled) {
ButtonEvent event = {false, false, false};
if (btn.isPressed()) {
if (!wasPressed) {
// Button just pressed
wasPressed = true;
pressStart = millis();
longHandled = false;
} else if (!longHandled && (millis() - pressStart >= LONG_PRESS_MS)) {
// Long press detected
event.longPressed = true;
longHandled = true;
}
} else {
if (wasPressed) {
// Button released
if (!longHandled) {
event.pressed = true; // Short press
}
event.released = true;
wasPressed = false;
}
}
return event;
}
// ============================================================================
// UI Drawing Functions
// ============================================================================
void drawMenuScreen() {
canvas.fillScreen(COLOR_BG);
PomodoroProgram* prog = &programs[currentProgramIndex];
char timeStr[10];
// Program name (top, centered, large)
canvas.setTextColor(COLOR_ACCENT);
canvas.setTextSize(3);
canvas.setTextDatum(TC_DATUM);
canvas.drawString(prog->name, SCREEN_W / 2, TOP_MARGIN + 3);
// Separator line
canvas.drawLine(10, TOP_MARGIN + 33, SCREEN_W - 10, TOP_MARGIN + 33, COLOR_DIM);
// Steps list (left side)
canvas.setTextSize(2);
canvas.setTextDatum(TL_DATUM);
int yPos = TOP_MARGIN + 43;
uint8_t displaySteps = min((uint8_t)2, prog->stepCount);
for (uint8_t i = 0; i < displaySteps; i++) {
PomodoroStep* step = &prog->steps[i];
// Step label
canvas.setTextColor(getStepColor(step->label));
canvas.drawString(step->label, 10, yPos);
// Step duration
formatTime(step->duration, timeStr);
canvas.setTextColor(COLOR_TEXT);
canvas.drawString(timeStr, 75, yPos);
yPos += 25;
}
// Right side: Total time and loop info
canvas.setTextDatum(TL_DATUM);
canvas.setTextColor(COLOR_DIM);
canvas.drawString("Total", 155, TOP_MARGIN + 43);
uint32_t totalTime = calculateTotalTime(prog);
formatTime(totalTime, timeStr);
canvas.setTextColor(COLOR_TEXT);
canvas.drawString(timeStr, 155, TOP_MARGIN + 68);
// Loop info
canvas.setTextColor(COLOR_DIM);
canvas.setTextDatum(TL_DATUM);
char loopStr[20];
if (prog->loopCount < 0) {
sprintf(loopStr, "Loop: inf");
} else {
sprintf(loopStr, "x%d", prog->loopCount);
}
canvas.drawString(loopStr, 10, TOP_MARGIN + 98);
// Program indicator bar (thin rectangle)
uint16_t barWidth = 80;
uint16_t segmentWidth = barWidth / PROGRAM_COUNT;
uint16_t barX = (SCREEN_W - barWidth) / 2;
uint16_t barY = SCREEN_H - 6;
canvas.drawRect(barX, barY, barWidth, 4, COLOR_DIM);
canvas.fillRect(barX + currentProgramIndex * segmentWidth, barY, segmentWidth, 4, COLOR_ACCENT);
}
void drawTimerScreen() {
canvas.fillScreen(COLOR_BG);
PomodoroProgram* prog = &programs[currentProgramIndex];
PomodoroStep* step = &prog->steps[currentStepIndex];
char timeStr[10];
// Step label (top left, large)
canvas.setTextColor(getStepColor(step->label));
canvas.setTextSize(3);
canvas.setTextDatum(TL_DATUM);
canvas.drawString(step->label, 10, TOP_MARGIN + 3);
// Status indicator (top right)
canvas.setTextSize(2);
canvas.setTextDatum(TR_DATUM);
if (currentState == STATE_TIMER_PAUSED) {
canvas.setTextColor(COLOR_WORK);
canvas.drawString("PAUSED", SCREEN_W - 10, TOP_MARGIN + 6);
} else {
canvas.setTextColor(COLOR_BREAK);
canvas.drawString("RUN", SCREEN_W - 10, TOP_MARGIN + 6);
}
// Large countdown timer (center)
formatTime(remainingSeconds, timeStr);
canvas.setTextSize(5);
canvas.setTextColor(COLOR_TEXT);
canvas.setTextDatum(TC_DATUM);
canvas.drawString(timeStr, SCREEN_W / 2, TOP_MARGIN + 36);
// Progress bar (below timer)
uint16_t barWidth = SCREEN_W - 20;
uint16_t barHeight = 8;
uint16_t barX = 10;
uint16_t barY = TOP_MARGIN + 88;
float progress = 1.0 - ((float)remainingSeconds / step->duration);
uint16_t fillWidth = (uint16_t)(barWidth * progress);
canvas.drawRect(barX, barY, barWidth, barHeight, COLOR_DIM);
if (fillWidth > 0) {
canvas.fillRect(barX + 1, barY + 1, fillWidth - 2, barHeight - 2, getStepColor(step->label));
}
// Bottom row: Next step and Loop counter
canvas.setTextSize(2);
canvas.setTextDatum(TL_DATUM);
// Next step preview (bottom left)
uint8_t nextStepIndex = (currentStepIndex + 1) % prog->stepCount;
uint8_t nextLoop = currentLoop;
if (nextStepIndex == 0) nextLoop++;
if (nextLoop < prog->loopCount || prog->loopCount < 0) {
PomodoroStep* nextStep = &prog->steps[nextStepIndex];
canvas.setTextColor(getStepColor(nextStep->label));
canvas.drawString(nextStep->label, 10, TOP_MARGIN + 106);
formatTime(nextStep->duration, timeStr);
canvas.setTextColor(COLOR_TEXT);
canvas.drawString(timeStr, 75, TOP_MARGIN + 106);
} else {
canvas.setTextColor(COLOR_ACCENT);
canvas.drawString("Last!", 10, TOP_MARGIN + 106);
}
// Loop counter (bottom right)
canvas.setTextColor(COLOR_DIM);
canvas.setTextDatum(TR_DATUM);
char loopStr[12];
if (prog->loopCount < 0) {
sprintf(loopStr, "#%d", currentLoop + 1);
} else {
sprintf(loopStr, "%d/%d", currentLoop + 1, prog->loopCount);
}
canvas.drawString(loopStr, SCREEN_W - 10, TOP_MARGIN + 106);
}
void drawConfirmDialog() {
// Draw dialog box on top of canvas (after timer screen is drawn)
canvas.fillRect(30, 20, 180, 95, COLOR_BG);
canvas.drawRect(30, 20, 180, 95, COLOR_ACCENT);
// Dialog content
canvas.setTextColor(COLOR_TEXT);
canvas.setTextSize(3);
canvas.setTextDatum(TC_DATUM);
canvas.drawString("Exit?", SCREEN_W / 2, 35);
canvas.setTextSize(2);
canvas.setTextColor(COLOR_BREAK);
canvas.drawString("A:Yes", 75, 80);
canvas.setTextColor(COLOR_WORK);
canvas.drawString("B:No", 165, 80);
}
// ============================================================================
// Timer Logic
// ============================================================================
void startTimer() {
PomodoroProgram* prog = &programs[currentProgramIndex];
currentStepIndex = 0;
currentLoop = 0;
remainingSeconds = prog->steps[0].duration;
lastTickMillis = millis();
currentState = STATE_TIMER_PAUSED; // Start paused, user presses A to begin
needsRedraw = true;
}
void advanceStep() {
PomodoroProgram* prog = &programs[currentProgramIndex];
// Play beep for completed step
playBeep(prog->steps[currentStepIndex].beepFrequency);
// Move to next step
currentStepIndex++;
// Check if we completed all steps in this loop
if (currentStepIndex >= prog->stepCount) {
currentStepIndex = 0;
currentLoop++;
// Check if all loops completed
if (prog->loopCount >= 0 && currentLoop >= prog->loopCount) {
// Timer complete - return to menu
currentState = STATE_MENU;
needsRedraw = true;
return;
}
}
// Start new step
remainingSeconds = prog->steps[currentStepIndex].duration;
lastTickMillis = millis();
needsRedraw = true;
}
void updateTimer() {
if (currentState != STATE_TIMER_RUNNING) return;
uint32_t now = millis();
if (now - lastTickMillis >= 1000) {
lastTickMillis += 1000;
if (remainingSeconds > 0) {
remainingSeconds--;
needsRedraw = true;
if (remainingSeconds == 0) {
advanceStep();
}
}
}
}
// ============================================================================
// Main Setup & Loop
// ============================================================================
void setup() {
auto cfg = M5.config();
cfg.internal_spk = true;
M5.begin(cfg);
// Power management
M5.Power.setLed(0); // Turn off LED
// Display setup (landscape, rotated 90 clockwise)
M5.Lcd.setRotation(1);
M5.Lcd.fillScreen(COLOR_BG);
M5.Lcd.setTextWrap(false);
// Create sprite for double buffering (eliminates flicker)
canvas.createSprite(SCREEN_W, SCREEN_H);
canvas.setTextWrap(false);
// Speaker setup
M5.Speaker.setVolume(128);
// Initial draw
needsRedraw = true;
}
void loop() {
M5.update();
// Check buttons
ButtonEvent btnA = checkButton(M5.BtnA, btnA_wasPressed, btnA_pressStart, btnA_longHandled);
ButtonEvent btnB = checkButton(M5.BtnB, btnB_wasPressed, btnB_pressStart, btnB_longHandled);
// State machine
switch (currentState) {
case STATE_MENU:
if (btnA.pressed) {
// Next program
currentProgramIndex = (currentProgramIndex + 1) % PROGRAM_COUNT;
needsRedraw = true;
}
if (btnA.longPressed) {
// Select and start
startTimer();
}
if (btnB.pressed) {
// Previous program
if (currentProgramIndex == 0) {
currentProgramIndex = PROGRAM_COUNT - 1;
} else {
currentProgramIndex--;
}
needsRedraw = true;
}
break;
case STATE_TIMER_RUNNING:
if (btnA.pressed) {
// Pause
currentState = STATE_TIMER_PAUSED;
needsRedraw = true;
}
if (btnA.longPressed) {
// Skip step
advanceStep();
}
if (btnB.longPressed) {
// Show exit confirmation
currentState = STATE_CONFIRM_EXIT;
needsRedraw = true;
}
// Update timer
updateTimer();
break;
case STATE_TIMER_PAUSED:
if (btnA.pressed) {
// Resume
lastTickMillis = millis(); // Reset tick reference
currentState = STATE_TIMER_RUNNING;
needsRedraw = true;
}
if (btnA.longPressed) {
// Skip step
lastTickMillis = millis();
currentState = STATE_TIMER_RUNNING;
advanceStep();
}
if (btnB.longPressed) {
// Show exit confirmation
currentState = STATE_CONFIRM_EXIT;
needsRedraw = true;
}
break;
case STATE_CONFIRM_EXIT:
if (btnA.pressed) {
// Confirm exit
currentState = STATE_MENU;
needsRedraw = true;
}
if (btnB.pressed) {
// Cancel - return to paused state
currentState = STATE_TIMER_PAUSED;
needsRedraw = true;
}
break;
}
// Redraw if needed
if (needsRedraw) {
needsRedraw = false;
switch (currentState) {
case STATE_MENU:
drawMenuScreen();
break;
case STATE_TIMER_RUNNING:
case STATE_TIMER_PAUSED:
drawTimerScreen();
break;
case STATE_CONFIRM_EXIT:
drawTimerScreen();
drawConfirmDialog();
break;
}
// Push the canvas to display (single push eliminates flicker)
canvas.pushSprite(0, 0);
}
delay(10); // Small delay to prevent tight loop
}
Comments