Загрузка данных
#include <Wire.h>
#include <U8g2lib.h>
// Инициализация экрана 128x64 I2C (адрес 0x3C)
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);
// ═══════════════════════════════════════
// ПИНЫ
// ═══════════════════════════════════════
#define BTN_UP 2
#define BTN_DOWN 3
#define LED_PIN 9
// ═══════════════════════════════════════
// НАСТРОЙКИ ТАЙМИНГОВ
// ══════════════════════════════════════
#define DEBOUNCE_MS 50 // Антидребезг
#define SHORT_PRESS_MAX 600 // Короткое нажатие (<0.6с)
#define MEDIUM_PRESS_MIN 800 // Среднее нажатие (0.8-2.4с)
#define LONG_PRESS_MIN 2500 // Долгое нажатие (>2.5с)
#define SIMULTANEOUS_TOL 250 // Погрешность одновременного нажатия (мс)
// ═══════════════════════════════════════
// ПЕРЕМЕННЫЕ ВРЕМЕНИ
// ═══════════════════════════════════════
byte h = 14, m = 43, s = 30;
unsigned long lastTick = 0;
// ═══════════════════════════════════════
// ПЕРЕМЕННЫЕ МЕТРОНОМА
// ══════════════════════════════════════
int bpm = 100;
bool metroRunning = false;
unsigned long lastBeat = 0;
// ═══════════════════════════════════════
// СОСТОЯНИЯ ИНТЕРФЕЙСА
// ═══════════════════════════════════════
enum State {
SLEEP,
SHOW_TIME,
SET_TIME_SELECT,
SET_TIME_HOURS,
SET_TIME_MINS,
METRO_SHOW,
METRO_ADJUST
};
State currentState = SLEEP;
// ═══════════════════════════════════════
// КНОПКИ (ВСЕ ОБЪЯВЛЕНЫ ГЛОБАЛЬНО)
// ═══════════════════════════════════════
bool btnUpPressed = false;
bool btnDownPressed = false;
bool btnUpActive = false;
bool btnDownActive = false;
unsigned long btnUpPressTime = 0;
unsigned long btnDownPressTime = 0;
bool btnBothActive = false;
// ═══════════════════════════════════════
// SETUP
// ═══════════════════════════════════════
void setup() {
pinMode(BTN_UP, INPUT_PULLUP);
pinMode(BTN_DOWN, INPUT_PULLUP);
pinMode(LED_PIN, OUTPUT);
digitalWrite(LED_PIN, LOW);
u8g2.begin();
u8g2.setFontMode(1);
lastTick = millis();
}
// ═══════════════════════════════════════
// LOOP
// ═══════════════════════════════════════
void loop() {
updateButtons();
handleStateLogic();
drawScreen();
}
// ═══════════════════════════════════════
// ОБРАБОТКА КНОПОК (НАДЁЖНАЯ, БЕЗ ЗАДЕРЖЕК)
// ══════════════════════════════════════
void updateButtons() {
bool upNow = (digitalRead(BTN_UP) == LOW);
bool downNow = (digitalRead(BTN_DOWN) == LOW);
unsigned long now = millis();
// Верхняя кнопка
if (upNow && !btnUpActive) {
btnUpActive = true;
btnUpPressed = true;
btnUpPressTime = now;
} else if (!upNow && btnUpActive) {
btnUpActive = false;
btnUpPressed = false; }
// Нижняя кнопка
if (downNow && !btnDownActive) {
btnDownActive = true;
btnDownPressed = true;
btnDownPressTime = now;
} else if (!downNow && btnDownActive) {
btnDownActive = false;
btnDownPressed = false;
}
// Детекция одновременного нажатия
if (btnUpPressed && btnDownPressed && !btnBothActive) {
if (abs((long)(btnUpPressTime - btnDownPressTime)) <= SIMULTANEOUS_TOL) {
btnBothActive = true;
triggerBothPress();
}
}
if (!btnUpPressed && !btnDownPressed) btnBothActive = false;
}
// ═══════════════════════════════════════
// ЛОГИКА СОСТОЯНИЙ
// ═══════════════════════════════════════
void handleStateLogic() {
unsigned long now = millis();
unsigned long upDur = now - btnUpPressTime;
unsigned long downDur = now - btnDownPressTime;
switch (currentState) {
case SLEEP:
if (btnDownPressed) { // Любое нажатие будит
currentState = SHOW_TIME;
btnDownPressed = false;
}
break;
case SHOW_TIME:
// Обновление времени
if (now - lastTick >= 1000) {
lastTick += 1000;
s++;
if (s >= 60) { s = 0; m++; if (m >= 60) { m = 0; h++; if (h >= 24) h = 0; } }
}
// Долгое нажатие ВВЕРХ → Настройка времени
if (btnUpActive && upDur >= LONG_PRESS_MIN) {
currentState = SET_TIME_SELECT;
btnUpActive = false; // Сброс чтобы не сработало дважды
} // Долгое нажатие ВНИЗ → Метроном
if (btnDownActive && downDur >= LONG_PRESS_MIN) {
currentState = METRO_SHOW;
btnDownActive = false;
}
break;
case SET_TIME_SELECT:
if (btnUpPressed) { currentState = SET_TIME_HOURS; btnUpPressed = false; }
if (btnDownPressed) { currentState = SET_TIME_MINS; btnDownPressed = false; }
break;
case SET_TIME_HOURS:
handleTimeAdjust(true);
if (btnBothActive) { currentState = SET_TIME_SELECT; btnBothActive = false; }
break;
case SET_TIME_MINS:
handleTimeAdjust(false);
if (btnBothActive) { currentState = SHOW_TIME; btnBothActive = false; }
break;
case METRO_SHOW:
// Любая короткая кнопка → старт/стоп
if (btnUpPressed || btnDownPressed) {
metroRunning = !metroRunning;
if (metroRunning) lastBeat = now;
btnUpPressed = false; btnDownPressed = false;
}
// Обе кнопки → настройка BPM
if (btnBothActive) { currentState = METRO_ADJUST; btnBothActive = false; }
// Долгое нажатие любой → выход
if ((btnUpActive && upDur >= LONG_PRESS_MIN) || (btnDownActive && downDur >= LONG_PRESS_MIN)) {
currentState = SHOW_TIME;
metroRunning = false;
btnUpActive = false; btnDownActive = false;
}
// Тик метронома
if (metroRunning) {
unsigned long interval = 60000UL / bpm;
if (now - lastBeat >= interval) {
lastBeat += interval;
digitalWrite(LED_PIN, HIGH);
// Кратковременный импульс без delay()
static unsigned long ledOffTime = 0;
ledOffTime = now + 60;
// Логика выключения в drawScreen или отдельном таймере
}
if (now > (lastBeat - 50) && now < (lastBeat + 10)) { // Импульс ~60мс
digitalWrite(LED_PIN, HIGH); } else {
digitalWrite(LED_PIN, LOW);
}
} else {
digitalWrite(LED_PIN, LOW);
}
break;
case METRO_ADJUST:
// Изменение BPM
unsigned long upDurAdj = now - btnUpPressTime;
unsigned long downDurAdj = now - btnDownPressTime;
if (btnUpPressed) { bpm = min(220, bpm + 1); btnUpPressed = false; }
if (btnDownPressed) { bpm = max(40, bpm - 1); btnDownPressed = false; }
if (btnUpActive && upDurAdj >= MEDIUM_PRESS_MIN && upDurAdj < LONG_PRESS_MIN) {
bpm = min(220, bpm + 10);
btnUpActive = false; // Сброс среднего нажатия
}
if (btnDownActive && downDurAdj >= MEDIUM_PRESS_MIN && downDurAdj < LONG_PRESS_MIN) {
bpm = max(40, bpm - 10);
btnDownActive = false;
}
if (btnBothActive) { currentState = METRO_SHOW; btnBothActive = false; }
if ((btnUpActive && upDurAdj >= LONG_PRESS_MIN) || (btnDownActive && downDurAdj >= LONG_PRESS_MIN)) {
currentState = METRO_SHOW;
btnUpActive = false; btnDownActive = false;
}
break;
}
}
void handleTimeAdjust(bool isHours) {
unsigned long now = millis();
unsigned long upDur = now - btnUpPressTime;
unsigned long downDur = now - btnDownPressTime;
if (btnUpPressed) {
if (isHours) h = (h + 1) % 24; else m = (m + 1) % 60;
btnUpPressed = false;
}
if (btnDownPressed) {
if (isHours) h = (h + 23) % 24; else m = (m + 59) % 60;
btnDownPressed = false;
}
if (btnUpActive && upDur >= MEDIUM_PRESS_MIN && upDur < LONG_PRESS_MIN) {
if (isHours) h = (h + 10) % 24; else m = (m + 10) % 60; btnUpActive = false;
}
if (btnDownActive && downDur >= MEDIUM_PRESS_MIN && downDur < LONG_PRESS_MIN) {
if (isHours) h = (h + 14) % 24; else m = (m + 50) % 60;
btnDownActive = false;
}
}
void triggerBothPress() {
// Обработка уже в handleStateLogic для чистоты
}
// ═══════════════════════════════════════
// ОТРИСОВКА
// ═══════════════════════════════════════
void drawScreen() {
u8g2.clearBuffer();
switch (currentState) {
case SLEEP:
// Черный экран
break;
case SHOW_TIME: {
// Основное время (увеличенное)
u8g2.setFont(u8g2_font_profont29_tr);
char buf[8];
sprintf(buf, "%02d:%02d", h, m);
int w = u8g2.getStrWidth(buf);
u8g2.drawStr((128 - w) / 2, 42, buf);
// Второстепенная строка
u8g2.setFont(u8g2_font_6x10_tr);
byte h12 = (h == 0 || h == 12) ? 12 : (h > 12 ? h - 12 : h);
sprintf(buf, "%02d:%02d:%02d %s", h12, m, s, h < 12 ? "AM" : "PM");
w = u8g2.getStrWidth(buf);
u8g2.drawStr((128 - w) / 2, 60, buf);
break;
}
case SET_TIME_SELECT: {
u8g2.setFont(u8g2_font_profont22_tr);
char buf[4];
// Часы
sprintf(buf, "%02d", h);
int wH = u8g2.getStrWidth(buf);
u8g2.drawBox(24, 18, wH + 6, 26);
u8g2.setDrawColor(0);
u8g2.drawStr(27, 38, buf); u8g2.setDrawColor(1);
u8g2.drawStr(56, 38, ":");
// Минуты
sprintf(buf, "%02d", m);
int wM = u8g2.getStrWidth(buf);
u8g2.drawBox(68, 18, wM + 6, 26);
u8g2.setDrawColor(0);
u8g2.drawStr(71, 38, buf);
u8g2.setDrawColor(1);
u8g2.setFont(u8g2_font_5x8_tr);
u8g2.drawStr(8, 10, "UP: HOURS DOWN: MINS");
u8g2.drawStr(25, 60, "BOTH: ADJUST");
break;
}
case SET_TIME_HOURS: {
u8g2.setFont(u8g2_font_profont22_tr);
char buf[4];
// Часы (Инверсия)
sprintf(buf, "%02d", h);
int w = u8g2.getStrWidth(buf);
u8g2.drawBox(24, 18, w + 6, 26);
u8g2.setDrawColor(0);
u8g2.drawStr(27, 38, buf);
u8g2.setDrawColor(1);
u8g2.drawStr(56, 38, ":");
// Минуты + Стрелки
sprintf(buf, "%02d", m);
u8g2.drawStr(71, 38, buf);
u8g2.setFont(u8g2_font_6x10_tr);
u8g2.drawStr(98, 28, "↑");
u8g2.drawStr(98, 44, "↓");
u8g2.setFont(u8g2_font_5x8_tr);
u8g2.drawStr(8, 10, "ADJUSTING HOURS");
u8g2.drawStr(20, 60, "BOTH: BACK");
break;
}
case SET_TIME_MINS: {
u8g2.setFont(u8g2_font_profont22_tr);
char buf[4];
// Часы + Стрелки sprintf(buf, "%02d", h);
u8g2.drawStr(27, 38, buf);
u8g2.setFont(u8g2_font_6x10_tr);
u8g2.drawStr(10, 28, "↑");
u8g2.drawStr(10, 44, "↓");
u8g2.setFont(u8g2_font_profont22_tr);
u8g2.drawStr(56, 38, ":");
// Минуты (Инверсия)
sprintf(buf, "%02d", m);
int w = u8g2.getStrWidth(buf);
u8g2.drawBox(68, 18, w + 6, 26);
u8g2.setDrawColor(0);
u8g2.drawStr(71, 38, buf);
u8g2.setDrawColor(1);
u8g2.setFont(u8g2_font_5x8_tr);
u8g2.drawStr(8, 10, "ADJUSTING MINS");
u8g2.drawStr(20, 60, "BOTH: CONFIRM & EXIT";
break;
}
case METRO_SHOW: {
u8g2.setFont(u8g2_font_profont29_tr);
char buf[10];
sprintf(buf, "%d BPM", bpm);
int w = u8g2.getStrWidth(buf);
u8g2.drawStr((128 - w) / 2, 42, buf);
u8g2.setFont(u8g2_font_6x10_tr);
if (metroRunning) {
u8g2.drawStr(30, 60, "● RUNNING");
} else {
u8g2.drawStr(15, 60, "PUSH ANY TO START");
}
u8g2.drawStr(45, 10, "BOTH: ADJUST");
break;
}
case METRO_ADJUST: {
u8g2.setFont(u8g2_font_profont29_tr);
char buf[6];
sprintf(buf, "%d", bpm);
int w = u8g2.getStrWidth(buf);
int x = (128 - w) / 2;
// Инверсия (белый квадрат, черный текст)
u8g2.drawBox(x - 4, 15, w + 8, 38);
u8g2.setDrawColor(0); u8g2.drawStr(x, 48, buf);
u8g2.setDrawColor(1);
u8g2.setFont(u8g2_font_6x10_tr);
u8g2.drawStr(45, 60, "BPM");
u8g2.setFont(u8g2_font_5x8_tr);
u8g2.drawStr(5, 10, "UP/DOWN: CHANGE");
u8g2.drawStr(25, 60, "BOTH: CONFIRM";
break;
}
}
u8g2.sendBuffer();
}