개인적으로 사용하기 위한 만능 타이머 제작
시계
알람 ON/OFF
포모도르
1분 2분 3분... 마다 알람
슬립모드
부품
아두이노 나노
RTC 모듈
배터리
충전 모듈
스피커 좋은 것
OLED
상태 LED - 전원, 동작 지시, 알람 시간 켜기, 흔들리면 켜기
버튼 - 전원, Mode(포모도로, 시계, 알람, 분당 타이머, 10분 제어, 시간 제어,
진동 센서?
사람 감지?
소리 감지?
사전 조사
추가로 시계에 넣을 수 있는 유용한 기능들을 몇 가지 제안해 드립니다. 특히 스마트 워치나 최신 시계 앱에서 찾아볼 수 있는 기능들입니다.
⌚️ 만능 시계에 추가할 수 있는 유용한 기능 아이디어
🧠 시간 관리 및 효율성 증대 기능
- 스톱워치 및 랩 타임 기록: 운동, 실험, 또는 특정 작업 시간을 정밀하게 측정하고 구간 기록(랩 타임)을 저장할 수 있게 합니다.
- 미니 타이머/카운트다운 타이머 위젯: 현재 사용 중인 다른 앱이나 홈 화면에서도 남은 타이머 시간을 바로 확인할 수 있도록 합니다. (예: 요리, 짧은 휴식 등)
- 반복 알람 일시 중지/건너뛰기: 불필요한 알람이 울리기 전에 미리 알림을 받고 해당 알람을 비활성화하거나 건너뛸 수 있는 기능입니다. (예: 알람 시간 전에 이미 일어났을 경우)
- 알람 소리 점진적 증가 (크게/진동): 설정한 시간이 되면 알람 볼륨이나 진동을 서서히 증가시켜 부드럽게 기상할 수 있도록 합니다.
🌎 정보 및 환경 기능
- 세계 시간: 여러 도시의 현재 시간을 동시에 표시하거나 쉽게 전환하여 확인할 수 있는 기능입니다. (여행, 해외 업무 시 유용)
- 날짜 및 요일 표시: 기본 기능이지만, 만능 시계라면 달력 기능과 연동하여 특정 날짜의 일정을 빠르게 확인할 수 있게 할 수도 있습니다.
- 자명종 해제 미션/재미있는 알람: 알람을 끄기 위해 특정 미션(예: 수학 문제 풀기, 일정 걸음 수 채우기, 특정 위치 밟기 등)을 수행하게 하여 늦잠을 방지하는 기능입니다.
🛏️ 수면 및 건강 관련 기능
- 취침 시간 모드/수면 추적 기능: 사용자가 설정한 취침 시간에 맞추어 수면 모드로 전환하고, 잠자리에 든 시간과 수면 패턴을 기록하고 분석할 수 있게 합니다.
- 주간/월간 통계 기록: 알람 사용 패턴, 수면 시간, 포모도로 사이클 사용 횟수 등을 기록하고 통계로 보여주어 사용자가 자신의 시간 관리 습관을 파악하는 데 도움을 줍니다.
제안 드린 기능들을 참고하여 가장 필요하고 흥미로운 기능을 만능 시계에 추가해 보세요!
Gemini 코드 생성
🅿️ 아두이노 나노 마이크로프로세서에서 모드 5가지 일반적인 알람 모드, 포모도르 모드, 시계모드, 일정시간 반복 알람모드, 스톱워치 와 랩타임 모드로 동작하려고 해. 각 모드 선택 버튼을 GPIO 7에 버튼을 연결하여 각 모드를 수행하는 아주 모듈화된 코드를 작성해 줘.
🅿️ RTC DS1302 모듈을 사용해 년월일, 시간과 각 모드 상태에서 사용자에게 보여주는 정보는 OLED ssd1306을 사용하려고 해. 스피커도 연결하여 일정한 소리가 출력하도록 해. GPIO는 네가 구성해도 됩니다.
🅿️ 위 모두를 구현하는 아두이노 코드를 모듈화하여 변경사항 발생시 해당 모듈만 수정하도록 코드를 작성해 줘. 반복적으로 검토해 에러가 없는 코드를 부탁해.

훌륭한 아이디어입니다! 요청하신 만능 시계 기능을 아두이노 나노 환경에서 모듈화하여 구현하는 코드를 작성했습니다.
이 코드는 다음과 같은 원칙에 따라 설계되었습니다:
- 모듈화된 클래스: RTC, 디스플레이, 부저, 그리고 각 모드(Mode)를 별도의 클래스로 분리하여, 특정 기능에 변경이 필요할 경우 해당 클래스만 수정하면 되도록 했습니다.
- 비차단 (Non-blocking): 모든 타이밍(스톱워치, 포모도로, 버튼 디바운싱)은 millis()를 사용하여 프로그램이 멈추지 않고 실시간으로 동작하도록 했습니다.
- 단일 파일 구현: 아두이노 스케치 환경에 맞게 모든 클래스를 하나의 파일에 포함했습니다.
필요한 라이브러리:
이 코드는 다음 라이브러리를 사용합니다.
- U8g2: SSD1306 OLED 디스플레이 제어용.
- DS1302: RTC 모듈 제어용.
이 두 라이브러리를 아두이노 IDE의 라이브러리 관리자에서 설치해야 합니다.
하드웨어 연결 구성 (GPIO)
| 기능 | 부품 | 아두이노 Nano 핀 |
| 모드 선택 버튼 | Button | D7 (Mode Select / Next) |
| 설정/확인 버튼 | Button | D5 (Set / Confirm / Start/Stop) |
| 값 변경 버튼 | Button | D4 (Up / Change Value) |
| RTC CLK | DS1302 | D10 |
| RTC DAT | DS1302 | D9 |
| RTC RST | DS1302 | D8 |
| OLED SDA | SSD1306 (I2C) | A4 |
| OLED SCL | SSD1306 (I2C) | A5 |
| 부저 | Buzzer | D6 |
코드 요약 및 모듈화 설명
이 코드는 다음 8가지 섹션으로 철저하게 모듈화되어 있습니다.
- 설정 (CONFIGURATION): 모든 핀 번호 및 초기 설정(예: OLED 객체)이 정의되어 있어 하드웨어 변경 시 이 섹션만 수정하면 됩니다.
- RTC 모듈 (RtcModule): DS1302 모듈의 시간 읽기, 쓰기, 업데이트 기능을 모두 캡슐화합니다.
- 디스플레이 모듈 (DisplayModule): OLED 디스플레이 초기화, 버퍼 클리어, 그리고 4줄의 텍스트를 일관된 형식으로 출력하는 draw() 함수를 제공하여 각 모드가 디스플레이 세부 사항을 신경 쓰지 않도록 합니다.
- 부저 모듈 (BuzzerModule): 비차단 방식으로 부저를 울리거나 끄는 로직을 담당합니다.
- 버튼 처리 헬퍼: 버튼 디바운싱 로직을 구현하여 안정적인 버튼 입력을 보장합니다.
- 모드 클래스:
- BaseMode: 모든 모드가 상속받아야 하는 인터페이스(함수 정의) 역할을 합니다.
- ClockMode, GeneralAlarmMode, PomodoroMode, RepeatAlarmMode, StopwatchMode, SleepMode: 각 모드가 개별적인 로직, 디스플레이, 버튼 처리 방식을 독립적으로 구현합니다. 예를 들어, PomodoroMode의 로직을 수정하고 싶다면 해당 클래스 내부만 변경하면 됩니다.
- 모드 관리자 (ModeManager): 현재 활성화된 모드를 추적하고, 버튼 입력에 따라 모드를 전환하며, 적절한 모드의 update() 또는 draw() 함수를 호출하는 중앙 통제소 역할을 합니다.
- 메인 스케치 (setup 및 loop): 모듈 초기화와 함께 버튼 입력 감지 및 모드 관리자의 함수를 반복적으로 호출합니다.
🅿️ 버튼을 한 개 추가해 하나는 1분에서 60분까지 설정하는 버튼으로 하여 3분으로 설정할 경우 3분 마다 반복적으로 알람이 울리는 기능을 추가해 줘.
🅿️ 버튼은 몇 개를 사용했고 각 버튼의 기능이 무언지 설명해주세요.
제공해 주신 아두이노 코드(multifunction_clock.ino)를 기준으로 총 3개의 버튼이 사용되었으며, 각 버튼은 시계의 현재 모드에 따라 다양한 기능을 수행합니다.
아래는 각 버튼의 이름, 연결 핀, 그리고 주요 기능에 대한 설명입니다.
| 버튼 이름 | 핀 번호 (GPIO) | 주요 기능 |
| MODE (모드 선택) | D7 (PIN_MODE_SELECT) | 주요 기능: 시계의 모드를 순환하며 전환합니다. (시계 -> 알람 -> 포모도로 -> 반복 알람 -> 스톱워치 -> 슬립) |
| SET (설정/시작) | D5 (PIN_SET_CONFIRM) | 주요 기능: 특정 모드에서 설정 진입/다음 단계 이동/시작/정지/알람 끄기 기능을 수행합니다. |
| UP (값 증가/변경) | D4 (PIN_UP_CHANGE) | 주요 기능: 설정 단계에서 값 증가 또는 기능 전환 기능을 수행합니다. |
🅿️ SET 버튼이 너무 복잡한데, 버튼을 몇 개 더 추가하여 쉽고 직관적인 사용이 가능하도록 다시 코드를 생성하세요.
사용자님의 요청에 따라 기능을 더욱 쉽고 직관적으로 사용할 수 있도록 3개의 버튼을 5개의 버튼으로 확장하여 코드를 재구성했습니다.
이제 각 버튼은 하나의 주된 기능을 담당하여, 모드 전환, 설정 진입, 값 변경, 타이머 시작/정지, 그리고 최종 저장/알람 끄기 기능이 명확하게 분리됩니다.
제가 수정한 multifunction_clock.ino 파일을 아래에 제공합니다.
버튼 구성 변경 요약
| 핀 번호 | 이전 버튼 기능 | 새 버튼 기능 |
| D7 | MODE (모드 전환) | MODE (모드 전환) |
| D5 | SET (설정, 시작/정지, 저장) | SELECT (설정 진입, 다음 단계) |
| D4 | UP (값 증가/변경) | UP (값 증가/간격 변경) |
| D3 | (새로 추가됨) | START/STOP (타이머 시작/정지) |
| D2 | (새로 추가됨) | SAVE/OFF (설정 저장, 알람 끄기) |
이제 모든 모드에서 버튼 기능이 다음과 같이 분리되어 훨씬 직관적으로 작동합니다.
- SELECT(D5): 설정 모드 진입 및 설정 항목 이동 (시계, 알람 모드에서 사용)
- START/STOP(D3): 타이머 또는 알람을 실행/일시정지/정지 (포모도로, 반복 알람, 스톱워치 모드에서 사용)
- SAVE/OFF(D2): 설정 값을 최종적으로 저장하거나, 울리고 있는 부저를 끄는 데 사용됩니다.
🅿️ 모든 모듈에서 OLED에 시간이나 현재 상태와 모드를 보여주고 디스플레이를 업데이트하는 코드를 넣으세요. 버튼이 눌리거나 기능을 세팅하는 등 부저로 출력하는 소리를 내는 기능이 빠짐없이 들어 있나 확인해 주세요. 알람 음악 소리가 점점 커지도록 하는 부분에 추가적으로 구현할 것이 있다면 알려주세요.
사용자님께서 요청하신 대로,
- OLED 디스플레이 업데이트 로직을 재확인하여 모든 모드에서 시간, 상태, 모드 정보가 명확하게 표시되도록 했습니다.
- 버튼 입력 및 기능 수행 시 부저 소리를 추가했습니다.
- 일반적인 버튼 누름에는 짧고 높은 clickBeep() 소리가 나도록 했습니다.
- 스톱워치에서 랩타임을 기록할 때는 낮은 confirmBeep() 소리가 나도록 했습니다.
- 알람 소리 증폭(Crescendo) 기능을 BuzzerModule에 구현했습니다. 이 기능은 알람이 울리기 시작하면 60초 동안 소리의 주파수를 높이고 펄스 간격을 좁혀 점점 더 크고(지속적으로) 긴급하게 느껴지도록 시뮬레이션합니다.
🅿️ 이 코드에 인체감지 PIR 센서와 진동센서, 소리센서 모듈을 추가하고 LED를 두 개 추가하여 사람이나 소리나 진동을 감지한 경우 LED에 불을 켜고, OLED 화면을 켜는 기능을 추가하세요. OLED가 화면이 꺼지는 버튼을 추가해서 꺼졌을 떄에 센서가 검출하면 자동으로 켜지도록 코드를 추가하세요. 모듈화 해서 변경시 최소한의 수정을 할 수 있도록 수정해주세요.
요청하신 인체 감지(PIR), 진동, 소리 센서와 두 개의 LED, 그리고 OLED 화면 수동 끄기 버튼 기능을 추가하여 코드를 모듈화했습니다.
주요 변경 사항은 다음과 같습니다.
- 핀 정의 추가: PIR (D11), 진동 (D12), 소리 (D13) 센서와 LED 두 개 (A1, A2), 그리고 화면 끄기 버튼 (A3)을 정의했습니다.
- SensorModule (4번 섹션): 센서의 상태를 읽고, 두 개의 LED를 제어하며, 세 가지 센서 중 하나라도 감지되었는지 (isAnySensorActive())를 반환하는 기능을 모듈화했습니다.
- DisplayModule (3번 섹션 수정): OLED의 전원 상태를 제어하는 setPower(bool powerOn) 함수를 추가하여, 화면을 켜고 끄는 로직을 담당합니다.
- 전원 제어 로직 (loop()):
- 새로 추가된 화면 끄기 버튼 (PIN_SCREEN_OFF_BUTTON)을 누르면 isManualScreenOff 상태가 토글됩니다.
- isManualScreenOff 상태일 때, 센서가 감지되면 OLED와 LED가 자동으로 켜지고, 센서 감지가 사라지면 다시 꺼집니다.
- 부저 모듈이 5번 섹션으로, 버튼 헬퍼가 6번 섹션으로 재배치되었습니다.
아래는 수정된 multifunction_clock.ino 파일입니다.
이 코드는 OLED 화면이 수동으로 꺼진 상태(D-A3 버튼을 눌렀을 때)일지라도, PIR, 진동, 또는 소리 센서 중 하나라도 감지되면 화면과 LED가 자동으로 켜지도록 설계되었습니다. SensorModule과 DisplayModule을 사용하여 로직을 분리했기 때문에, 향후 센서 추가나 LED 제어 로직 변경 시 수정이 최소화될 것입니다.
🅿️ 추가한 3개의 센서가 반응을 감지하면 OLED에 영문으로 Motion, Sound, Vibration을 감지한 센서 줄만큼 모든 문장을 1초간 표시했다가 원래 상태로 돌아가는 코드를 모듈화해서 추가해 주세요.
요청하신 센서 감지 시 1초 동안 영어 메시지를 표시하는 기능을 모듈화하여 추가했습니다.
주요 변경 사항:
- SensorModule (4번 섹션):
- 현재의 개별 센서 상태를 묶어 반환하는 SensorStates 구조체와 getSensorStates() 메서드를 추가했습니다.
- DisplayModule (3번 섹션):
- 센서 감지 메시지("Motion", "Sound", "Vibration")를 화면에 출력하는 drawDetectionMessage() 메서드를 추가했습니다. 감지된 센서의 개수만큼 줄을 바꿔가며 표시합니다.
- loop() 함수 (9번 섹션):
- isDetectionDisplayActive 플래그와 detectionStartTime 변수를 추가하여 1초 타이머를 관리합니다.
- 센서가 감지되면(isAnySensorActive()가 true일 때) 타이머를 시작하고 isDetectionDisplayActive를 true로 설정합니다.
- 디스플레이 그리기 단계에서 이 플래그가 true이면 기존 모드 대신 drawDetectionMessage()를 호출하도록 우선순위를 부여했습니다.
아래는 모든 기능이 포함된 multifunction_clock.ino 파일입니다.
수정된 코드는 이제 센서 감지 시 1초 동안 감지된 센서 종류를 표시한 후 자동으로 원래 시계 모드로 복귀하며, 모든 기능이 기존의 모듈화된 구조를 유지하고 있습니다.
🅿️ 다시 한번 기능을 점검하고, 모듈을 잘 나누었는지 체크해주세요. 제가 어떤 기능을 수정하고 싶을 일이 발생할 수 있는 항목을 순서대로 나열하고 어느 부분을 검토해야 하는 지 알려주세요.
네, 코드의 모듈화 상태를 자세히 점검하고, 향후 기능 수정 시 검토해야 할 항목들을 우선순위별로 정리해 드리겠습니다.
1. 모듈화 점검 결과
전반적으로 코드는 매우 잘 모듈화되어 있습니다. 각 기능(시간, 디스플레이, 센서, 알람/타이머 모드)이 독립적인 클래스(Module)로 분리되어 있어, 한 모듈을 수정해도 다른 모듈에 미치는 영향을 최소화할 수 있습니다.
| 모듈 | 역할 | 평가 |
| Configuration (1) | 하드웨어 핀 및 상수 정의 | Excellent. 중앙 집중화되어 핀 변경이 용이합니다. |
| RtcModule (2) | 시계(DS1302) 시간 관리 | Excellent. 시간 설정/갱신 로직이 깔끔하게 분리되어 있습니다. |
| DisplayModule (3) | OLED 화면 출력 및 전원 관리 | Excellent. 화면 출력 로직(draw, drawDetectionMessage)과 전원 관리(setPower)가 분리되어 UI 변경이 쉽습니다. |
| SensorModule (4) | 센서 상태 확인 및 LED 제어 | Excellent. 센서의 상태 감지 및 LED 제어만 담당합니다. |
| BuzzerModule (5) | 부저 출력 및 알람 Crescendo 로직 | Excellent. 알람 소리 패턴, 버튼 클릭음 로직이 독립적입니다. |
| BaseMode / ModeManager (7, 8) | 모드별 고유 로직 및 모드 전환 관리 | Excellent. 각 모드의 update/draw/handleButton 메서드가 독립적입니다. |
| loop() (9) | 메인 루프 (시스템 컨트롤러) | Good. 버튼 입력 처리, 센서 타이머 관리, 모드 업데이트/그리기 순서 정의 등 시스템 전체 흐름을 관리합니다. |
2. 향후 기능 수정 시 검토 항목 (우선순위 순)
사용자로서 가장 자주 수정하고 싶을 만한 기능들을 **수정의 용이성(Easy) 및 빈도(Frequency)**를 고려하여 우선순위를 매겼습니다.
| 우선순위 | 수정하고 싶은 기능 | 검토해야 할 모듈 및 파일 위치 |
| 1순위 (UI/메시지) | 표시 텍스트, 폰트, 레이아웃 변경, 감지 메시지 내용/스타일 변경 | DisplayModule (Section 3) |
| 2순위 (모드 로직) | 포모도로 간격, 알람 시각/개수, 반복 알람 주기 등 모드별 핵심 작동 규칙 변경 | Mode Classes (Section 7) |
| 3순위 (타이머/센서 반응) | 센서 감지 시 1초 표시 시간을 2초로 변경, 화면 끄는 로직(수동/센서) 변경 | loop() (Section 9) |
| 4순위 (버튼/소리) | 버튼 디바운스 시간 변경, 부저 알람 소리 패턴, 버튼 클릭음 변경 | BuzzerModule (Section 5), Button Helper (Section 6) |
| 5순위 (하드웨어 핀) | LED, 부저, 센서, 버튼의 핀 번호 변경 | Configuration (Section 1) |
상세 검토 가이드
💡 1순위: 디스플레이 (UI / 메시지) 수정 가이드
이 영역은 사용자 피드백이 가장 많은 부분이며, DisplayModule에 집중되어 있습니다.
| 기능 | 검토 위치 | 상세 가이드 |
| 표준 화면 출력 | DisplayModule::draw() | u8g2.setFont() 호출 후 u8g2.drawStr()의 위치(Y축)와 출력할 문자열(line1 ~ line4)을 수정합니다. |
| 감지 메시지 출력 | DisplayModule::drawDetectionMessage() | 텍스트("Motion", "Sound", "Vibration")를 다른 언어로 바꾸거나, 표시 스타일(u8g2.drawStr(5, 30 + line * 12))을 변경할 때 이 함수를 수정합니다. |
💡 2순위: 모드별 작동 로직 수정 가이드
특정 모드의 작동 방식이나 기본값을 변경할 때 사용합니다.
| 모드 | 검토 위치 | 수정 항목 예시 |
| ClockMode | ClockMode::handleUpButton() | 시계 설정 시 증감 단위를 변경하거나, 설정 단계를 추가/삭제할 때 수정합니다. |
| GeneralAlarmMode | MAX_ALARMS 상수, 클래스 내 alarms[] 초기화, update() | 알람 개수(MAX_ALARMS)를 늘리거나, 알람이 울리는 조건을 변경할 때 수정합니다. |
| PomodoroMode | 클래스 내 workDuration, restDuration 상수 | 집중 및 휴식 시간을 25분/5분에서 다른 값으로 변경할 때 수정합니다. |
| RepeatAlarmMode | 클래스 내 intervalMinutes 초기값, update() | 기본 반복 간격(3분)을 변경하거나, 알람 주기 계산 방식을 수정합니다. |
💡 3순위: 시스템 타이머 및 전원 제어 수정 가이드
센서 감지 반응 시간이나, OLED 전원 관리 규칙을 바꿀 때 사용합니다.
| 기능 | 검토 위치 | 수정 항목 예시 |
| 센서 감지 표시 시간 변경 | loop() (Section 9) | if (millis() - detectionStartTime >= 1000) 에서 1000을 원하는 시간(밀리초)으로 변경합니다. |
| 화면 자동 켜짐/꺼짐 규칙 변경 | loop() (Section 9) | desiredOledPower를 결정하는 로직(if (isManualScreenOff) { ... } else { ... })을 수정하여, 예를 들어 일정 시간 센서 감지가 없으면 자동으로 꺼지도록 로직을 추가할 수 있습니다. |
💡 4순위: 부저/상호작용 피드백 수정 가이드
| 기능 | 검토 위치 | 수정 항목 예시 |
| 알람 소리 변경 | BuzzerModule 클래스 정의 | baseFreq, maxFreq, baseDelay, maxDelay 상수를 조정하여 알람 소리(주파수, 펄스 간격)를 변경합니다. |
| 버튼 클릭음 변경 | BuzzerModule::clickBeep(), BuzzerModule::confirmBeep() | clickFreq, clickDuration을 변경하여 버튼 피드백 소리의 높낮이와 길이를 변경합니다. |
| 버튼 디바운스 변경 | Button Helper (Section 6) | debounceDelay 상수의 값을 변경하여 버튼 인식 안정성을 조정합니다. |
// 필요한 라이브러리 포함 (Arduino IDE에서 반드시 설치해야 합니다)
#include <U8g2lib.h> // OLED Display Library
#include <DS1302.h> // RTC DS1302 Library
#include <Wire.h> // I2C for OLED (used by U8g2)
// ====================================================================
// 1. 설정 (CONFIGURATION)
// ====================================================================
// --- 핀 정의 (총 5개 버튼) ---
const int PIN_MODE_SELECT = 7; // 모드 순환 전환 (MODE)
const int PIN_SELECT_NEXT = 5; // 설정 진입 및 다음 단계 이동 (SELECT)
const int PIN_UP_CHANGE = 4; // 값 증가 및 변경 (UP)
const int PIN_START_STOP = 3; // 타이머/알람 시작 및 정지 (START/STOP)
const int PIN_SAVE_ALARM_OFF = 2; // 설정 저장 및 알람/부저 끄기 (SAVE/OFF)
const int PIN_BUZZER = 6; // 부저 출력 핀 (GPIO 6)
// --- 센서 및 LED 핀 정의 (NEW) ---
const int PIN_PIR_SENSOR = 11; // 인체 감지 센서 (디지털 입력)
const int PIN_VIBRATION_SENSOR = 12; // 진동 센서 (디지털 입력)
const int PIN_SOUND_SENSOR = 13; // 소리 센서 (디지털 입력)
const int PIN_LED_1 = A1; // LED 1
const int PIN_LED_2 = A2; // LED 2
const int PIN_SCREEN_OFF_BUTTON = A3; // OLED 화면 수동 끄기 버튼
// DS1302 RTC 핀 정의 (CLK, DAT, RST)
const int RTC_CLK_PIN = 10;
const int RTC_DAT_PIN = 9;
const int RTC_RST_PIN = 8;
// OLED 설정 (SSD1306, 128x64, I2C 주소 0x3C)
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);
// 전역 상태 변수
bool isManualScreenOff = false; // 화면 수동 끄기/켜기 상태 플래그
unsigned long detectionStartTime = 0; // 감지 메시지 시작 시간
bool isDetectionDisplayActive = false; // 감지 메시지 활성화 플래그
// ====================================================================
// 2. RTC 모듈 (RTC_Module)
// ====================================================================
DS1302 rtc(RTC_RST_PIN, RTC_CLK_PIN, RTC_DAT_PIN);
class RtcModule {
private:
Time t;
public:
RtcModule() {
rtc.writeProtect(false);
rtc.halt(false);
}
void updateTime() {
t = rtc.getTime();
}
Time getCurrentTime() const {
return t;
}
void setTime(const Time& newTime) {
rtc.setTime(newTime);
}
String getTimeString(bool includeDate = false) {
String s = "";
if (includeDate) {
s += String(t.year) + "/";
s += (t.mon < 10 ? "0" : "") + String(t.mon) + "/";
s += (t.date < 10 ? "0" : "") + String(t.date) + " ";
}
s += (t.hour < 10 ? "0" : "") + String(t.hour) + ":";
s += (t.min < 10 ? "0" : "") + String(t.min) + ":";
s += (t.sec < 10 ? "0" : "") + String(t.sec);
return s;
}
};
RtcModule rtcModule;
// ====================================================================
// 4. 센서 및 LED 모듈 (SensorModule) - 상태 반환 구조체 추가
// ====================================================================
class SensorModule {
private:
bool pirActive = false;
bool vibrationActive = false;
bool soundActive = false;
public:
// NEW: 개별 센서 상태를 묶어 반환하는 구조체
struct SensorStates {
bool pir;
bool vibration;
bool sound;
};
void setup() {
// 센서 핀 설정 (디지털 입력)
pinMode(PIN_PIR_SENSOR, INPUT);
pinMode(PIN_VIBRATION_SENSOR, INPUT);
pinMode(PIN_SOUND_SENSOR, INPUT);
// LED 핀 설정 (출력)
pinMode(PIN_LED_1, OUTPUT);
pinMode(PIN_LED_2, OUTPUT);
digitalWrite(PIN_LED_1, LOW);
digitalWrite(PIN_LED_2, LOW);
}
void update() {
// 센서 상태 업데이트 (HIGH를 감지로 가정)
pirActive = digitalRead(PIN_PIR_SENSOR) == HIGH;
vibrationActive = digitalRead(PIN_VIBRATION_SENSOR) == HIGH;
soundActive = digitalRead(PIN_SOUND_SENSOR) == HIGH;
}
// 세 가지 센서 중 하나라도 활성 상태인지 확인
bool isAnySensorActive() const {
return pirActive || vibrationActive || soundActive;
}
// NEW: 현재의 개별 센서 상태를 반환
SensorStates getSensorStates() const {
return {pirActive, vibrationActive, soundActive};
}
// OLED 상태와 동기화하여 LED 제어
void controlLeds(bool on) {
if (on) {
digitalWrite(PIN_LED_1, HIGH);
digitalWrite(PIN_LED_2, HIGH);
} else {
digitalWrite(PIN_LED_1, LOW);
digitalWrite(PIN_LED_2, LOW);
}
}
};
SensorModule sensorModule;
// ====================================================================
// 3. 디스플레이 모듈 (Display_Module) - 전원 및 감지 메시지 제어 기능 추가
// ====================================================================
class DisplayModule {
private:
bool isPoweredOn = true;
public:
DisplayModule() {
u8g2.begin();
u8g2.setFontMode(1);
u8g2.setFontDirection(0);
u8g2.enableUTF8Print();
u8g2.setPowerSave(0); // 초기에는 켜짐
}
// OLED 전원 켜기/끄기 제어
void setPower(bool powerOn) {
if (powerOn && !isPoweredOn) {
u8g2.setPowerSave(0); // 전원 켜기
isPoweredOn = true;
} else if (!powerOn && isPoweredOn) {
u8g2.clearDisplay(); // 화면 내용을 지워 전력 소모 최소화
u8g2.setPowerSave(1); // 전원 끄기
isPoweredOn = false;
}
}
bool getPowerState() const {
return isPoweredOn;
}
void clear() {
if(isPoweredOn) u8g2.clearBuffer();
}
void draw(const char* line1, const char* line2 = "", const char* line3 = "", const char* line4 = "") {
if (!isPoweredOn) return; // 전원 꺼짐 상태면 그리지 않음
clear();
u8g2.setFont(u8g2_font_unifont_t_korean);
// Line 1: 상단 작은 글씨 (모드/상태)
u8g2.setFont(u8g2_font_9x15_t_korean);
u8g2.drawStr(0, 15, line1);
// Line 2: 중간 큰 글씨 (핵심 정보/시간)
u8g2.setFont(u8g2_font_inb30_mn);
u8g2.drawStr(0, 48, line2);
// Line 3: 중간 작은 글씨 (부가 정보/랩타임)
u8g2.setFont(u8g2_font_7x13_t_korean);
u8g2.drawStr(0, 60, line3);
// Line 4: 하단 작은 글씨 (안내)
u8g2.setFont(u8g2_font_7x13_t_korean);
u8g2.drawStr(0, 63, line4);
u8g2.sendBuffer();
}
// NEW: 감지 메시지 오버레이 그리기
void drawDetectionMessage(const SensorModule::SensorStates& states) {
if (!isPoweredOn) return;
clear();
// 헤더: SENSOR DETECTED
u8g2.setFont(u8g2_font_9x15_t_korean);
u8g2.drawStr(0, 15, "SENSOR DETECTED");
// 감지된 센서 메시지 출력
int line = 0;
u8g2.setFont(u8g2_font_7x13_t_korean); // 중간 크기 폰트
if (states.pir) {
u8g2.drawStr(5, 30 + line * 12, " > Motion"); line++;
}
if (states.sound) {
u8g2.drawStr(5, 30 + line * 12, " > Sound"); line++;
}
if (states.vibration) {
u8g2.drawStr(5, 30 + line * 12, " > Vibration"); line++;
}
// 푸터: 타이머 안내
u8g2.setFont(u8g2_font_7x13_t_korean);
u8g2.drawStr(0, 63, "메인 화면으로 1초 후 전환...");
u8g2.sendBuffer();
}
};
DisplayModule displayModule;
// ====================================================================
// 5. 부저 모듈 (Buzzer_Module) - Crescendo/Ramping 기능
// ====================================================================
class BuzzerModule {
private:
bool isPlaying = false;
unsigned long lastToggleTime = 0;
// Ramping properties
unsigned long rampStartTime = 0;
const unsigned long maxRampDuration = 60000; // 60초 동안 알람 소리 증폭
const int baseFreq = 1000;
const int maxFreq = 2000;
const int baseDelay = 100; // 펄스 간 최소 지연 (최대 소리)
const int maxDelay = 300; // 펄스 간 최대 지연 (최소 소리)
const int clickFreq = 2000;
const int clickDuration = 20;
public:
void update() {
if (isPlaying) {
// 경과 시간 계산 및 진행률(0.0 ~ 1.0) 계산
unsigned long elapsedRamp = millis() - rampStartTime;
float progress = min(1.0f, (float)elapsedRamp / maxRampDuration);
// 주파수 증가: 1000Hz -> 2000Hz
int currentFreq = baseFreq + (int)((maxFreq - baseFreq) * progress);
// 펄스 딜레이 감소: 300ms -> 100ms (소리가 더 자주 울림 = 더 크게 들림)
int pulseDelay = maxDelay - (int)((maxDelay - baseDelay) * progress);
if (millis() - lastToggleTime > pulseDelay) {
if (digitalRead(PIN_BUZZER) == HIGH) {
noTone(PIN_BUZZER);
digitalWrite(PIN_BUZZER, LOW);
} else {
tone(PIN_BUZZER, currentFreq);
digitalWrite(PIN_BUZZER, HIGH);
}
lastToggleTime = millis();
}
}
}
void startAlarm() {
isPlaying = true;
rampStartTime = millis(); // 증폭 시작 시간 기록
lastToggleTime = 0;
}
void stopAlarm() {
isPlaying = false;
noTone(PIN_BUZZER);
digitalWrite(PIN_BUZZER, LOW);
}
bool isAlarming() const {
return isPlaying;
}
// NEW: 버튼 클릭 피드백 소리 (짧고 높음)
void clickBeep() {
tone(PIN_BUZZER, clickFreq, clickDuration);
}
// NEW: 기능 확인 피드백 소리 (낮고 보통 길이)
void confirmBeep() {
tone(PIN_BUZZER, 500, 100);
}
};
BuzzerModule buzzerModule;
// ====================================================================
// 6. 버튼 처리 헬퍼 (Button Helper) - 변수 추가
// ====================================================================
unsigned long lastDebounceTime_Mode = 0;
unsigned long lastDebounceTime_Select = 0;
unsigned long lastDebounceTime_Up = 0;
unsigned long lastDebounceTime_StartStop = 0;
unsigned long lastDebounceTime_SaveOff = 0;
unsigned long lastDebounceTime_ScreenOff = 0; // NEW
const unsigned long debounceDelay = 50;
// 버튼 상태 읽기 및 디바운싱
bool readButton(int pin, unsigned long& lastDebounce) {
bool reading = digitalRead(pin) == LOW; // 풀업 저항을 가정하여 LOW가 눌린 상태
if (reading) {
if (millis() - lastDebounce > debounceDelay) {
lastDebounce = millis();
return true;
}
} else {
lastDebounce = millis();
}
return false;
}
// ====================================================================
// 7. 모드 인터페이스 및 기본 모드 (BaseMode & Modes)
// ====================================================================
class BaseMode {
protected:
int settingStep = 0; // 설정 단계 (0: 일반 상태, 1, 2, 3...: 설정 중)
public:
virtual void enter() = 0;
virtual void update() = 0;
virtual void draw() = 0;
virtual void handleModeButton() = 0;
// SET 버튼 기능 분리:
virtual void handleSelectButton() = 0; // 설정 진입 / 다음 단계
virtual void handleStartStopButton() = 0; // 시작 / 정지
virtual void handleSaveAlarmOffButton() = 0; // 저장 / 알람 끄기
// UP 버튼 기능
virtual void handleUpButton() = 0;
};
// --- Clock Mode ---
class ClockMode : public BaseMode {
private:
Time timeToSet;
public:
void enter() override {
settingStep = 0;
buzzerModule.stopAlarm();
}
void update() override {
if (settingStep == 0) {
rtcModule.updateTime();
}
}
void draw() override {
if (!displayModule.getPowerState()) return; // OLED 꺼짐 상태면 그리지 않음
char buffer[20];
Time t = (settingStep == 0) ? rtcModule.getCurrentTime() : timeToSet;
const char* days[] = {"?", "일", "월", "화", "수", "목", "금", "토"};
String status = "시계 모드";
status += " (" + String(days[t.day]) + ")";
// Line 2: 시간 (Setting 중에는 깜빡이도록)
// 설정 중에는 설정 중인 필드만 깜빡이도록 처리
bool blink = (millis() / 500) % 2 == 0;
if (settingStep == 0) {
sprintf(buffer, "%02d:%02d:%02d", t.hour, t.min, t.sec);
} else if (settingStep == 1) { // 년도 설정
sprintf(buffer, "%02d/%02d %02d:%02d", (blink ? 0 : t.mon), t.date, t.hour, t.min);
} else if (settingStep == 2) { // 월 설정
sprintf(buffer, "%02d/%02d %02d:%02d", t.mon, (blink ? 0 : t.date), t.hour, t.min);
} else if (settingStep == 3) { // 일 설정
sprintf(buffer, "%02d/%02d %02d:%02d", (blink ? 0 : t.mon), t.date, t.hour, t.min);
} else if (settingStep == 4) { // 시 설정
sprintf(buffer, "%02d/%02d %02d:%02d", t.mon, t.date, (blink ? 0 : t.hour), t.min);
} else if (settingStep == 5) { // 분 설정
sprintf(buffer, "%02d/%02d %02d:%02d", t.mon, t.date, t.hour, (blink ? 0 : t.min));
} else if (settingStep == 6) { // 요일 설정
sprintf(buffer, "%02d/%02d %02d:%02d", t.mon, t.date, t.hour, t.min); // 요일은 Line 3에 표시
} else {
sprintf(buffer, "%02d:%02d:%02d", t.hour, t.min, t.sec);
}
// Line 3: 날짜 및 설정 상태
char dateBuffer[20];
sprintf(dateBuffer, "%d년 %d월 %d일", t.year + 2000, t.mon, t.date);
// Line 4: 안내 메시지
const char* guide = (settingStep == 0) ?
"SELECT(D5)로 설정 시작" :
"UP(D4)로 변경 / SELECT(D5)로 다음 / SAVE(D2)로 저장";
displayModule.draw(status.c_str(), buffer, dateBuffer, guide);
}
void handleModeButton() override {}
void handleSelectButton() override {
if (settingStep == 0) {
// 설정 시작
buzzerModule.confirmBeep(); // 설정 진입 확인음
timeToSet = rtcModule.getCurrentTime();
settingStep = 1; // 년도 설정으로 시작
} else if (settingStep < 6) {
// 다음 단계로 이동
settingStep++;
}
}
void handleStartStopButton() override {
// 시계 모드에서는 사용하지 않음
}
void handleSaveAlarmOffButton() override {
if (buzzerModule.isAlarming()) {
buzzerModule.stopAlarm();
return;
}
if (settingStep > 0) {
// 설정 저장
buzzerModule.confirmBeep(); // 저장 확인음
timeToSet.sec = 0; // 초는 0으로 리셋
rtcModule.setTime(timeToSet);
settingStep = 0; // 설정 완료
}
}
void handleUpButton() override {
if (settingStep > 0) {
buzzerModule.clickBeep(); // 값 변경 시 피드백
}
if (settingStep == 1) { // 년도 설정
timeToSet.year = (timeToSet.year + 1) % 100; // 00~99
} else if (settingStep == 2) { // 월 설정
timeToSet.mon = (timeToSet.mon % 12) + 1;
} else if (settingStep == 3) { // 일 설정
timeToSet.date = (timeToSet.date % 31) + 1;
} else if (settingStep == 4) { // 시 설정
timeToSet.hour = (timeToSet.hour + 1) % 24;
} else if (settingStep == 5) { // 분 설정
timeToSet.min = (timeToSet.min + 1) % 60;
} else if (settingStep == 6) { // 요일 설정
timeToSet.day = (timeToSet.day % 7) + 1; // 1~7
}
}
};
// --- General Alarm Mode (5 Alarms) ---
struct Alarm {
int hour;
int minute;
bool enabled;
};
const int MAX_ALARMS = 5;
class GeneralAlarmMode : public BaseMode {
private:
Alarm alarms[MAX_ALARMS] = {
{8, 0, false}, {10, 30, false}, {14, 0, false}, {18, 45, false}, {22, 0, false}
};
int currentAlarm = 0;
public:
void enter() override {
settingStep = 0;
currentAlarm = 0;
buzzerModule.stopAlarm();
}
void update() override {
// 알람 체크 (매 분 0초에만 체크하여 부하 줄이기)
Time t = rtcModule.getCurrentTime();
if (t.sec == 0) {
for (int i = 0; i < MAX_ALARMS; ++i) {
if (alarms[i].enabled && alarms[i].hour == t.hour && alarms[i].minute == t.min) {
buzzerModule.startAlarm(); // 알람 시작 (Crescendo 적용됨)
}
}
}
buzzerModule.update();
}
void draw() override {
if (!displayModule.getPowerState()) return; // OLED 꺼짐 상태면 그리지 않음
char buffer[20];
String status = "알람 모드 (" + String(currentAlarm + 1) + "/" + String(MAX_ALARMS) + ")";
Alarm& current = alarms[currentAlarm];
// Line 2: 알람 시간
bool blink = (millis() / 500) % 2 == 0;
if (settingStep == 0 || settingStep == 1) { // 일반/ON/OFF
sprintf(buffer, "%02d:%02d", current.hour, current.minute);
} else if (settingStep == 2) { // 시 설정 중
sprintf(buffer, (blink ? " :%02d" : "%02d:%02d"), current.hour, current.minute);
} else if (settingStep == 3) { // 분 설정 중
sprintf(buffer, (blink ? "%02d: " : "%02d:%02d"), current.hour, current.minute);
}
// Line 3: 상태 및 안내
String line3 = current.enabled ? "상태: ON" : "상태: OFF";
if (settingStep == 1) line3 = blink ? "ON/OFF 설정" : (current.enabled ? "ON" : "OFF");
const char* guide = "MODE(D7)로 알람 전환 / SELECT(D5)로 설정";
if (settingStep == 1) guide = "UP(D4)로 ON/OFF / SELECT(D5)로 시간 설정";
else if (settingStep == 2) guide = "UP(D4)로 시 변경 / SELECT(D5)로 분 설정";
else if (settingStep == 3) guide = "UP(D4)로 분 변경 / SAVE(D2)로 저장 및 종료";
else if (buzzerModule.isAlarming()) guide = "SAVE(D2)를 눌러 알람 끄기!";
displayModule.draw(status.c_str(), buffer, line3.c_str(), guide);
}
void handleModeButton() override {
if (settingStep == 0) {
// 일반 모드에서 MODE 버튼: 다음 알람으로 전환
currentAlarm = (currentAlarm + 1) % MAX_ALARMS;
}
}
void handleSelectButton() override {
if (settingStep == 0) {
settingStep = 1; // ON/OFF 설정 시작
buzzerModule.confirmBeep();
} else if (settingStep == 1) {
settingStep = 2; // 시 설정 시작
} else if (settingStep == 2) {
settingStep = 3; // 분 설정 시작
} else if (settingStep == 3) {
settingStep = 0; // 실수로 또 누를 경우를 대비해 설정 종료
}
}
void handleStartStopButton() override {
// 이 모드에서는 사용하지 않음
}
void handleSaveAlarmOffButton() override {
if (buzzerModule.isAlarming()) {
buzzerModule.stopAlarm();
return;
}
if (settingStep > 0) {
settingStep = 0; // 설정 완료
buzzerModule.confirmBeep(); // 저장 확인음
}
}
void handleUpButton() override {
Alarm& current = alarms[currentAlarm];
if (settingStep == 1) { // ON/OFF
current.enabled = !current.enabled;
} else if (settingStep == 2) { // 시 설정
current.hour = (current.hour + 1) % 24;
} else if (settingStep == 3) { // 분 설정
current.minute = (current.minute + 1) % 60;
}
}
};
// --- Pomodoro Mode ---
class PomodoroMode : public BaseMode {
private:
unsigned long startTime = 0;
unsigned long duration = 0;
bool isWorking = true;
bool isRunning = false;
const unsigned long workDuration = 25 * 60 * 1000UL;
const unsigned long restDuration = 5 * 60 * 1000UL;
public:
void enter() override {
settingStep = 0;
isRunning = false;
isWorking = true;
duration = workDuration;
startTime = millis() - (workDuration - 1);
buzzerModule.stopAlarm();
}
void update() override {
if (!isRunning) return;
unsigned long elapsed = millis() - startTime;
if (elapsed >= duration) {
// 시간 완료, 다음 단계로 전환
buzzerModule.startAlarm(); // 알람 시작 (Crescendo 적용됨)
isRunning = false;
isWorking = !isWorking;
duration = isWorking ? workDuration : restDuration;
startTime = millis();
}
buzzerModule.update();
}
void draw() override {
if (!displayModule.getPowerState()) return; // OLED 꺼짐 상태면 그리지 않음
char buffer[20];
String status = isWorking ? "집중 시간 (25분)" : "휴식 시간 (5분)";
status = "포모도로: " + status;
unsigned long remaining = duration - (isRunning ? (millis() - startTime) : 0);
int minutes = remaining / (1000 * 60);
int seconds = (remaining / 1000) % 60;
sprintf(buffer, "%02d:%02d", minutes, seconds);
String line3 = isRunning ? "동작 중" : (isWorking ? "준비 완료" : "휴식 완료!");
const char* guide = isRunning ?
"START/STOP(D3)로 일시정지" :
"START/STOP(D3)로 시작 / UP(D4)로 단계 전환";
if (buzzerModule.isAlarming()) guide = "SAVE(D2)를 눌러 알람 끄기!";
displayModule.draw(status.c_str(), buffer, line3.c_str(), guide);
}
void handleModeButton() override {}
void handleSelectButton() override {}
void handleStartStopButton() override {
if (buzzerModule.isAlarming()) {
// 알람이 울리면 알람 끄기
buzzerModule.stopAlarm();
return;
}
if (isRunning) {
isRunning = false;
duration = millis() - startTime; // 경과 시간 저장
buzzerModule.confirmBeep(); // 정지 확인음
} else {
// 정지 상태에서 시작 (혹은 재시작)
if (duration < 1000) { // 완료 상태였다면 재설정
duration = isWorking ? workDuration : restDuration;
}
startTime = millis() - (duration - workDuration);
isRunning = true;
buzzerModule.confirmBeep(); // 시작 확인음
}
}
void handleSaveAlarmOffButton() override {
// 알람이 울릴 때 끄는 용도로만 사용
if (buzzerModule.isAlarming()) {
buzzerModule.stopAlarm();
}
}
void handleUpButton() override {
if (!isRunning) {
// 단계 전환 (집중 <-> 휴식)
isWorking = !isWorking;
duration = isWorking ? workDuration : restDuration;
startTime = millis() - (duration - 1); // 초기화
buzzerModule.clickBeep(); // 단계 전환 피드백
}
}
};
// --- Scheduled Repeat Alarm Mode (일정 시간 반복 알람 모드) ---
class RepeatAlarmMode : public BaseMode {
private:
int intervalMinutes = 3; // 기본값을 3분으로 설정 (1분 ~ 60분)
unsigned long lastAlarmTime = 0;
bool isRunning = false;
public:
void enter() override {
isRunning = false;
buzzerModule.stopAlarm();
}
void update() override {
if (!isRunning) return;
unsigned long interval = (unsigned long)intervalMinutes * 60 * 1000UL;
if (millis() - lastAlarmTime >= interval) {
buzzerModule.startAlarm(); // 알람 시작 (Crescendo 적용됨)
lastAlarmTime = millis(); // 알람 울린 후 타이머 리셋
}
buzzerModule.update();
}
void draw() override {
if (!displayModule.getPowerState()) return; // OLED 꺼짐 상태면 그리지 않음
char buffer[20];
String status = "반복 알람 모드";
// Line 2: 간격 정보
sprintf(buffer, "%d분 간격", intervalMinutes);
// Line 3: 상태 및 남은 시간
String line3;
if (isRunning) {
unsigned long interval = (unsigned long)intervalMinutes * 60 * 1000UL;
unsigned long nextAlarm = interval - (millis() - lastAlarmTime);
if (nextAlarm > interval) nextAlarm = interval;
int minutes = nextAlarm / (1000 * 60);
int seconds = (nextAlarm / 1000) % 60;
line3 = "다음 알람: " + String(minutes) + "분 " + String(seconds) + "초 후";
} else {
line3 = "현재 정지됨. 설정 간격: " + String(intervalMinutes) + "분";
}
// Line 4: 안내
const char* guide = isRunning ?
"START/STOP(D3)로 정지 / SAVE(D2)로 알람 끄기" :
"UP(D4)로 간격 변경(1~60) / START/STOP(D3)로 시작";
if (buzzerModule.isAlarming()) guide = "SAVE(D2)를 눌러 알람 끄기! (다음 주기 시작)";
displayModule.draw(status.c_str(), buffer, line3.c_str(), guide);
}
void handleModeButton() override {}
void handleSelectButton() override {}
void handleStartStopButton() override {
// ON/OFF 토글
isRunning = !isRunning;
if (isRunning) {
lastAlarmTime = millis(); // 시작 시 타이머 리셋
buzzerModule.confirmBeep(); // 시작 확인음
} else {
buzzerModule.confirmBeep(); // 정지 확인음
}
}
void handleSaveAlarmOffButton() override {
if (buzzerModule.isAlarming()) {
buzzerModule.stopAlarm();
if (isRunning) {
lastAlarmTime = millis(); // 알람 끈 후 다음 사이클 즉시 시작
}
return;
}
}
void handleUpButton() override {
if (!isRunning) {
// 정지 상태일 때만 간격 변경 가능 (1~60분)
intervalMinutes = (intervalMinutes % 60) + 1;
}
}
};
// --- Stopwatch & Lap Time Mode ---
class StopwatchMode : public BaseMode {
private:
unsigned long startTime = 0;
unsigned long pausedTime = 0;
bool isRunning = false;
unsigned long lapTimes[5] = {0};
int lapCount = 0;
public:
void enter() override {
isRunning = false;
startTime = 0;
pausedTime = 0;
lapCount = 0;
buzzerModule.stopAlarm();
}
void update() override {
buzzerModule.update();
}
void draw() override {
if (!displayModule.getPowerState()) return; // OLED 꺼짐 상태면 그리지 않음
char buffer[20];
String status = "스톱워치 모드";
unsigned long elapsed = pausedTime;
if (isRunning) {
elapsed = millis() - startTime;
}
int hours = elapsed / (1000 * 60 * 60);
int minutes = (elapsed / (1000 * 60)) % 60;
int seconds = (elapsed / 1000) % 60;
int centiseconds = (elapsed % 1000) / 10;
// Line 2: 현재 시간
sprintf(buffer, "%02d:%02d:%02d.%02d", hours, minutes, seconds, centiseconds);
// Line 3: 랩 타임 표시 (가장 최근 랩)
String lapInfo = "";
if (lapCount > 0) {
unsigned long lastLap = lapTimes[lapCount - 1];
int l_min = (lastLap / (1000 * 60)) % 60;
int l_sec = (lastLap / 1000) % 60;
int l_csec = (lastLap % 1000) / 10;
lapInfo = "Lap " + String(lapCount) + ": " + String(l_min < 10 ? "0" : "") + String(l_min) + ":" + String(l_sec < 10 ? "0" : "") + String(l_sec) + "." + String(l_csec < 10 ? "0" : "") + String(l_csec);
} else {
lapInfo = "랩 기록 없음";
}
// Line 4: 안내
const char* guide = isRunning ?
"UP(D4)로 랩 기록 / START/STOP(D3)로 정지" :
"START/STOP(D3)로 시작 / UP(D4)로 초기화";
if (!isRunning && elapsed > 0) guide = "START/STOP(D3)로 재개 / UP(D4)로 초기화";
displayModule.draw(status.c_str(), buffer, lapInfo.c_str(), guide);
}
void handleModeButton() override {}
void handleSelectButton() override {}
void handleStartStopButton() override {
if (isRunning) {
// 정지
pausedTime = millis() - startTime;
isRunning = false;
buzzerModule.confirmBeep(); // 정지 확인음
} else {
// 시작 또는 재개
startTime = millis() - pausedTime;
isRunning = true;
pausedTime = 0;
buzzerModule.confirmBeep(); // 시작 확인음
}
}
void handleSaveAlarmOffButton() override {
// 알람이 울릴 때 끄는 용도로만 사용
if (buzzerModule.isAlarming()) {
buzzerModule.stopAlarm();
}
}
void handleUpButton() override {
if (isRunning) {
// 랩 타임 기록 (최대 5개)
if (lapCount < 5) {
lapTimes[lapCount] = millis() - startTime;
lapCount++;
buzzerModule.confirmBeep(); // 랩 기록 확인음
}
} else {
// 초기화
startTime = 0;
pausedTime = 0;
lapCount = 0;
buzzerModule.confirmBeep(); // 초기화 확인음
}
}
};
// --- Sleep Mode ---
class SleepMode : public BaseMode {
public:
void enter() override {
// OLED 전원 제어는 loop()에서 처리하므로 여기서는 모듈 전원 제어만 수행
buzzerModule.stopAlarm();
}
void update() override {}
void draw() override {
if (!displayModule.getPowerState()) return; // OLED 꺼짐 상태면 그리지 않음
// OLED가 켜져 있다면, 슬립 모드 안내 표시
displayModule.draw("슬립 모드", "화면 꺼짐", "MODE 버튼을 누르면 다음 모드로 전환됩니다.", "센서 감지 시 화면 자동 켜짐");
}
void handleModeButton() override {}
void handleSelectButton() override {}
void handleStartStopButton() override {}
void handleSaveAlarmOffButton() override {}
void handleUpButton() override {}
};
// ====================================================================
// 8. 모드 관리자 (Mode_Manager)
// ====================================================================
class ModeManager {
private:
BaseMode* modes[6];
int currentModeIndex = 0;
const char* modeNames[6] = {"시계", "알람", "포모도로", "반복알람", "스톱워치", "슬립"};
public:
ModeManager() {
modes[0] = new ClockMode();
modes[1] = new GeneralAlarmMode();
modes[2] = new PomodoroMode();
modes[3] = new RepeatAlarmMode();
modes[4] = new StopwatchMode();
modes[5] = new SleepMode();
}
void initialize() {
modes[currentModeIndex]->enter();
}
void updateCurrentMode() {
modes[currentModeIndex]->update();
}
void drawCurrentMode() {
modes[currentModeIndex]->draw();
}
void switchMode() {
currentModeIndex = (currentModeIndex + 1) % 6;
modes[currentModeIndex]->enter();
}
void handleModeButton() {
buzzerModule.clickBeep(); // 버튼 피드백
if (currentModeIndex == 5) {
modes[currentModeIndex]->handleModeButton();
}
switchMode();
}
// 새로운 버튼 핸들러
void handleSelectButton() {
buzzerModule.clickBeep(); // 버튼 피드백
modes[currentModeIndex]->handleSelectButton();
}
void handleStartStopButton() {
buzzerModule.clickBeep(); // 버튼 피드백
modes[currentModeIndex]->handleStartStopButton();
}
void handleSaveAlarmOffButton() {
buzzerModule.clickBeep(); // 버튼 피드백
modes[currentModeIndex]->handleSaveAlarmOffButton();
}
void handleUpButton() {
buzzerModule.clickBeep(); // 버튼 피드백
modes[currentModeIndex]->handleUpButton();
}
};
ModeManager modeManager;
// ====================================================================
// 9. 메인 스케치 (Main Sketch)
// ====================================================================
void setup() {
Serial.begin(115200);
Serial.println("Multi-Function Clock Starting...");
// 핀 설정
pinMode(PIN_MODE_SELECT, INPUT_PULLUP);
pinMode(PIN_SELECT_NEXT, INPUT_PULLUP);
pinMode(PIN_UP_CHANGE, INPUT_PULLUP);
pinMode(PIN_START_STOP, INPUT_PULLUP);
pinMode(PIN_SAVE_ALARM_OFF, INPUT_PULLUP);
pinMode(PIN_SCREEN_OFF_BUTTON, INPUT_PULLUP); // NEW Button
pinMode(PIN_BUZZER, OUTPUT);
digitalWrite(PIN_BUZZER, LOW);
sensorModule.setup(); // 센서 및 LED 초기화
modeManager.initialize();
}
void loop() {
// 1. 버튼 입력 처리 (6개 버튼)
bool modePressed = readButton(PIN_MODE_SELECT, lastDebounceTime_Mode);
bool selectPressed = readButton(PIN_SELECT_NEXT, lastDebounceTime_Select);
bool upPressed = readButton(PIN_UP_CHANGE, lastDebounceTime_Up);
bool startStopPressed = readButton(PIN_START_STOP, lastDebounceTime_StartStop);
bool saveOffPressed = readButton(PIN_SAVE_ALARM_OFF, lastDebounceTime_SaveOff);
bool screenOffPressed = readButton(PIN_SCREEN_OFF_BUTTON, lastDebounceTime_ScreenOff); // NEW
if (modePressed) {
modeManager.handleModeButton();
} else if (selectPressed) {
modeManager.handleSelectButton();
} else if (upPressed) {
modeManager.handleUpButton();
} else if (startStopPressed) {
modeManager.handleStartStopButton();
} else if (saveOffPressed) {
modeManager.handleSaveAlarmOffButton();
} else if (screenOffPressed) { // NEW: 화면 끄기 버튼 처리
isManualScreenOff = !isManualScreenOff;
buzzerModule.confirmBeep(); // 끄거나 켤 때 확인음
}
// 2. 센서 모듈 업데이트 및 OLED 전원 제어 (NEW LOGIC)
sensorModule.update();
bool isSensorEvent = sensorModule.isAnySensorActive();
// NEW: 센서 감지 메시지 타이머 관리
if (isSensorEvent && !isDetectionDisplayActive) {
// 센서 감지가 새로 시작되었고, 아직 감지 화면이 활성화되지 않은 경우
detectionStartTime = millis();
isDetectionDisplayActive = true;
}
// 1초 타이머 체크
if (isDetectionDisplayActive && (millis() - detectionStartTime >= 1000)) {
isDetectionDisplayActive = false;
}
bool desiredOledPower;
if (isManualScreenOff) {
// 수동으로 꺼짐 상태: 센서가 감지하거나 감지 메시지 표시 중일 때만 켜짐
desiredOledPower = isSensorEvent || isDetectionDisplayActive;
} else {
// 일반 상태: 항상 켜짐 (센서 이벤트 유무와 관계 없음)
desiredOledPower = true;
}
// OLED와 LED 상태 동기화
displayModule.setPower(desiredOledPower);
sensorModule.controlLeds(desiredOledPower);
// 3. 현재 모드 업데이트 (로직 처리)
modeManager.updateCurrentMode();
// 4. 디스플레이 그리기 (Detection 메시지가 우선권을 가짐)
static unsigned long lastDrawTime = 0;
if (millis() - lastDrawTime >= 100) {
if (isDetectionDisplayActive) {
// 센서 감지 메시지를 그립니다.
displayModule.drawDetectionMessage(sensorModule.getSensorStates());
} else {
// 평소처럼 현재 모드를 그립니다.
modeManager.drawCurrentMode();
}
lastDrawTime = millis();
}
}
🅿️ 전체 코드가 어떻게 동작하는지 모든 버튼 입력 과정과 상태를 상세하게 단계적으로 설명하세요. 어떤 모듈이 실행되는지 클래스와 함수 이름도 같이 적어주세요. 동작 과정을 문장으로 적을 때 혹시 변경할 코드가 있다면 수정하거나 추가해주세요.
훌륭한 질문입니다. 전체 코드는 loop() 함수를 중심으로 매우 깔끔하게 구조화되어 있으며, 각 기능은 독립적인 모듈(클래스)에 위임되어 있습니다.
아래에서는 메인 루프(loop())를 기준으로 시스템의 전체 동작 과정을 단계적으로 설명하고, 각 단계에서 어떤 모듈과 함수가 실행되는지 자세히 분석해 드립니다.
1. 시스템 초기화 과정 (setup())
코드가 시작될 때 단 한 번만 실행되며, 모든 하드웨어 및 소프트웨어 모듈을 사용할 준비를 마칩니다.
| 순서 | 동작 | 실행되는 모듈/함수 | 설명 |
| 1 | 핀모드 설정 | pinMode() (Arduino API) | 버튼 핀 6개(INPUT_PULLUP), 부저 핀(OUTPUT) 등을 초기화합니다. |
| 2 | 센서/LED 설정 | sensorModule.setup() | 센서 핀 3개(INPUT) 및 LED 핀 2개(OUTPUT)를 초기화합니다. |
| 3 | RTC 초기화 | RtcModule 생성자 | DS1302 RTC 모듈을 초기화하고 쓰기 방지를 해제합니다. |
| 4 | OLED 초기화 | DisplayModule 생성자 | U8G2 라이브러리를 사용해 OLED를 초기화하고 폰트 모드/방향 등을 설정합니다. |
| 5 | 모드 관리자 초기화 | modeManager.initialize() | ClockMode를 포함한 모든 모드 객체를 생성하고, 현재 모드(ClockMode)의 enter() 함수를 호출합니다. |
2. 메인 루프 상세 동작 과정 (loop())
loop() 함수는 시스템의 심장부이며, 4단계의 고정된 주기로 끊임없이 반복 실행됩니다.
단계 1: 버튼 입력 처리
가장 먼저 모든 버튼의 눌림 상태를 확인하고, 눌린 버튼에 해당하는 모드 관리자의 핸들러를 실행합니다.
| 입력 | 동작 | 실행되는 모듈/함수 | 효과 및 상태 변경 |
| 모든 버튼 | 눌림 감지 | readButton() (Helper Function) | 풀업 저항을 가정하여 LOW가 감지되면 디바운싱 과정을 거쳐 true를 반환합니다. |
| MODE (D7) | 모드 전환 | modeManager.handleModeButton() | 1. buzzerModule.clickBeep() (피드백). 2. modeManager.switchMode()를 호출하여 다음 모드로 전환하고 새 모드의 enter() 실행. |
| SELECT (D5) | 설정 진입/다음 단계 | modeManager.handleSelectButton() | 1. buzzerModule.clickBeep() (피드백). 2. currentMode->handleSelectButton() 실행 (예: ClockMode::handleSelectButton으로 설정 단계 진입). |
| UP (D4) | 값 변경/다음 단계 | modeManager.handleUpButton() | 1. buzzerModule.clickBeep() (피드백). 2. currentMode->handleUpButton() 실행 (예: 시계 모드에서 시간 값 변경). |
| START/STOP (D3) | 시작/정지 | modeManager.handleStartStopButton() | 1. buzzerModule.clickBeep() (피드백). 2. currentMode->handleStartStopButton() 실행 (예: 포모도로 타이머 시작/정지). |
| SAVE/OFF (D2) | 저장/알람 끄기 | modeManager.handleSaveAlarmOffButton() | 1. buzzerModule.clickBeep() (피드백). 2. currentMode->handleSaveAlarmOffButton() 실행 (알람 중이면 부저를 끄고, 설정 중이면 값을 저장). |
| SCREEN_OFF (A3) | 화면 수동 제어 | (메인 loop()에서 처리) | isManualScreenOff 플래그를 토글합니다 (true ↔ false). |
단계 2: 센서 감지 및 OLED 전원 관리 (핵심 로직)
센서 감지 여부에 따라 OLED 화면 표시의 우선순위와 전원 상태를 결정합니다.
| 순서 | 동작 | 실행되는 모듈/함수 | 효과 및 상태 변경 |
| 1 | 센서 상태 업데이트 | sensorModule.update() | PIN_PIR_SENSOR 등의 디지털 값을 읽어 pirActive, vibrationActive 등을 갱신합니다. |
| 2 | 감지 플래그 설정 | (메인 loop() 로직) | 센서가 새로 감지되면 detectionStartTime을 현재 시간으로 설정하고 isDetectionDisplayActive를 true로 설정합니다. |
| 3 | 감지 타이머 종료 | (메인 loop() 로직) | (millis() - detectionStartTime >= 1000) 검사를 통해 감지 시작 후 **1초(1000ms)**가 지나면 isDetectionDisplayActive를 false로 설정합니다. |
| 4 | 전원 상태 결정 | (메인 loop() 로직) | isManualScreenOff 상태에 따라 desiredOledPower를 결정합니다. 수동으로 꺼져 있다면 센서 이벤트(감지 또는 감지 표시 활성 상태)가 있을 때만 켜짐을 유지합니다. |
| 5 | OLED/LED 전원 제어 | displayModule.setPower() sensorModule.controlLeds() | OLED와 LED의 실제 전원 상태를 desiredOledPower에 맞게 동기화합니다. |
💡 코드 수정 제안 (단계 2-3: 감지 메시지 표시 시간)
센서 감지 메시지(SENSOR DETECTED)가 표시되는 시간을 1초에서 다른 시간으로 변경하고 싶다면, 다음 라인의 1000 값을 수정하면 됩니다.
단계 3: 현재 모드 로직 업데이트
현재 활성화된 모드의 핵심 로직을 실행합니다. 이 단계에서 알람 확인, 타이머 카운트다운 등이 처리됩니다.
| 동작 | 실행되는 모듈/함수 | 주요 실행 로직 (예시) |
| 현재 모드 업데이트 | modeManager.updateCurrentMode() | ClockMode::update(): RTC 시간을 갱신(rtcModule.updateTime()). GeneralAlarmMode::update(): 현재 RTC 시간과 설정된 알람 시간을 비교하여 알람이 울릴지(buzzerModule.startAlarm()) 체크. |
| 부저 Crescendo 업데이트 | BuzzerModule::update() | 알람이 울리는 중이라면, 경과 시간에 따라 주파수(currentFreq)를 높이고 펄스 딜레이(pulseDelay)를 줄여 소리를 증폭(Crescendo)시킵니다. |
단계 4: 디스플레이 그리기 (100ms 주기로)
화면 깜빡임을 방지하고 CPU 부하를 줄이기 위해 100ms마다 화면을 갱신합니다. 이 단계에서 센서 감지 메시지가 가장 높은 우선순위를 가집니다.
| 조건 | 동작 | 실행되는 모듈/함수 | 효과 |
| 화면 갱신 주기 | 갱신 시간 확인 | (메인 loop() 로직) | millis() - lastDrawTime >= 100 검사 후 통과하면 그리기 시작. |
| 센서 감지 시 | 감지 메시지 출력 | displayModule.drawDetectionMessage() | 현재 센서 상태(SensorStates)를 인수로 받아 "SENSOR DETECTED" 메시지와 감지된 센서 목록을 오버레이로 출력합니다. |
| 평상시 | 모드 화면 출력 | modeManager.drawCurrentMode() | 현재 모드의 draw() 함수(예: PomodoroMode::draw())를 호출하여 해당 모드의 상태를 화면에 그립니다 |
//... (중략) ...
// 1초 타이머 체크 (수정 필요 시 이 값(1000)을 변경)
if (isDetectionDisplayActive && (millis() - detectionStartTime >= 1000)) {
isDetectionDisplayActive = false;
}
//... (중략) ...


