Skip to content

Instantly share code, notes, and snippets.

@maliqq
Created March 13, 2026 00:10
Show Gist options
  • Select an option

  • Save maliqq/738f8e7205adb631ef1289d1eafbdbbc to your computer and use it in GitHub Desktop.

Select an option

Save maliqq/738f8e7205adb631ef1289d1eafbdbbc to your computer and use it in GitHub Desktop.
/*
* Kitchen Pomodoro Timer — Arduino Nano (3-Button Version)
*
* Hardware:
* - 4-digit 7-segment LED display (TM1637) → CLK=D2, DIO=D3
* - Piezo buzzer → D8
* - BTN_M (Mode) → D4 (INPUT_PULLUP)
* - BTN_PLUS (+) → D5 (INPUT_PULLUP)
* - BTN_MINUS (−) → D6 (INPUT_PULLUP)
*
* 3-Button Logic:
* M cycles: READY → PRESET → MANUAL → READY → ...
*
* READY state: [+] = Start [−] = (no action)
* PRESET state: [+] = Next [−] = Prev (5/15/30/60 min)
* MANUAL state: [+] = +5 min [−] = −5 min
* RUNNING: [+] = Pause [−] = Stop → READY
* PAUSED: [+] = Resume [−] = Stop → READY
*
* Install library: "TM1637Display" by Avishay Orpaz (Library Manager)
*/
#include <TM1637Display.h>
// ── Pins ─────────────────────────────────────────────────────
#define CLK 2
#define DIO 3
#define BTN_M 4
#define BTN_PLUS 5
#define BTN_MINUS 6
#define BUZZER 8
// ── Presets (seconds) ────────────────────────────────────────
const uint16_t PRESETS[] = { 300, 900, 1800, 3600 };
const uint8_t NUM_PRESETS = 4;
// ── State Machine ────────────────────────────────────────────
enum State { READY, PRESET, MANUAL, RUNNING, PAUSED };
TM1637Display display(CLK, DIO);
State state = READY;
uint8_t presetIndex = 0;
uint16_t totalSeconds = PRESETS[0];
uint16_t remaining = 0;
unsigned long prevMillis = 0;
// Debounce
unsigned long lastPress[3] = {0, 0, 0};
const unsigned long DEBOUNCE = 200;
// Blink control for edit modes
unsigned long lastBlink = 0;
bool blinkOn = true;
// ── Helpers ──────────────────────────────────────────────────
bool pressed(uint8_t pin, uint8_t idx) {
if (digitalRead(pin) == LOW) {
unsigned long now = millis();
if (now - lastPress[idx] > DEBOUNCE) {
lastPress[idx] = now;
return true;
}
}
return false;
}
void showTime(uint16_t secs, bool colon) {
uint8_t mm = secs / 60;
uint8_t ss = secs % 60;
uint8_t data[] = {
display.encodeDigit(mm / 10),
display.encodeDigit(mm % 10),
display.encodeDigit(ss / 10),
display.encodeDigit(ss % 10)
};
if (colon) data[1] |= 0x80;
display.setSegments(data);
}
void showBlank() {
uint8_t blank[] = { 0, 0, 0, 0 };
display.setSegments(blank);
}
void beepDone() {
for (uint8_t i = 0; i < 3; i++) {
tone(BUZZER, 2000, 400);
delay(600);
}
noTone(BUZZER);
}
void beepClick() {
tone(BUZZER, 4000, 30);
}
void beepMode() {
tone(BUZZER, 3000, 60);
}
// ── Setup ────────────────────────────────────────────────────
void setup() {
pinMode(BTN_M, INPUT_PULLUP);
pinMode(BTN_PLUS, INPUT_PULLUP);
pinMode(BTN_MINUS, INPUT_PULLUP);
pinMode(BUZZER, OUTPUT);
display.setBrightness(5);
showTime(totalSeconds, true);
}
// ── Main Loop ────────────────────────────────────────────────
void loop() {
bool pM = pressed(BTN_M, 0);
bool pPlus = pressed(BTN_PLUS, 1);
bool pMinus = pressed(BTN_MINUS, 2);
switch (state) {
// ·············· READY — display set time, + starts ········
case READY:
showTime(totalSeconds, true);
if (pM) {
beepMode();
state = PRESET;
lastBlink = millis();
blinkOn = true;
}
if (pPlus) { // START
beepClick();
remaining = totalSeconds;
prevMillis = millis();
state = RUNNING;
}
break;
// ·············· PRESET — cycle 5/15/30/60 with +/− ·······
case PRESET:
// Blink display to indicate edit mode
if (millis() - lastBlink > 300) {
lastBlink = millis();
blinkOn = !blinkOn;
}
if (blinkOn) showTime(PRESETS[presetIndex], true);
else showBlank();
if (pM) { // → MANUAL mode
beepMode();
totalSeconds = PRESETS[presetIndex];
state = MANUAL;
lastBlink = millis();
blinkOn = true;
}
if (pPlus) { // next preset
beepClick();
presetIndex = (presetIndex + 1) % NUM_PRESETS;
totalSeconds = PRESETS[presetIndex];
blinkOn = true; lastBlink = millis();
}
if (pMinus) { // prev preset
beepClick();
presetIndex = (presetIndex + NUM_PRESETS - 1) % NUM_PRESETS;
totalSeconds = PRESETS[presetIndex];
blinkOn = true; lastBlink = millis();
}
break;
// ·············· MANUAL — fine-tune ±5 min ·················
case MANUAL:
// Slower blink to distinguish from PRESET mode
if (millis() - lastBlink > 500) {
lastBlink = millis();
blinkOn = !blinkOn;
}
if (blinkOn) showTime(totalSeconds, false); // no colon = visual cue
else showBlank();
if (pM) { // → back to READY
beepMode();
state = READY;
}
if (pPlus) { // +5 min
beepClick();
if (totalSeconds <= 5695) totalSeconds += 300;
blinkOn = true; lastBlink = millis();
}
if (pMinus) { // −5 min
beepClick();
if (totalSeconds > 300) totalSeconds -= 300;
blinkOn = true; lastBlink = millis();
}
break;
// ·············· RUNNING ···································
case RUNNING:
if (millis() - prevMillis >= 1000) {
prevMillis += 1000;
if (remaining > 0) {
remaining--;
showTime(remaining, true);
}
if (remaining == 0) {
showTime(0, true);
beepDone();
state = READY;
showTime(totalSeconds, true);
}
}
if (pPlus) { // PAUSE
beepClick();
state = PAUSED;
}
if (pMinus) { // STOP → READY
beepClick();
state = READY;
showTime(totalSeconds, true);
}
break;
// ·············· PAUSED — blinking colon ···················
case PAUSED: {
bool colonOn = (millis() / 500) % 2;
showTime(remaining, colonOn);
if (pPlus) { // RESUME
beepClick();
prevMillis = millis();
state = RUNNING;
}
if (pMinus) { // STOP → READY
beepClick();
state = READY;
showTime(totalSeconds, true);
}
break;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment