Загрузка данных
#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); // плавная отзывчивость
}