'메이커 Maker' 카테고리의 다른 글
| 듀얼 주파수 NFC 비접촉식 USB RS232 UART 카드 리더 모듈 (0) | 2025.11.26 |
|---|---|
| 듀얼 IC 카드 PCB 안테나 센서용 RFID 리더 무선 모듈 (0) | 2025.11.26 |
| NFC 스마트 카드 리더기 RFID 복사기 (0) | 2025.11.26 |
| 작고 휴대하기 편한 DIY 블루투스 스피커 만들기 (0) | 2025.11.25 |
| 아두이노 메이커 교육: 나이를 초월한 창조의 기회 (0) | 2025.11.08 |
| 아두이노 GPS 트랙커 만들기 (0) | 2025.11.06 |
| 3Pin Reed Swtich & TP4056을 이용한 illy LED light 만들기 (0) | 2025.10.17 |
| AVR 퓨즈 비트의 올바른 구성 정보 (0) | 2025.10.17 |
취업, 창업의 막막함, 외주 관리, 제품 부재!
당신의 고민은 무엇입니까? 현실과 동떨어진 교육, 실패만 반복하는 외주 계약,
아이디어는 있지만 구현할 기술이 없는 막막함.
우리는 알고 있습니다. 문제의 원인은 '명확한 학습, 실전 경험과 신뢰할 수 있는 기술력의 부재'에서 시작됩니다.
이제 고민을 멈추고, 캐어랩을 만나세요!
코딩(펌웨어), 전자부품과 디지털 회로설계, PCB 설계 제작, 고객(시장/수출) 발굴과 마케팅 전략으로 당신을 지원합니다.
제품 설계의 고수는 성공이 만든 게 아니라 실패가 만듭니다. 아이디어를 양산 가능한 제품으로!
귀사의 제품을 만드세요. 교육과 개발 실적으로 신뢰할 수 있는 파트너를 확보하세요.
캐어랩