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


#include <Wire.h>
#include <U8g2lib.h>
#include <ESP32Time.h>
#include <esp_timer.h>

// ================== НАСТРОЙКИ ==================
#define TILTED_CLOCK true

// Пины (ESP32 Super Mini)
#define PIN_BTN_UP     1
#define PIN_BTN_DOWN   0
#define PIN_BUZZER     2     // или LED
#define I2C_SDA        3
#define I2C_SCL        4

// Тайминги
#define SCREEN_TIMEOUT_MS     5000UL
#define SHORT_PRESS_MS        1000UL
#define LONG_PRESS_MS         2000UL
#define BOTH_OK_MS            2000UL
#define ASYNC_TOLERANCE_MS    250UL   // допуск на разновременное нажатие двух кнопок

// ================== ГЛОБАЛЬНЫЕ ==================
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /*reset*/ U8X8_PIN_NONE, I2C_SCL, I2C_SDA);
ESP32Time rtc(0);  // UTC offset

enum State {
  SLEEP,
  SHOW_TIME,
  SET_TIME_H,
  SET_TIME_M,
  METRO_SHOW
};

State currentState = SHOW_TIME;
bool metroRunning = false;
int bpm = 120;
int setHour = 12;
int setMinute = 0;

// Кнопки
unsigned long btnUpPressStart = 0;
unsigned long btnDownPressStart = 0;
bool btnUpWasPressed = false;
bool btnDownWasPressed = false;
unsigned long lastActionTime = 0;

// Метроном
esp_timer_handle_t metroTimer = NULL;
volatile bool tickFlag = false;

// ================== ПРОТОТИПЫ ==================
void IRAM_ATTR metroISR(void* arg);
void startMetronome();
void stopMetronome();
void drawMainClock();
void updateScreen();
void handleButtons();
void processBothOK();
void saveTimeToRTC();
void resetScreenTimeout();

// ================== SETUP ==================
void setup() {
  pinMode(PIN_BTN_UP, INPUT_PULLUP);
  pinMode(PIN_BTN_DOWN, INPUT_PULLUP);
  pinMode(PIN_BUZZER, OUTPUT);

  Wire.begin(I2C_SDA, I2C_SCL);
  u8g2.begin();
  
  // Начальное время
  rtc.setTime(12, 0, 0, 8, 6, 2026);
  
  lastActionTime = millis();
  updateScreen();
}

// ================== HARDWARE METRONOME ==================
void IRAM_ATTR metroISR(void* arg) {
  tickFlag = true;
  digitalWrite(PIN_BUZZER, HIGH);
  delayMicroseconds(8000);        // длительность клика
  digitalWrite(PIN_BUZZER, LOW);
}

void startMetronome() {
  if (metroTimer) esp_timer_stop(metroTimer);
  uint64_t interval = 60000000ULL / bpm;   // микросекунды между тиками
  esp_timer_create_args_t args = {.callback = metroISR, .arg = nullptr};
  esp_timer_create(&args, &metroTimer);
  esp_timer_start_periodic(metroTimer, interval);
  metroRunning = true;
}

void stopMetronome() {
  if (metroTimer) {
    esp_timer_stop(metroTimer);
    esp_timer_delete(metroTimer);
    metroTimer = nullptr;
  }
  metroRunning = false;
  digitalWrite(PIN_BUZZER, LOW);
}

// ================== ОТРИСОВКА ==================
void drawMainClock() {
  u8g2.clearBuffer();
  
  int h = rtc.getHour(true);
  int m = rtc.getMinute();
  int s = rtc.getSecond();
  
  char mainTime[6];
  sprintf(mainTime, "%02d:%02d", h, m);

  u8g2.setFont(u8g2_font_fub20_tf);   // крупный ретро-шрифт

  if (TILTED_CLOCK) {
    // Диагональный вывод (8→2 часа)
    int x = 18;
    int y = 48;
    for (char* p = mainTime; *p; p++) {
      u8g2.drawStr(x, y, String(*p).c_str());
      x += 22;
      y -= 6;   // наклон \~30°
    }
  } else {
    u8g2.drawStr(28, 48, mainTime);
  }

  // Маленькое время в углу (12h + секунды + AM/PM)
  int h12 = h % 12; if (h12 == 0) h12 = 12;
  const char* ampm = (h >= 12) ? "PM" : "AM";
  char small[16];
  sprintf(small, "%02d:%02d:%02d %s", h12, m, s, ampm);
  
  u8g2.setFont(u8g2_font_5x8_tr);
  u8g2.drawStr(72, 12, small);

  // Ретро рамка
  u8g2.drawFrame(0, 0, 128, 64);
  u8g2.sendBuffer();
}

