Загрузка данных


#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);
  }
}