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


/*
 * Скетч: Часы + Метроном на базе State Machine для ESP32 Super Mini
 * Версия для Arduino Core 3.3.x
 * Дизайн: Чистый минимализм с фиксированными позициями
 * ФИНАЛЬНАЯ ВЕРСИЯ:
 * - Компактный инверсный квадрат (не упирается в верхнюю строку)
 * - Мини-анимация эквалайзера в такт метроному
 * 
 * Библиотеки для установки:
 * - U8g2 (by oliver)
 * - ESP32Time (by fbiego)
 * 
 * Аппаратная конфигурация:
 * - OLED 0.96" I2C: SCL=4, SDA=3
 * - Кнопки: UP=1, DOWN=0 (INPUT_PULLUP, активный LOW)
 */

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

// ================== НАСТРАИВАЕМЫЕ ПАРАМЕТРЫ ==================
#define TILTED_CLOCK true

// ------------------ Пины ------------------
#define PIN_BTN_UP     1
#define PIN_BTN_DOWN   0
#define PIN_LED        2
#define PIN_BUZZER     10

// ------------------ Тайминги ------------------
#define SHORT_PRESS_MAX     1000
#define LONG_PRESS_MIN      2000
#define BOTH_PRESS_WINDOW   200
#define SLEEP_TIMEOUT       5000
#define SLEEP_ANIM_FRAMES   5
#define SLEEP_ANIM_DELAY    15
#define PULSE_DURATION_MS   70      // Длительность отображения пика анимации

// ------------------ Метроном ------------------
#define DEFAULT_BPM     100
#define MIN_BPM         30
#define MAX_BPM         250

// ------------------ Константы позиционирования ------------------
#define TEXT_Y_CENTER       48      // Базовая линия для крупного текста (Logisoso24)
#define HELP_TEXT_Y         62      // Позиция нижних подсказок

// ------------------ Параметры инверсного квадрата (КОМПАКТНЫЕ) ------------------
#define BOX_PAD_X           2       // Боковой отступ внутри квадрата
#define BOX_PAD_Y_TOP       1       // Отступ сверху (минимальный, чтоб не упираться в заголовок)
#define BOX_PAD_Y_BOTTOM    1       // Отступ снизу от базовой линии

// ------------------ Параметры анимации эквалайзера ------------------
#define EQ_BAR_COUNT        4       // Количество полосок
#define EQ_BAR_WIDTH        3       // Ширина одной полоски
#define EQ_BAR_GAP          2       // Зазор между полосками
#define EQ_BAR_MAX_H        10      // Максимальная высота полоски
#define EQ_BASE_Y           58      // Базовая линия эквалайзера (низ полосок)

// ------------------ Дисплей ------------------
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* clock=*/ 4, /* data=*/ 3);
ESP32Time rtc;

// ================== КОНЕЧНЫЙ АВТОМАТ ==================
enum DeviceState : uint8_t {
    SHOW_TIME,
    SHOW_TIME_SLEEP,
    TIME_PICK,
    TIME_EDIT,
    METRO_SHOW,
    METRO_EDIT
} currentState = SHOW_TIME;

// ================== ПЕРЕМЕННЫЕ КНОПОК ==================
struct ButtonState {
    bool pressed = false;
    unsigned long pressStartTime = 0;
    bool longPressTriggered = false;
    bool shortPressReady = false;
};

ButtonState btnUp, btnDown;
unsigned long lastActivityTime = 0;

// ================== ПЕРЕМЕННЫЕ ВРЕМЕНИ ==================
int currentHour = 12, currentMinute = 0;
bool editMode = 0; // 0 - часы, 1 - минуты

