Загрузка данных
#include <Arduino.h>
#include <Wire.h>
#include <U8g2lib.h>
#include <ESP32Time.h>
#include <math.h> // For radians function
// Define radians function if not already defined (e.g., in Arduino.h)
#ifndef radians
#define radians(deg) ((deg) * DEG_TO_RAD)
#endif
// --- Hardware Configuration ---
#define OLED_SCL 4
#define OLED_SDA 3
#define BUTTON_UP_PIN 1
#define BUTTON_DOWN_PIN 0
#define METRONOME_LED_PIN 2 // Example pin for metronome LED
#define METRONOME_BUZZER_PIN 5 // Example pin for metronome buzzer
// --- Display Object ---
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, OLED_SCL, OLED_SDA);
// --- Time Object ---
ESP32Time rtc;
// --- State Machine ---
enum AppState {
STATE_CLOCK_MAIN,
STATE_SET_TIME,
STATE_METRONOME_MAIN,
STATE_METRONOME_SET_BPM,
STATE_SLEEP
};
AppState currentState = STATE_CLOCK_MAIN;
AppState previousState = STATE_CLOCK_MAIN;
// --- Button Handling ---
#define DEBOUNCE_DELAY 50 // ms
#define SHORT_PRESS_THRESHOLD 2000 // ms
unsigned long buttonUpPressTime = 0;
unsigned long buttonDownPressTime = 0;
bool buttonUpPressed = false;
bool buttonDownPressed = false;
bool bothButtonsPressed = false;
// --- Metronome Variables ---
hw_timer_t * timer = NULL;
volatile bool metronomeTick = false;
volatile bool metronomeRunning = false;
int metronomeBPM = 120;
int metronomeBeatCount = 0;
// --- Power Management ---
#define DISPLAY_SLEEP_TIMEOUT 4000 // ms
unsigned long lastActivityTime = 0;
bool displayOn = true;
// --- Tilted Clock Setting ---
#define TILTED_CLOCK true
// --- Function Prototypes ---
void enterState(AppState newState);
void updateState();
void exitState();
void handleButtons();
bool isButtonUpShortPress();
bool isButtonDownShortPress();
bool isButtonUpLongPress();
bool isButtonDownLongPress();
bool isBothButtonsPressed();
void drawMainClock();
void drawSetTime();
void drawMetronomeMain();
void drawMetronomeSetBPM();
void drawSleepScreen();
void updateDisplay();
void IRAM_ATTR onTimer();
void setupMetronomeTimer();
void setMetronomeBPM(int bpm);
// --- Setup Function ---
void setup() {
Serial.begin(115200);
// Initialize OLED
u8g2.begin();
u8g2.setFont(u8g2_font_ncenB14_tr);
u8g2.clearBuffer();
u8g2.drawStr(0, 15, "Initializing...");
u8g2.sendBuffer();
// Initialize Buttons
pinMode(BUTTON_UP_PIN, INPUT_PULLUP);
pinMode(BUTTON_DOWN_PIN, INPUT_PULLUP);
// Initialize Metronome Output Pins
pinMode(METRONOME_LED_PIN, OUTPUT);
pinMode(METRONOME_BUZZER_PIN, OUTPUT);
digitalWrite(METRONOME_LED_PIN, LOW);
digitalWrite(METRONOME_BUZZER_PIN, LOW);
// Initialize RTC (set a default time for testing)
rtc.setTime(0, 0, 12, 1, 1, 2023); // 12:00:00 on Jan 1, 2023
// Setup Metronome Hardware Timer
setupMetronomeTimer();
lastActivityTime = millis();
enterState(STATE_CLOCK_MAIN);
}
// --- Loop Function ---
void loop() {
handleButtons();
updateState();
updateDisplay();
// Power Management: Display Sleep
if (currentState != STATE_METRONOME_MAIN &&
currentState != STATE_METRONOME_SET_BPM &&
currentState != STATE_SET_TIME) {
if (millis() - lastActivityTime > DISPLAY_SLEEP_TIMEOUT && displayOn) {
displayOn = false;
u8g2.clearDisplay();
Serial.println("Display Off");
enterState(STATE_SLEEP);
}
}
// Metronome Tick Handling (outside ISR to avoid blocking)
if (metronomeTick) {
metronomeTick = false;
digitalWrite(METRONOME_LED_PIN, HIGH);
digitalWrite(METRONOME_BUZZER_PIN, HIGH);
delay(50); // Short pulse for LED/Buzzer
digitalWrite(METRONOME_LED_PIN, LOW);
digitalWrite(METRONOME_BUZZER_PIN, LOW);
metronomeBeatCount++;
Serial.print("Metronome Beat: ");
Serial.println(metronomeBeatCount);
}
}
// --- State Machine Implementation ---
void enterState(AppState newState) {
previousState = currentState;
currentState = newState;
Serial.print("Entering State: ");
Serial.println(newState);
// State-specific entry actions
switch (currentState) {
case STATE_CLOCK_MAIN:
displayOn = true;
lastActivityTime = millis();
break;
case STATE_SET_TIME:
displayOn = true;
lastActivityTime = millis();
break;
case STATE_METRONOME_MAIN:
displayOn = true;
lastActivityTime = millis();
break;
case STATE_METRONOME_SET_BPM:
displayOn = true;
lastActivityTime = millis();
break;
case STATE_SLEEP:
// Handled in loop for now, clear display here if needed
break;
}
}
void updateState() {
// State-specific update actions
switch (currentState) {
case STATE_CLOCK_MAIN:
if (isButtonUpLongPress()) {
enterState(STATE_SET_TIME);
} else if (isButtonDownLongPress()) {
enterState(STATE_METRONOME_MAIN);
} else if (isButtonUpShortPress() || isButtonDownShortPress()) {
// No action for short press on main clock screen
}
break;
case STATE_SET_TIME:
if (isButtonUpShortPress()) {
// Toggle between setting hours/minutes
// For now, just increment hour as an example
rtc.setTime(0, rtc.getMinute(), (rtc.getHour(true) + 1) % 24, rtc.getDay(), rtc.getMonth(), rtc.getYear());
lastActivityTime = millis();
} else if (isButtonDownShortPress()) {
// Decrement hour as an example
rtc.setTime(0, rtc.getMinute(), (rtc.getHour(true) - 1 + 24) % 24, rtc.getDay(), rtc.getMonth(), rtc.getYear());
lastActivityTime = millis();
} else if (isButtonUpLongPress()) {
// Increment by 10
rtc.setTime(0, rtc.getMinute(), (rtc.getHour(true) + 10) % 24, rtc.getDay(), rtc.getMonth(), rtc.getYear());
lastActivityTime = millis();
} else if (isButtonDownLongPress()) {
// Decrement by 10
rtc.setTime(0, rtc.getMinute(), (rtc.getHour(true) - 10 + 24) % 24, rtc.getDay(), rtc.getMonth(), rtc.getYear());
lastActivityTime = millis();
}
break;
case STATE_METRONOME_MAIN:
if (isButtonUpLongPress()) {
enterState(STATE_METRONOME_SET_BPM);
} else if (isButtonDownLongPress()) {
enterState(STATE_CLOCK_MAIN);
} else if (isButtonUpShortPress() || isButtonDownShortPress()) {
metronomeRunning = !metronomeRunning;
Serial.print("Metronome Running: ");
Serial.println(metronomeRunning);
if (metronomeRunning) {
timerAlarmEnable(timer);
} else {
timerAlarmDisable(timer);
}
}
break;
case STATE_METRONOME_SET_BPM:
if (isButtonUpShortPress()) {
setMetronomeBPM(metronomeBPM + 1);
lastActivityTime = millis();
} else if (isButtonDownShortPress()) {
setMetronomeBPM(metronomeBPM - 1);
lastActivityTime = millis();
} else if (isButtonUpLongPress()) {
setMetronomeBPM(metronomeBPM + 10);
lastActivityTime = millis();
} else if (isButtonDownLongPress()) {
setMetronomeBPM(metronomeBPM - 10);
lastActivityTime = millis();
}
break;
case STATE_SLEEP:
// No update needed, waiting for button press to wake up
break;
}
}
void exitState() {
// State-specific exit actions
switch (previousState) {
case STATE_CLOCK_MAIN:
break;
case STATE_SET_TIME:
break;
case STATE_METRONOME_MAIN:
break;
case STATE_METRONOME_SET_BPM:
break;
case STATE_SLEEP:
break;
}
}
// --- Button Handling Implementation ---
void handleButtons() {
static unsigned long lastButtonUpStateChange = 0;
static unsigned long lastButtonDownStateChange = 0;
static int lastButtonUpState = HIGH;
static int lastButtonDownState = HIGH;
int currentButtonUpState = digitalRead(BUTTON_UP_PIN);
int currentButtonDownState = digitalRead(BUTTON_DOWN_PIN);
// Debounce Button UP
if (currentButtonUpState != lastButtonUpState) {
lastButtonUpStateChange = millis();
}
if ((millis() - lastButtonUpStateChange) > DEBOUNCE_DELAY) {
if (currentButtonUpState != buttonUpPressed) {
buttonUpPressed = (currentButtonUpState == LOW);
if (buttonUpPressed) {
buttonUpPressTime = millis();
lastActivityTime = millis(); // Reset activity timer
if (!displayOn && currentState == STATE_SLEEP) {
displayOn = true;
enterState(previousState); // Wake up to previous state
return; // Don't process other button actions on first wake-up press
}
} else {
// Button released
}
}
}
lastButtonUpState = currentButtonUpState;
// Debounce Button DOWN
if (currentButtonDownState != lastButtonDownState) {
lastButtonDownStateChange = millis();
}
if ((millis() - lastButtonDownStateChange) > DEBOUNCE_DELAY) {
if (currentButtonDownState != buttonDownPressed) {
buttonDownPressed = (currentButtonDownState == LOW);
if (buttonDownPressed) {
buttonDownPressTime = millis();
lastActivityTime = millis(); // Reset activity timer
if (!displayOn && currentState == STATE_SLEEP) {
displayOn = true;
enterState(previousState); // Wake up to previous state
return; // Don't process other button actions on first wake-up press
}
} else {
// Button released
}
}
}
lastButtonDownState = currentButtonDownState;
// Check for both buttons pressed
bool currentBothButtonsPressed = (digitalRead(BUTTON_UP_PIN) == LOW && digitalRead(BUTTON_DOWN_PIN) == LOW);
if (currentBothButtonsPressed && !bothButtonsPressed) {
bothButtonsPressed = true;
lastActivityTime = millis(); // Reset activity timer
Serial.println("Both buttons pressed!");
// Handle BOTH OK action
if (currentState == STATE_SET_TIME || currentState == STATE_METRONOME_SET_BPM) {
// Save / OK action
Serial.println("SAVE / OK action");
enterState(STATE_CLOCK_MAIN); // Example: return to main clock after saving
}
} else if (!currentBothButtonsPressed && bothButtonsPressed) {
bothButtonsPressed = false;
Serial.println("Both buttons released!");
}
// If both buttons are pressed, block single button actions
if (bothButtonsPressed) return;
}
bool isButtonUpShortPress() {
static bool prevButtonUpState = false;
bool currentButtonUpState = digitalRead(BUTTON_UP_PIN) == LOW;
bool shortPress = false;
if (prevButtonUpState && !currentButtonUpState) {
if ((millis() - buttonUpPressTime < SHORT_PRESS_THRESHOLD) && !buttonUpLongPressTriggered) {
shortPress = true;
lastActivityTime = millis(); // Reset activity timer
}
buttonUpLongPressTriggered = false; // Reset flag on release
}
prevButtonUpState = currentButtonUpState;
return shortPress;
}
bool isButtonDownShortPress() {
static bool prevButtonDownState = false;
bool currentButtonDownState = digitalRead(BUTTON_DOWN_PIN) == LOW;
bool shortPress = false;
if (prevButtonDownState && !currentButtonDownState) {
if ((millis() - buttonDownPressTime < SHORT_PRESS_THRESHOLD) && !buttonDownLongPressTriggered) {
shortPress = true;
lastActivityTime = millis(); // Reset activity timer
}
buttonDownLongPressTriggered = false; // Reset flag on release
}
prevButtonDownState = currentButtonDownState;
return shortPress;
}
// Global declarations for long press flags
bool buttonUpLongPressTriggered = false;
bool isButtonUpLongPress() {
if (digitalRead(BUTTON_UP_PIN) == LOW && (millis() - buttonUpPressTime >= SHORT_PRESS_THRESHOLD) && !buttonUpLongPressTriggered) {
buttonUpLongPressTriggered = true;
lastActivityTime = millis(); // Reset activity timer
return true;
} else if (digitalRead(BUTTON_UP_PIN) == HIGH) {
buttonUpLongPressTriggered = false; // Reset flag on release
}
return false;
}
bool buttonDownLongPressTriggered = false;
bool isButtonDownLongPress() {
if (digitalRead(BUTTON_DOWN_PIN) == LOW && (millis() - buttonDownPressTime >= SHORT_PRESS_THRESHOLD) && !buttonDownLongPressTriggered) {
buttonDownLongPressTriggered = true;
lastActivityTime = millis(); // Reset activity timer
return true;
} else if (digitalRead(BUTTON_DOWN_PIN) == HIGH) {
buttonDownLongPressTriggered = false; // Reset flag on release
}
return false;
}
bool isBothButtonsPressed() {
return bothButtonsPressed;
}
// --- Display Rendering Implementation ---
void updateDisplay() {
if (!displayOn) return;
u8g2.clearBuffer();
switch (currentState) {
case STATE_CLOCK_MAIN:
drawMainClock();
break;
case STATE_SET_TIME:
drawSetTime();
break;
case STATE_METRONOME_MAIN:
drawMetronomeMain();
break;
case STATE_METRONOME_SET_BPM:
drawMetronomeSetBPM();
break;
case STATE_SLEEP:
// Should not happen if displayOn is false
break;
}
// Debug/Setting Overlay
if (currentState == STATE_SET_TIME || currentState == STATE_METRONOME_SET_BPM) {
u8g2.setFont(u8g2_font_profont10_mf);
u8g2.drawStr(0, 63, "P 1, H 10, BOTH OK");
}
u8g2.sendBuffer();
}
void drawMainClock() {
char timeBuffer[9]; // HH:MM:SS
char dateBuffer[11]; // DD/MM/YYYY
char smallTimeBuffer[12]; // HH:MM:SS AM/PM
// Get current time
int hour = rtc.getHour(true); // 24-hour format
int minute = rtc.getMinute();
int second = rtc.getSecond();
// Large 24H time
sprintf(timeBuffer, "%02d:%02d", hour, minute);
u8g2.setFont(u8g2_font_logisoso30_tn);
#if TILTED_CLOCK
// Tilted text (approx 30-35 degrees, 8 to 2 o'clock)
// U8g2 does not have direct rotation for drawStr, but we can draw individual characters with calculated positions.
u8g2.setFont(u8g2_font_logisoso30_tn);
u8g2.setDrawColor(1);
int x_offset = 0;
int y_offset = 30;
float angle_rad = radians(30); // Approximately 30 degrees
for (int i = 0; i < strlen(timeBuffer); i++) {
char c_str[2];
c_str[0] = timeBuffer[i];
c_str[1] = '\0';
int char_width = u8g2.getStrWidth(c_str);
// Calculate rotated position for each character
// This is a simplified linear approximation for a tilted effect.
// For a true rotation, one would need to rotate the entire display buffer or use more complex glyph drawing.
int x_char = x_offset + (int)(i * char_width * cos(angle_rad));
int y_char = y_offset - (int)(i * char_width * sin(angle_rad));
u8g2.drawStr(x_char, y_char, c_str);
x_offset += char_width; // Advance x_offset for the next character
}
#endif
// Small 12H + seconds + AM/PM
sprintf(smallTimeBuffer, "%02d:%02d:%02d %s",
(hour % 12 == 0) ? 12 : (hour % 12),
minute, second,
(hour >= 12) ? "PM" : "AM");
u8g2.setFont(u8g2_font_profont10_mf);
u8g2.drawStr(0, 63, smallTimeBuffer);
}
void drawSetTime() {
u8g2.setFont(u8g2_font_ncenB14_tr);
u8g2.drawStr(0, 15, "Set Time");
// Placeholder for time setting UI
char timeBuffer[9];
sprintf(timeBuffer, "%02d:%02d", rtc.getHour(true), rtc.getMinute());
u8g2.drawStr(0, 40, timeBuffer);
}
void drawMetronomeMain() {
u8g2.setFont(u8g2_font_ncenB14_tr);
u8g2.drawStr(0, 15, "Metronome");
char bpmBuffer[10];
sprintf(bpmBuffer, "BPM: %d", metronomeBPM);
u8g2.drawStr(0, 40, bpmBuffer);
if (metronomeRunning) {
u8g2.drawStr(0, 60, "RUNNING");
} else {
u8g2.drawStr(0, 60, "STOPPED");
}
}
void drawMetronomeSetBPM() {
u8g2.setFont(u8g2_font_ncenB14_tr);
u8g2.drawStr(0, 15, "Set BPM");
char bpmBuffer[10];
sprintf(bpmBuffer, "BPM: %d", metronomeBPM);
u8g2.drawStr(0, 40, bpmBuffer);
}
void drawSleepScreen() {
// Nothing to draw, display is off
}
// --- Metronome Hardware Timer Implementation ---
void IRAM_ATTR onTimer() {
if (metronomeRunning) {
metronomeTick = true;
}
}
void setupMetronomeTimer() {
timer = timerBegin(0, 80, true); // Timer 0, prescaler 80 (for 1us tick), count up
timerAttachInterrupt(timer, &onTimer, true); // Attach interrupt
setMetronomeBPM(metronomeBPM); // Set initial BPM
}
void setMetronomeBPM(int bpm) {
metronomeBPM = bpm;
// Calculate alarm value for the given BPM
// 60 seconds/minute * 1,000,000 us/second / BPM = us per beat
unsigned long usPerBeat = (60000000 / metronomeBPM);
timerAlarmWrite(timer, usPerBeat, true); // Set alarm, autoreload
if (metronomeRunning) {
timerAlarmEnable(timer);
} else {
timerAlarmDisable(timer);
}
}