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


// ============================================================
// 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