// ================== ПЕРЕМЕННЫЕ МЕТРОНОМА ==================
volatile bool metroRunning = false;
volatile bool metroBeatFlag = false;
volatile bool pulseActive = false;          // Флаг для анимации эквалайзера
volatile unsigned long pulseStartTime = 0;  // Время начала импульса
int metroBPM = DEFAULT_BPM;
int editBPM = DEFAULT_BPM;
hw_timer_t *metroTimer = NULL;
bool screenWokeUpFromMetro = false;

// Флаги для отображения стрелок
bool showUpArrow = false;
bool showDownArrow = false;

// Состояние полосок эквалайзера (высоты в пикселях)
int eqBars[EQ_BAR_COUNT] = {0, 0, 0, 0};

// ================== ПРЕРЫВАНИЕ МЕТРОНОМА ==================
void IRAM_ATTR onMetronomeBeat() {
    if (metroRunning) {
        metroBeatFlag = true;
        pulseActive = true;
        pulseStartTime = millis();
        digitalWrite(PIN_LED, !digitalRead(PIN_LED));
    }
}

void updateMetronomeTimer() {
    if (metroTimer) {
        unsigned long period = (60 * 1000000UL) / metroBPM;
        if (period < 1000) period = 1000;
        timerAlarm(metroTimer, period, true, 0);
    }
}

// ================== ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ==================
void drawHelperText() {
    u8g2.setFont(u8g2_font_5x8_tr);
    u8g2.setDrawColor(1);
    u8g2.drawStr(2, HELP_TEXT_Y, "P 1");
    u8g2.drawStr(45, HELP_TEXT_Y, "H 10");
    u8g2.drawStr(95, HELP_TEXT_Y, "BOTH OK");
}

// Рисует стрелку вверх (справа от числа)
void drawUpArrow(int x, int y) {
    u8g2.setDrawColor(1);
    u8g2.drawTriangle(x, y - 8, x - 6, y + 2, x + 6, y + 2);
}

// Рисует стрелку вниз (слева от числа)
void drawDownArrow(int x, int y) {
    u8g2.setDrawColor(1);
    u8g2.drawTriangle(x, y + 8, x - 6, y - 2, x + 6, y - 2);
}

/*
 * КОМПАКТНАЯ функция выделения текста инверсией
 * 
 * Исправлено: верхний отступ уменьшен, чтобы квадрат не упирался в заголовок
 * X, Y — базовые координаты текста (НИКОГДА не меняются)
 */
void drawHighlightedText(int x, int y, const char* str, bool highlighted) {
    if (highlighted) {
        // Получаем реальные размеры шрифта
        int w = u8g2.getStrWidth(str);
        int h = u8g2.getMaxCharHeight();
        
        // Компактные координаты прямоугольника
        int boxX = x - BOX_PAD_X;
        int boxW = w + BOX_PAD_X * 2;
        
        // Y: минимальный отступ сверху (1px), чтобы макушки были почти вплотную к краю
        int boxY = y - h - BOX_PAD_Y_TOP;
        int boxH = h + BOX_PAD_Y_TOP + BOX_PAD_Y_BOTTOM;
        
        // 1. Белый фон
        u8g2.setDrawColor(1);
        u8g2.drawBox(boxX, boxY, boxW, boxH);
        
        // 2. Чёрный текст на том же месте
        u8g2.setDrawColor(0);
        u8g2.drawStr(x, y, str);
        
        // 3. Сброс цвета
        u8g2.setDrawColor(1);
    } else {
        // Обычный белый текст
        u8g2.setDrawColor(1);
        u8g2.drawStr(x, y, str);
    }
}

/*
 * Ретро-анимация эквалайзера (Nokia-стиль)
 * Рисует 4 вертикальные пиксельные полоски в нижней части экрана
 */
