Загрузка данных
// ============================================================
// FIRMWARE: Clock + Metronome (ESP32 Super Mini)
// Libraries required:
// 1. U8g2 — by olikraus (Менеджер бібліотек: "U8g2")
// 2. ESP32Time — by fbiego (Менеджер бібліотек: "ESP32Time")
// Built-in (no install needed):
// Wire.h, math.h
// ============================================================
#include <Wire.h>
#include <U8g2lib.h>
#include <math.h>
#include <ESP32Time.h>
#define TILTED_CLOCK true
// ESP32 Super Mini pins: GPIO8=SDA, GPIO9=SCL, GPIO10=LED
// Перевір свої піни. Тут ставлю безпечні для Super Mini
#define BTN_UP 2
#define BTN_DOWN 3
#define LED_PIN 10 // На Super Mini GPIO9 це Strapping pin, краще взяти 10
#define SDA_PIN 3
#define SCL_PIN 4
// Для Super Mini явно вказуємо піни I2C
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE, SCL_PIN, SDA_PIN);
const unsigned long DEBOUNCE_MS = 40;
const unsigned long HOLD_INCREMENT_INTERVAL_MS = 200;
const unsigned long LONG_PRESS_MS = 2000;
const unsigned long BOTH_OK_HOLD_MS = 2000;
const unsigned long BOTH_PRESS_WINDOW = 200;
const unsigned long LED_PULSE_MS = 35;
const unsigned long SLEEP_TIMEOUT_MS = 5000;
#define HOLD_MS LONG_PRESS_MS
ESP32Time rtc(0); // offset in seconds
enum State : uint8_t {
SLEEP,
SHOW_TIME,
TIME_PICK,
TIME_EDIT,
METRO_SHOW,
METRO_EDIT
};
State state = SHOW_TIME;
byte editMode = 0;
unsigned long lastActivityTime = 0;
struct Button {
byte pin;
bool isPressed;
bool lastIsPressed;
unsigned long pressStartTime;
bool longPressFired;
bool holdIncrementFired;
unsigned long lastHoldIncrementTime;
};
Button upBtn = {BTN_UP, false, false, 0, false, false, 0};
Button downBtn = {BTN_DOWN, false, false, 0, false, false, 0};
int bpm = 100;
bool metroRunning = false;
bool ledPulse = false;
unsigned long ledOffAt = 0;
hw_timer_t *timer = NULL;
volatile bool metroBeat = false;
void IRAM_ATTR onTimer() {
metroBeat = true;
}
void updateButtonState(Button &b) {
b.lastIsPressed = b.isPressed;
bool rawState =!digitalRead(b.pin);
static unsigned long lastChangeTime[2] = {0, 0};
int btnIndex = (b.pin == BTN_UP)? 0 : 1;
if (rawState!= b.isPressed) {
if (millis() - lastChangeTime[btnIndex] > DEBOUNCE_MS) {
b.isPressed = rawState;
lastChangeTime[btnIndex] = millis();
}
}
}
void drawInverseTextBox(int x, int y, const char *txt, const uint8_t *font) {
u8g2.setFont(font);
int w = u8g2.getStrWidth(txt);
int fh = u8g2.getAscent() - u8g2.getDescent();
u8g2.drawBox(x, y - fh + 2, w + 6, fh + 4);
u8g2.setDrawColor(0);
u8g2.drawStr(x + 3, y, txt);
u8g2.setDrawColor(1);
}
void drawRotatedText(int x, int y, const char *text, float angle_deg, const uint8_t *font) {
u8g2.setFont(font);
float rad = angle_deg * (float)M_PI / 180.0f;
float cosA = cos(rad);
float sinA = sin(rad);
int curX = 0;
for (const char *p = text; *p; p++) {
char c[2] = {*p, '\0'}; // FIX: '\0' instead of ''
int rx = x + (int)(curX * cosA);
int ry = y - (int)(curX * sinA);
u8g2.setCursor(rx, ry);
u8g2.print(*p);
curX += u8g2.getStrWidth(c) + 1;
}
}
void gotoShowTime() {
state = SHOW_TIME;
lastActivityTime = millis();
}
void goSleep() {
state = SLEEP;
u8g2.clearBuffer();
u8g2.sendBuffer();
digitalWrite(LED_PIN, LOW);
}
void setTimerBPM(int newBpm) {
bpm = constrain(newBpm, 40, 220);
// ESP32 Core 3.x: timerAlarm(pointer, ticks, autoreload, reloadCount)
// 80MHz / 80 = 1MHz = 1us per tick
timerAlarm(timer, 60000000ULL / bpm, true, 0);
}
void handleShortPress(byte btn) {
lastActivityTime = millis();
if (state == TIME_PICK) {
editMode = (btn == BTN_UP)? 0 : 1;
} else if (state == TIME_EDIT) {
int h = rtc.getHour();
int m = rtc.getMinute();
if (editMode == 0) { // hour
h = (btn == BTN_UP)? (h + 1) % 24 : (h + 23) % 24;
} else { // minute
m = (btn == BTN_UP)? (m + 1) % 60 : (m + 59) % 60;
}
rtc.setTime(rtc.getSecond(), m, h, rtc.getDay(), rtc.getMonth(), rtc.getYear());
} else if (state == METRO_SHOW) {
metroRunning =!metroRunning;
if (metroRunning) {
timerStart(timer); // FIX: new API
} else {
timerStop(timer); // FIX: new API
digitalWrite(LED_PIN, LOW);
ledPulse = false;
}
} else if (state == METRO_EDIT) {
if (btn == BTN_UP) setTimerBPM(bpm + 1);
else setTimerBPM(bpm - 1);
}
}
void handleHoldIncrement(byte btn) {
lastActivityTime = millis();
if (state == TIME_EDIT) {
int h = rtc.getHour();
int m = rtc.getMinute();
if (editMode == 0) { // hour
h = (btn == BTN_UP)? (h + 10) % 24 : (h + 14) % 24;
} else { // minute
m = (btn == BTN_UP)? (m + 10) % 60 : (m + 50) % 60;
}
rtc.setTime(rtc.getSecond(), m, h, rtc.getDay(), rtc.getMonth(), rtc.getYear());
} else if (state == METRO_EDIT) {
if (btn == BTN_UP) setTimerBPM(bpm + 10);
else setTimerBPM(bpm - 10);
}
}
void handleLongPress(byte btn) {
lastActivityTime = millis();
if (state == SHOW_TIME) {
if (btn == BTN_UP) {
state = TIME_PICK;
editMode = 0;
} else {
state = METRO_SHOW;
metroRunning = false;
timerStop(timer); // FIX: new API
digitalWrite(LED_PIN, LOW);
ledPulse = false;
}
} else if (state == TIME_PICK || state == TIME_EDIT) {
if (btn == BTN_UP) gotoShowTime();
} else if (state == METRO_SHOW || state == METRO_EDIT) {
if (btn == BTN_DOWN) gotoShowTime();
}
}
void handleBothOK() {
lastActivityTime = millis();
if (state == TIME_PICK) {
state = TIME_EDIT;
} else if (state == TIME_EDIT) {
state = TIME_PICK;
} else if (state == METRO_EDIT) {
state = METRO_SHOW;
setTimerBPM(bpm);
}
}
void renderScreen() {
if (state == SLEEP) return;
u8g2.clearBuffer();
int current_h = rtc.getHour();
int current_m = rtc.getMinute();
int current_s = rtc.getSecond();
if (state == SHOW_TIME) {
char secTime[20];
int h12 = current_h % 12;
if (h12 == 0) h12 = 12;
const char *ampm = (current_h >= 12)? "PM" : "AM";
sprintf(secTime, "%02d:%02d:%02d %s", h12, current_m, current_s, ampm);
u8g2.setFont(u8g2_font_6x10_tr);
u8g2.drawStr(0, 10, secTime);
char mainTime[6];
sprintf(mainTime, "%02d:%02d", current_h, current_m);
#if TILTED_CLOCK
drawRotatedText(14, 58, mainTime, 30.0f, u8g2_font_profont29_tr);
#else
u8g2.setFont(u8g2_font_profont29_tr);
int tw = u8g2.getStrWidth(mainTime);
u8g2.drawStr((128 - tw) / 2, 48, mainTime);
#endif
} else if (state == TIME_PICK) {
u8g2.setFont(u8g2_font_6x10_tr);
u8g2.drawStr(4, 10, "SET TIME");
u8g2.drawStr(92, 10, editMode == 0? "HOUR" : "MIN");
char hh[3], mm[3];
sprintf(hh, "%02d", current_h);
sprintf(mm, "%02d", current_m);
u8g2.setFont(u8g2_font_profont22_tr);
if (editMode == 0) {
drawInverseTextBox(22, 42, hh, u8g2_font_profont22_tr);
u8g2.drawStr