void updateScreen() {
  u8g2.clearBuffer();
  
  // Ретро-стиль: рамки + инверсные элементы
  u8g2.drawFrame(0, 0, 128, 64);
  
  switch (currentState) {
    case SHOW_TIME:
      drawMainClock();
      break;

    case SET_TIME_H:
      u8g2.setFont(u8g2_font_6x10_tr);
      u8g2.drawStr(15, 18, "SET HOURS");
      u8g2.setFont(u8g2_font_fub20_tf);
      u8g2.drawStr(45, 48, String(setHour).c_str());
      break;

    case SET_TIME_M:
      u8g2.setFont(u8g2_font_6x10_tr);
      u8g2.drawStr(10, 18, "SET MINUTES");
      u8g2.setFont(u8g2_font_fub20_tf);
      u8g2.drawStr(45, 48, String(setMinute).c_str());
      break;

    case METRO_SHOW:
      u8g2.setFont(u8g2_font_6x10_tr);
      u8g2.drawStr(25, 18, "METRONOME");
      u8g2.setFont(u8g2_font_fub17_tf);
      char buf[10];
      sprintf(buf, "%d", bpm);
      u8g2.drawStr(50, 48, buf);
      u8g2.setFont(u8g2_font_5x8_tr);
      u8g2.drawStr(75, 48, "BPM");
      
      u8g2.setFont(u8g2_font_5x8_tr);
      if (metroRunning) {
        u8g2.drawStr(10, 10, "RUNNING");
      } else {
        u8g2.drawStr(10, 10, "STOPPED");
      }
      break;

    case SLEEP:
      u8g2.clearDisplay();  // экран выключен
      return;
  }

  // Нижняя подсказка (ретро-стиль)
  if (currentState != SHOW_TIME && currentState != SLEEP) {
    u8g2.setFont(u8g2_font_5x8_tr);
    u8g2.drawStr(5, 62, "P1  H10  BOTH OK");
  }

  u8g2.sendBuffer();
}

// ================== КНОПКИ + ЗАЩИТА ОТ АСИНХРОННОГО НАЖАТИЯ ==================
void handleButtons() {
  unsigned long now = millis();
  bool up = !digitalRead(PIN_BTN_UP);
  bool down = !digitalRead(PIN_BTN_DOWN);

  if (up || down) {
    resetScreenTimeout();
  }

  // Нажатие UP
  if (up && !btnUpWasPressed) {
    btnUpPressStart = now;
    btnUpWasPressed = true;
  }
  // Нажатие DOWN
  if (down && !btnDownWasPressed) {
    btnDownPressStart = now;
    btnDownWasPressed = true;
  }

  // Отпускание UP
  if (!up && btnUpWasPressed) {
    if ((now - btnUpPressStart) < SHORT_PRESS_MS && !btnDownWasPressed) {
      // Короткое нажатие UP
      if (currentState == SET_TIME_H) setHour = (setHour + 1) % 24;
      else if (currentState == SET_TIME_M) setMinute = (setMinute + 1) % 60;
      else if (currentState == METRO_SHOW) bpm = min(240, bpm + 1);
    }
    btnUpWasPressed = false;
  }

  // Отпускание DOWN
  if (!down && btnDownWasPressed) {
    if ((now - btnDownPressStart) < SHORT_PRESS_MS && !btnUpWasPressed) {
      // Короткое нажатие DOWN
      if (currentState == SET_TIME_H) setHour = (setHour + 23) % 24;
      else if (currentState == SET_TIME_M) setMinute = (setMinute + 59) % 60;
      else if (currentState == METRO_SHOW) bpm = max(40, bpm - 1);
    }
    btnDownWasPressed = false;
  }

  // Длинное удержание одной кнопки (ускорение)
  if (btnUpWasPressed && (now - btnUpPressStart) > LONG_PRESS_MS) {
    if (currentState == SET_TIME_H) setHour = (setHour + 10) % 24;
    else if (currentState == SET_TIME_M) setMinute = (setMinute + 10) % 60;
    else if (currentState == METRO_SHOW) bpm = min(240, bpm + 10);
  }
  if (btnDownWasPressed && (now - btnDownPressStart) > LONG_PRESS_MS) {
    if (currentState == SET_TIME_H) setHour = (setHour + 14) % 24; // -10
    else if (currentState == SET_TIME_M) setMinute = (setMinute + 50) % 60;
    else if (currentState == METRO_SHOW) bpm = max(40, bpm - 10);
  }

  // BOTH OK (с допуском на асинхронность)
  if (btnUpWasPressed && btnDownWasPressed) {
    unsigned long upTime = now - btnUpPressStart;
    unsigned long downTime = now - btnDownPressStart;
    
    if (upTime > BOTH_OK_MS && downTime > BOTH_OK_MS) {
      processBothOK();
      btnUpWasPressed = btnDownWasPressed = false;
    }
  }

  // Таймаут сна
  if (!metroRunning && (now - lastActionTime > SCREEN_TIMEOUT_MS)) {
    if (currentState != SLEEP) {
      currentState = SLEEP;
      u8g2.clearDisplay();
    }
  }
}

void resetScreenTimeout() {
  lastActionTime = millis();
  if (currentState == SLEEP) {
    if (metroRunning) {
      currentState = METRO_SHOW;
    } else {
      currentState = SHOW_TIME;
    }
  }
}

void processBothOK() {
  switch (currentState) {
    case SHOW_TIME:
      currentState = SET_TIME_H;
      setHour = rtc.getHour(true);
      setMinute = rtc.getMinute();
      break;
      
    case SET_TIME_H:
      currentState = SET_TIME_M;
      break;
      
    case SET_TIME_M:
      saveTimeToRTC();
      currentState = SHOW_TIME;
      break;
      
    case METRO_SHOW:
      if (metroRunning) stopMetronome();
      else startMetronome();
      break;
  }
}

void saveTimeToRTC() {
  int sec = rtc.getSecond();
  rtc.setTime(sec, setMinute, setHour, rtc.getDay(), rtc.getMonth() + 1, rtc.getYear());
}

// ================== LOOP ==================
void loop() {
  handleButtons();

  // Обновление времени из RTC
  if (currentState == SHOW_TIME) {
    // ничего не делаем — drawMainClock читает напрямую
  }

  if (tickFlag) {
    tickFlag = false;
    // можно добавить визуальный отклик метронома здесь при необходимости
  }

  // Пробуждение при работающем метрономе
  if (metroRunning && currentState == SLEEP) {
    currentState = METRO_SHOW;
  }

  updateScreen();
  delay(30);   // плавная отзывчивость
}