void drawEqualizer() {
    if (!metroRunning) return;
    
    // Вычисляем общую ширину эквалайзера для центрирования
    int totalW = EQ_BAR_COUNT * EQ_BAR_WIDTH + (EQ_BAR_COUNT - 1) * EQ_BAR_GAP;
    int startX = (128 - totalW) / 2;
    
    for (int i = 0; i < EQ_BAR_COUNT; i++) {
        int barX = startX + i * (EQ_BAR_WIDTH + EQ_BAR_GAP);
        int barH = eqBars[i];
        
        if (barH > 0) {
            // Рисуем залитую полоску
            u8g2.setDrawColor(1);
            u8g2.drawBox(barX, EQ_BASE_Y - barH, EQ_BAR_WIDTH, barH);
        } else {
            // Рисуем точку (базовый уровень)
            u8g2.setDrawColor(1);
            u8g2.drawPixel(barX + 1, EQ_BASE_Y - 1);
        }
    }
}

/*
 * Обновление состояния эквалайзера
 * Вызывается в loop() для анимации падения полосок
 */
void updateEqualizer() {
    unsigned long now = millis();
    
    if (pulseActive) {
        // В момент удара — все полоски на максимум
        for (int i = 0; i < EQ_BAR_COUNT; i++) {
            eqBars[i] = EQ_BAR_MAX_H;
        }
        
        // Сбрасываем флаг через короткое время
        if (now - pulseStartTime >= PULSE_DURATION_MS) {
            pulseActive = false;
        }
    } else {
        // Плавное падение полосок (дискретное, в стиле ретро)
        for (int i = 0; i < EQ_BAR_COUNT; i++) {
            if (eqBars[i] > 0) {
                // Каждая полоска падает со своей скоростью
                int decaySpeed = 1 + (i % 3); // 1, 2, 3, 1 - разная скорость падения
                eqBars[i] = max(0, eqBars[i] - decaySpeed);
            }
        }
    }
}

// Главная функция отрисовки часов
void drawMainClock() {
    int h = rtc.getHour(true);
    int m = rtc.getMinute();
    int s = rtc.getSecond();
    char buf[6];
    sprintf(buf, "%02d:%02d", h, m);

    if (TILTED_CLOCK) {
        u8g2.setFont(u8g2_font_logisoso30_tn);
        int x = 10, y = 65;
        for(int i = 0; i < 5; i++) {
            char c[2] = {buf[i], '\0'};
            u8g2.drawStr(x + i*20, y - i*8, c);
        }
    } else {
        u8g2.setFont(u8g2_font_logisoso32_tn);
        u8g2.drawStr(10, 45, buf);
    }

    u8g2.setFont(u8g2_font_profont12_mf);
    char buf12[12];
    int h12 = (h % 12 == 0) ? 12 : h % 12;
    sprintf(buf12, "%02d:%02d:%02d", h12, m, s);
    u8g2.drawStr(70, 60, buf12);
}

// Анимация засыпания дисплея
void playSleepAnimation() {
    for (int i = 0; i < SLEEP_ANIM_FRAMES; i++) {
        u8g2.clearBuffer();
        drawMainClock();
        int contrast = 255 - (i * 50);
        if (contrast < 0) contrast = 0;
        u8g2.setContrast(contrast);
        u8g2.sendBuffer();
        delay(SLEEP_ANIM_DELAY);
    }
    u8g2.setPowerSave(1);
    u8g2.setContrast(255);
}

void resetActivityTimer() {
    lastActivityTime = millis();
    if (currentState == SHOW_TIME_SLEEP) {
        screenWokeUpFromMetro = false;
    }
    if (currentState == SHOW_TIME_SLEEP && !metroRunning) {
        currentState = SHOW_TIME;
        u8g2.setPowerSave(0);
    }
}

void enterSleepMode() {
    if (currentState == SHOW_TIME) {
        playSleepAnimation();
        currentState = SHOW_TIME_SLEEP;
    }
}

void saveTimeToRTC(int h, int m) {
    rtc.setTime(0, m, h, 1, 1, 2024);
    currentHour = h;
    currentMinute = m;
}

