// rev2-V4 : 2차 개선 및 리팩터링 완료
/*
[rev02-v4 수정사항]
- 마지막 1분에 LED 깜빡임으로 종료 임박 알림
- 코드 정리 완료
*/
#include <Arduino.h>
#include <TM1637.h>
// ============================================================
// 설정값 (Config)
// ============================================================
namespace Config
{
// 로터리 엔코더 핀 (인터럽트 사용)
constexpr int ENCODER_CLK_PIN = 2; // INT0
constexpr int ENCODER_DT_PIN = 3; // INT1
constexpr int ENCODER_SW_PIN = 4; // 버튼
// 7세그먼트 핀
constexpr int SEGMENT_CLK_PIN = 5;
constexpr int SEGMENT_DIO_PIN = 6;
// LED 및 부저 핀
constexpr int WORK_LED_PIN = 11;
constexpr int BREAK_LED_PIN = 10;
constexpr int BUZZER_PIN = 9;
// 부저 설정
constexpr int BUZZER_FREQUENCY = 1000;
constexpr int BUZZER_DURATION_MS = 1000;
// 시간 단위
constexpr unsigned long ONE_SECOND_MS = 1000;
constexpr unsigned long ONE_MINUTE_MS = 60 * ONE_SECOND_MS;
// 뽀모도로 기본 시간 설정
constexpr unsigned long DEFAULT_WORK_TIME_MS = 25 * ONE_MINUTE_MS;
constexpr unsigned long DEFAULT_BREAK_TIME_MS = 5 * ONE_MINUTE_MS;
constexpr unsigned long MIN_TIME_MS = 1 * ONE_MINUTE_MS;
constexpr unsigned long MAX_TIME_MS = 60 * ONE_MINUTE_MS;
// LED 깜빡임 설정
constexpr unsigned long WARNING_TIME_MS = 1 * ONE_MINUTE_MS; // 마지막 1분
constexpr unsigned long BLINK_INTERVAL_MS = 250; // 깜빡임 주기
// 버튼 설정
constexpr unsigned long LONG_PRESS_MS = 2000;
constexpr unsigned long DEBOUNCE_MS = 50;
// 디버그 모드
constexpr bool DEBUG_MODE = true;
}
// ============================================================
// 전역 객체 및 상태 변수
// ============================================================
TM1637 segment(Config::SEGMENT_CLK_PIN, Config::SEGMENT_DIO_PIN);
namespace Timer
{
bool isBreakTime = false;
bool isPaused = false;
unsigned long previousTime = 0;
unsigned long pausedTime = 0;
unsigned long workTimeMs = Config::DEFAULT_WORK_TIME_MS;
unsigned long breakTimeMs = Config::DEFAULT_BREAK_TIME_MS;
}
namespace Encoder
{
volatile int rotationCount = 0;
int lastRotationCount = 0;
}
namespace Button
{
bool lastState = HIGH;
unsigned long pressStartTime = 0;
bool isPressed = false;
}
// ============================================================
// 함수 선언
// ============================================================
void updateSystem();
void handlePomodoroTimer();
void handleEncoderButton();
void handleEncoderRotation();
void updateLedIndicator();
void updateSegmentDisplay();
void displayTwoDigits(int startPos, int value);
unsigned long calculateRemainTime();
void notifyPhaseComplete();
void resetTimer();
void togglePause();
void adjustTime(int direction);
bool isWarningTime();
// 인터럽트 서비스 루틴
void encoderISR();
// ============================================================
// setup / loop
// ============================================================
void setup()
{
Serial.begin(9600);
// LED 및 부저 핀 설정
pinMode(Config::WORK_LED_PIN, OUTPUT);
pinMode(Config::BREAK_LED_PIN, OUTPUT);
pinMode(Config::BUZZER_PIN, OUTPUT);
// 로터리 엔코더 핀 설정
pinMode(Config::ENCODER_CLK_PIN, INPUT_PULLUP);
pinMode(Config::ENCODER_DT_PIN, INPUT_PULLUP);
pinMode(Config::ENCODER_SW_PIN, INPUT_PULLUP);
// 인터럽트 설정
attachInterrupt(digitalPinToInterrupt(Config::ENCODER_CLK_PIN), encoderISR, FALLING);
// 7세그먼트 초기화
segment.init();
segment.set(BRIGHT_TYPICAL);
// LED 초기 상태
digitalWrite(Config::WORK_LED_PIN, LOW);
digitalWrite(Config::BREAK_LED_PIN, LOW);
Serial.println("뽀모도로 타이머 시작");
Serial.println("버튼 클릭: 일시정지/재개");
Serial.println("버튼 길게(2초): 리셋");
Serial.println("회전(일시정지중): 시간 조절");
}
void loop()
{
// 1. 입력 처리
handleEncoderButton();
handleEncoderRotation();
// 2. 시스템 상태 갱신
updateSystem();
// 3. 뽀모도로 타이머 로직
if (!Timer::isPaused)
{
handlePomodoroTimer();
}
}
// ============================================================
// 인터럽트 서비스 루틴
// ============================================================
void encoderISR()
{
if (digitalRead(Config::ENCODER_DT_PIN) == HIGH)
{
Encoder::rotationCount++;
}
else
{
Encoder::rotationCount--;
}
}
// ============================================================
// 로터리 엔코더 버튼 처리
// ============================================================
void handleEncoderButton()
{
bool currentState = digitalRead(Config::ENCODER_SW_PIN);
unsigned long currentTime = millis();
// 버튼 눌림 감지
if (currentState == LOW && Button::lastState == HIGH)
{
Button::pressStartTime = currentTime;
Button::isPressed = true;
}
// 버튼 뗌 감지
if (currentState == HIGH && Button::lastState == LOW && Button::isPressed)
{
unsigned long pressDuration = currentTime - Button::pressStartTime;
if (pressDuration >= Config::LONG_PRESS_MS)
{
resetTimer();
Serial.println("타이머 리셋!");
}
else if (pressDuration >= Config::DEBOUNCE_MS)
{
togglePause();
}
Button::isPressed = false;
}
Button::lastState = currentState;
}
// ============================================================
// 로터리 엔코더 회전 처리
// ============================================================
void handleEncoderRotation()
{
if (!Timer::isPaused)
{
Encoder::lastRotationCount = Encoder::rotationCount;
return;
}
int diff = Encoder::rotationCount - Encoder::lastRotationCount;
if (diff != 0)
{
adjustTime(diff);
Encoder::lastRotationCount = Encoder::rotationCount;
}
}
// ============================================================
// 일시정지 토글
// ============================================================
void togglePause()
{
Timer::isPaused = !Timer::isPaused;
if (Timer::isPaused)
{
Timer::pausedTime = millis() - Timer::previousTime;
Serial.println("일시정지");
}
else
{
// 재개 시 일시정지했던 시점부터 이어서 시작
Timer::previousTime = millis() - Timer::pausedTime;
Timer::pausedTime = 0;
Serial.println("재개");
}
}
// ============================================================
// 타이머 리셋 (사용자 설정 시간 유지, 타이머만 처음부터)
// ============================================================
void resetTimer()
{
Timer::isBreakTime = false;
Timer::isPaused = false;
Timer::previousTime = millis();
Timer::pausedTime = 0;
// 사용자가 설정한 시간은 유지 (workTimeMs, breakTimeMs 리셋 안 함)
tone(Config::BUZZER_PIN, 2000, 100);
}
// ============================================================
// 시간 조절 (일시정지 상태에서)
// ============================================================
void adjustTime(int direction)
{
unsigned long *targetTime = Timer::isBreakTime ? &Timer::breakTimeMs : &Timer::workTimeMs;
long newTime = (long)*targetTime + (direction * Config::ONE_MINUTE_MS);
// 범위 제한
if (newTime < (long)Config::MIN_TIME_MS)
{
newTime = Config::MIN_TIME_MS;
}
else if (newTime > (long)Config::MAX_TIME_MS)
{
newTime = Config::MAX_TIME_MS;
}
*targetTime = (unsigned long)newTime;
// 시간 조절 시 타이머를 처음부터 시작하도록 리셋
Timer::pausedTime = 0;
if (Config::DEBUG_MODE)
{
Serial.print("시간 설정: ");
Serial.print(*targetTime / Config::ONE_MINUTE_MS);
Serial.println("분");
}
}
// ============================================================
// 시스템 갱신 함수
// ============================================================
void updateSystem()
{
updateLedIndicator();
updateSegmentDisplay();
}
// ============================================================
// 뽀모도로 타이머 핵심 로직
// ============================================================
void handlePomodoroTimer()
{
unsigned long currentTime = millis();
// millis() 오버플로우 처리
if (currentTime < Timer::previousTime)
{
Timer::previousTime = 0;
}
unsigned long elapsedTime = currentTime - Timer::previousTime;
unsigned long targetTime = Timer::isBreakTime ? Timer::breakTimeMs : Timer::workTimeMs;
// 시간 완료 체크
if (elapsedTime >= targetTime)
{
notifyPhaseComplete();
Timer::isBreakTime = !Timer::isBreakTime;
Timer::previousTime = currentTime;
}
}
// ============================================================
// 마지막 1분인지 확인
// ============================================================
bool isWarningTime()
{
unsigned long remainTime = calculateRemainTime();
return remainTime <= Config::WARNING_TIME_MS && remainTime > 0;
}
// ============================================================
// LED 상태 표시
// ============================================================
void updateLedIndicator()
{
bool blink = (millis() / Config::BLINK_INTERVAL_MS) % 2;
// 일시정지 중: 느린 깜빡임 (500ms)
if (Timer::isPaused)
{
bool pauseBlink = (millis() / 500) % 2;
digitalWrite(Config::WORK_LED_PIN, !Timer::isBreakTime && pauseBlink);
digitalWrite(Config::BREAK_LED_PIN, Timer::isBreakTime && pauseBlink);
}
// 마지막 1분: 빠른 깜빡임 (250ms)
else if (isWarningTime())
{
digitalWrite(Config::WORK_LED_PIN, !Timer::isBreakTime && blink);
digitalWrite(Config::BREAK_LED_PIN, Timer::isBreakTime && blink);
}
// 일반 상태: 상시 켜짐
else
{
digitalWrite(Config::WORK_LED_PIN, !Timer::isBreakTime);
digitalWrite(Config::BREAK_LED_PIN, Timer::isBreakTime);
}
}
// ============================================================
// 7세그먼트 디스플레이
// ============================================================
void updateSegmentDisplay()
{
unsigned long remainTime = calculateRemainTime();
int minute = remainTime / Config::ONE_SECOND_MS / 60;
int second = (remainTime / Config::ONE_SECOND_MS) % 60;
if (Config::DEBUG_MODE)
{
Serial.print("남은 시간: ");
Serial.print(minute);
Serial.print(":");
Serial.println(second);
}
displayTwoDigits(0, minute);
displayTwoDigits(2, second);
}
unsigned long calculateRemainTime()
{
unsigned long totalTime = Timer::isBreakTime ? Timer::breakTimeMs : Timer::workTimeMs;
unsigned long elapsedTime;
if (Timer::isPaused)
{
// 일시정지 중: pausedTime이 있으면 그 시점의 남은 시간, 없으면 초기 상태
if (Timer::pausedTime > 0)
{
elapsedTime = Timer::pausedTime;
}
else
{
// 초기 일시정지 상태: 설정 시간 그대로 표시 (27분 설정 → 27:00)
return totalTime;
}
}
else
{
elapsedTime = millis() - Timer::previousTime;
}
if (elapsedTime >= totalTime)
{
return 0;
}
return totalTime - elapsedTime;
}
void displayTwoDigits(int startPos, int value)
{
if (value < 10)
{
segment.display(startPos, 0);
segment.display(startPos + 1, value);
}
else
{
segment.display(startPos, value / 10);
segment.display(startPos + 1, value % 10);
}
}
// ============================================================
// 알림 함수
// ============================================================
void notifyPhaseComplete()
{
const char *message = Timer::isBreakTime ? "휴식 완료! 작업 시간 시작" : "작업 완료! 휴식 시간 시작";
Serial.println(message);
tone(Config::BUZZER_PIN, Config::BUZZER_FREQUENCY, Config::BUZZER_DURATION_MS);
}
working
stopWorking