// ================== ОБРАБОТЧИКИ НАЖАТИЙ ==================
void onLongPress(byte btn) {
    if (currentState == SHOW_TIME || currentState == SHOW_TIME_SLEEP) {
        if (btn == PIN_BTN_UP) {
            currentHour = rtc.getHour(true);
            currentMinute = rtc.getMinute();
            editMode = 0;
            currentState = TIME_PICK;
            resetActivityTimer();
        } else {
            currentState = METRO_SHOW;
            metroRunning = false;
            // Сбрасываем эквалайзер
            for (int i = 0; i < EQ_BAR_COUNT; i++) eqBars[i] = 0;
            resetActivityTimer();
        }
    } 
    else if (currentState == TIME_PICK || currentState == TIME_EDIT) {
        if (btn == PIN_BTN_UP) {
            saveTimeToRTC(currentHour, currentMinute);
            currentState = SHOW_TIME;
        }
    } 
    else if (currentState == METRO_SHOW) {
        if (btn == PIN_BTN_UP) {
            editBPM = metroBPM;
            metroRunning = false;
            for (int i = 0; i < EQ_BAR_COUNT; i++) eqBars[i] = 0;
            currentState = METRO_EDIT;
            resetActivityTimer();
        } else {
            metroRunning = false;
            for (int i = 0; i < EQ_BAR_COUNT; i++) eqBars[i] = 0;
            currentState = SHOW_TIME;
        }
    }
}

void onShortPress(byte btn) {
    if (currentState == TIME_PICK) {
        editMode = !editMode;
    }
    else if (currentState == TIME_EDIT) {
        if (btn == PIN_BTN_UP) {
            if (editMode == 0) currentHour = (currentHour + 1) % 24;
            else currentMinute = (currentMinute + 1) % 60;
        } else {
            if (editMode == 0) currentHour = (currentHour - 1 + 24) % 24;
            else currentMinute = (currentMinute - 1 + 60) % 60;
        }
    }
    else if (currentState == METRO_SHOW) {
        metroRunning = !metroRunning;
        digitalWrite(PIN_LED, metroRunning ? LOW : HIGH);
        if (!metroRunning) {
            for (int i = 0; i < EQ_BAR_COUNT; i++) eqBars[i] = 0;
        }
    }
    else if (currentState == METRO_EDIT) {
        if (btn == PIN_BTN_UP) editBPM = constrain(editBPM + 1, MIN_BPM, MAX_BPM);
        else editBPM = constrain(editBPM - 1, MIN_BPM, MAX_BPM);
    }
}

void onHoldPress(byte btn) {
    if (currentState == TIME_EDIT) {
        if (btn == PIN_BTN_UP) {
            if (editMode == 0) currentHour = (currentHour + 10) % 24;
            else currentMinute = (currentMinute + 10) % 60;
        } else {
            if (editMode == 0) currentHour = (currentHour - 10 + 24) % 24;
            else currentMinute = (currentMinute - 10 + 60) % 60;
        }
    }
    else if (currentState == METRO_EDIT) {
        if (btn == PIN_BTN_UP) editBPM = constrain(editBPM + 10, MIN_BPM, MAX_BPM);
        else editBPM = constrain(editBPM - 10, MIN_BPM, MAX_BPM);
    }
}

void onBothHold() {
    if (currentState == TIME_PICK) {
        currentState = TIME_EDIT;
    }
    else if (currentState == TIME_EDIT) {
        currentState = TIME_PICK;
    }
    else if (currentState == METRO_EDIT) {
        metroBPM = editBPM;
        updateMetronomeTimer();
        currentState = METRO_SHOW;
    }
}

// ================== ОБРАБОТЧИК КНОПОК ==================
void handleButtons() {
    unsigned long now = millis();
    bool sUp = !digitalRead(PIN_BTN_UP);
    bool sDown = !digitalRead(PIN_BTN_DOWN);

    if (currentState == TIME_EDIT || currentState == METRO_EDIT) {
        showUpArrow = sUp && !sDown;
        showDownArrow = sDown && !sUp;
    } else {
        showUpArrow = false;
        showDownArrow = false;
    }

    if (currentState == SHOW_TIME_SLEEP) {
        if (sUp || sDown) {
            if (metroRunning) {
                currentState = METRO_SHOW;
                screenWokeUpFromMetro = true;
            } else {
                currentState = SHOW_TIME;
            }
            u8g2.setPowerSave(0);
            u8g2.setContrast(255);
            lastActivityTime = now;
            while(!digitalRead(PIN_BTN_UP) && !digitalRead(PIN_BTN_DOWN)) delay(5);
            return;
        }
        return;
    }

    // Кнопка ВВЕРХ
    if (sUp) {
        if (!btnUp.pressed) {
            btnUp.pressed = true;
            btnUp.pressStartTime = now;
            btnUp.longPressTriggered = false;
            btnUp.shortPressReady = false;
            lastActivityTime = now;
        }
        unsigned long dur = now - btnUp.pressStartTime;
        if (!btnUp.longPressTriggered) {
            if (sDown && dur >= 1000) {
                onBothHold();
                btnUp.longPressTriggered = true;
                btnDown.longPressTriggered = true;
            }
            else if (dur >= LONG_PRESS_MIN) {
                onLongPress(PIN_BTN_UP);
                btnUp.longPressTriggered = true;
            }
            else if (dur >= 1000 && (currentState == TIME_EDIT || currentState == METRO_EDIT)) {
                onHoldPress(PIN_BTN_UP);
                btnUp.longPressTriggered = true;
            }
        }
    } else {
        if (btnUp.pressed) {
            if (!btnUp.longPressTriggered && (now - btnUp.pressStartTime > 50)) {
                onShortPress(PIN_BTN_UP);
            }
            btnUp.pressed = false;
        }
    }

    // Кнопка ВНИЗ
    if (sDown) {
        if (!btnDown.pressed) {
            btnDown.pressed = true;
            btnDown.pressStartTime = now;
            btnDown.longPressTriggered = false;
            btnDown.shortPressReady = false;
            lastActivityTime = now;
        }
        unsigned long dur = now - btnDown.pressStartTime;
        if (!btnDown.longPressTriggered) {
            if (sUp && dur >= 1000) {
                onBothHold();
                btnUp.longPressTriggered = true;
                btnDown.longPressTriggered = true;
            }
            else if (dur >= LONG_PRESS_MIN) {
                onLongPress(PIN_BTN_DOWN);
                btnDown.longPressTriggered = true;
            }
            else if (dur >= 1000 && (currentState == TIME_EDIT || currentState == METRO_EDIT)) {
                onHoldPress(PIN_BTN_DOWN);
                btnDown.longPressTriggered = true;
            }
        }
    } else {
        if (btnDown.pressed) {
            if (!btnDown.longPressTriggered && (now - btnDown.pressStartTime > 50)) {
                onShortPress(PIN_BTN_DOWN);
            }
            btnDown.pressed = false;
        }
    }

    if (screenWokeUpFromMetro && currentState == METRO_SHOW) {
        screenWokeUpFromMetro = false;
        btnUp.longPressTriggered = true;
        btnDown.longPressTriggered = true;
    }
}

// ================== ОТРИСОВКА ДИСПЛЕЯ ==================
void drawUI() {
    u8g2.clearBuffer();
    
    char buf[6];
    
    switch (currentState) {
        case SHOW_TIME:
            drawMainClock();
            break;

        case SHOW_TIME_SLEEP:
            break;

        case TIME_PICK: {
            // Верхняя строка
            u8g2.setFont(u8g2_font_profont12_mf);
            u8g2.setDrawColor(1);
            u8g2.drawStr(2, 12, "SET TIME");
            u8g2.drawStr(85, 12, editMode == 0 ? "HOUR" : "MIN");
            
            // Подготавливаем строки
            char hourStr[3], minStr[3];
            sprintf(hourStr, "%02d", currentHour);
            sprintf(minStr, "%02d", currentMinute);
            
            // Устанавливаем шрифт для времени
            u8g2.setFont(u8g2_font_logisoso24_tr);
            
            // Вычисляем ширину для центрирования
            int hourW = u8g2.getStrWidth(hourStr);
            int colonW = u8g2.getStrWidth(":");
            int minW = u8g2.getStrWidth(minStr);
            
            // Дополнительный зазор чтобы квадрат не перекрывал двоеточие
            int gap = 4;
            int totalW = hourW + gap + colonW + gap + minW;
            int startX = (128 - totalW) / 2;
            
            // Координаты элементов (НИКОГДА не меняются)
            int hourX = startX;
            int colonX = startX + hourW + gap;
            int minX = startX + hourW + gap + colonW + gap;
            
            // Рисуем часы (с инверсией или без)
            drawHighlightedText(hourX, TEXT_Y_CENTER, hourStr, editMode == 0);
            
            // Двоеточие всегда белое
            u8g2.setDrawColor(1);
            u8g2.drawStr(colonX, TEXT_Y_CENTER, ":");
            
            // Рисуем минуты (с инверсией или без)
            drawHighlightedText(minX, TEXT_Y_CENTER, minStr, editMode == 1);
            
            // Нижняя подсказка
            u8g2.setFont(u8g2_font_5x8_tr);
            u8g2.setDrawColor(1);
            u8g2.drawStr(35, HELP_TEXT_Y, "BOTH OK to edit");
            break;
        }

        case TIME_EDIT: {
            // Верхняя строка
            u8g2.setFont(u8g2_font_profont12_mf);
            u8g2.setDrawColor(1);
            u8g2.drawStr(2, 12, "CHANGES...");
            u8g2.drawStr(85, 12, editMode == 0 ? "HOUR" : "MIN");
            
            // Редактируемое значение
            if (editMode == 0) {
                sprintf(buf, "%02d", currentHour);
            } else {
                sprintf(buf, "%02d", currentMinute);
            }
            
            u8g2.setFont(u8g2_font_logisoso24_tr);
            int w = u8g2.getStrWidth(buf);
            int h = u8g2.getMaxCharHeight();
            int textX = (128 - w) / 2;
            int centerY = TEXT_Y_CENTER - h / 2 + 2;
            
            // Стрелки при нажатии
            if (showUpArrow) {
                drawUpArrow(textX + w + 12, centerY);
            }
            if (showDownArrow) {
                drawDownArrow(textX - 8, centerY);
            }
            
            // Рисуем выделенный текст
            drawHighlightedText(textX, TEXT_Y_CENTER, buf, true);
            
            // Подсказки
            drawHelperText();
            break;
        }

        case METRO_SHOW: {
            // Верхняя строка
            u8g2.setFont(u8g2_font_profont12_mf);
            u8g2.setDrawColor(1);
            u8g2.drawStr(30, 12, "METRONOME");
            
            // BPM на одной линии
            u8g2.setFont(u8g2_font_logisoso24_tr);
            sprintf(buf, "%d", metroBPM);
            int w = u8g2.getStrWidth(buf);
            
            u8g2.setFont(u8g2_font_profont12_mf);
            int bpmW = u8g2.getStrWidth("BPM");
            int totalW = w + 4 + bpmW;
            int startX = (128 - totalW) / 2;
            
            // Число BPM
            u8g2.setFont(u8g2_font_logisoso24_tr);
            u8g2.setDrawColor(1);
            u8g2.drawStr(startX, TEXT_Y_CENTER, buf);
            
            // "BPM" на той же линии
            u8g2.setFont(u8g2_font_profont12_mf);
            u8g2.drawStr(startX + w + 4,