#include <ESP8266WiFi.h>
#include <WiFiUdp.h>
#include <NTPClient.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <ESP8266WebServer.h>
#include <time.h>

// Настройки для OLED-дисплея
#define SCREEN_WIDTH 128 // Ширина дисплея OLED в пикселях
#define SCREEN_HEIGHT 64 // Высота дисплея OLED в пикселях
#define OLED_RESET -1    // На некоторых модулях контроллер сброса не используется
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

// Настройки WiFi
const char* ssid = "Beeline_2G_A7F18A"; // Имя сети WiFi
const char* password = "iD2tAXJN3e";    // Пароль от WiFi

// Настройки NTP
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, "pool.ntp.org", 10800, 60000); // Смещение для Москвы +10800 секунд (3 часа)

// Настройки зуммера
const int buzzerPin = 14; // Пин, к которому подключён зуммер (GPIO14)

// Настройки веб-сервера
ESP8266WebServer server(80);

// Переменные для хранения времени будильника
int alarmHour = -1;
int alarmMinute = -1;

// IP-адрес сервера
String serverIP = "";

// Таймеры для ненавязчивого обновления дисплея и проверки будильника
unsigned long previousDisplayMillis = 0;
const unsigned long displayInterval = 1000; // Обновление дисплея каждые 1000 мс

unsigned long previousAlarmCheckMillis = 0;
const unsigned long alarmCheckInterval = 1000; // Проверка будильника каждые 1000 мс

// Флаг для предотвращения многократных срабатываний будильника в одну секунду
bool alarmTriggered = false;

// Время загрузки устройства
unsigned long bootFinishTime = 0;

// Флаг для начала анимации
bool animationStarted = false;

// Флаг показывает, анимация IP-адреса завершена
bool ipAnimationComplete = false;

// Структура для частиц пыли
struct Particle {
  float x;
  float y;
  float vx;
  float vy;
  bool active;
};

// Максимальное количество частиц
const int maxParticles = 100;
Particle particles[maxParticles];

// Текст IP-адреса
String ipText;

// Функция для форматирования чисел с ведущими нулями
String formatTwoDigits(int digits) {
  if (digits < 10) {
    return "0" + String(digits);
  }
  return String(digits);
}

// HTML и CSS для веб-интерфейса (темная тема macOS)
const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE HTML><html>
<head>
  <title>ESP8266 Clock</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <style>
    body {
      background-color: #2b2b2b;
      color: #e0e0e0;
      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
      text-align: center;
      padding: 40px;
    }
    .container {
      background-color: #3c3c3c;
      padding: 20px;
      border-radius: 10px;
      display: inline-block;
    }
    h1 {
      color: #f2f2f7;
    }
    .time {
      font-size: 2em;
      margin: 20px 0;
    }
    .date {
      font-size: 1.5em;
      margin-bottom: 30px;
    }
    input[type=number] {
      width: 60px;
      padding: 10px;
      margin: 5px;
      border: 1px solid #555555;
      border-radius: 5px;
      background-color: #4a4a4a;
      color: #e0e0e0;
      text-align: center;
      font-size: 1em;
    }
    input[type=submit] {
      padding: 10px 20px;
      margin-top: 10px;
      border: none;
      border-radius: 5px;
      background-color: #007aff;
      color: white;
      font-size: 1em;
      cursor: pointer;
    }
    input[type=submit]:hover {
      background-color: #0051a8;
    }
    .ip-address {
      margin-top: 20px;
      font-size: 0.9em;
      color: #a0a0a0;
    }
  </style>
</head>
<body>
  <div class="container">
    <h1>ESP8266 Clock</h1>
    <div class="time">--:--:--</div>
    <div class="date">--/--/----</div>
    <h2>Set Alarm</h2>
    <form action="/setAlarm" method="get">
      <input type="number" name="hour" min="0" max="23" placeholder="HH" required> :
      <input type="number" name="minute" min="0" max="59" placeholder="MM" required><br><br>
      <input type="submit" value="Set Alarm">
    </form>
  </div>
  <div class="ip-address">Server IP: --.--.--.--</div>
  
  <script>
    function updateTime() {
      fetch('/time')
        .then(response => response.json())
        .then(data => {
          document.querySelector('.time').innerText = data.time;
          document.querySelector('.date').innerText = data.date;
          document.querySelector('.ip-address').innerText = "Server IP: " + data.ip;
        });
    }
    setInterval(updateTime, 1000);
    window.onload = updateTime;
  </script>
</body>
</html>
)rawliteral";

// Функция для проверки аутентификации
bool checkAuthentication() {
  if (!server.authenticate("admin", "35539900")) {
    server.requestAuthentication();
    return false;
  }
  return true;
}

// Обработчик для главной страницы с аутентификацией
void handleRootWithAuth() {
  if (!checkAuthentication()) {
    return;
  }
  server.send_P(200, "text/html", index_html);
}

// Обработчик для получения текущего времени, даты и IP в формате JSON с аутентификацией
void handleTimeWithAuth() {
  if (!checkAuthentication()) {
    return;
  }

  unsigned long epochTime = timeClient.getEpochTime();
  time_t rawTime = epochTime;
  struct tm* ptm = gmtime(&rawTime); // Смещение уже учтено в NTPClient

  String formattedDate = String(ptm->tm_mday) + "/" +
                         String(ptm->tm_mon + 1) + "/" +
                         String(ptm->tm_year + 1900);
  String formattedTime = formatTwoDigits(ptm->tm_hour) + ":" +
                         formatTwoDigits(ptm->tm_min) + ":" +
                         formatTwoDigits(ptm->tm_sec);

  String json = "{\"date\":\"" + formattedDate + "\",\"time\":\"" + formattedTime + "\",\"ip\":\"" + serverIP + "\"}";

  server.send(200, "application/json", json);
}

// Обработчик для установки будильника с аутентификацией
void handleSetAlarmWithAuth() {
  if (!checkAuthentication()) {
    return;
  }

  if (server.hasArg("hour") && server.hasArg("minute")) {
    alarmHour = server.arg("hour").toInt();
    alarmMinute = server.arg("minute").toInt();
    String message = "Alarm set for " + String(formatTwoDigits(alarmHour)) + ":" + String(formatTwoDigits(alarmMinute));
    server.send(200, "text/html", "<html><body><script>alert('" + message + "'); window.location.href='/';</script></body></html>");
    Serial.println(message);
  } else {
    server.send(400, "text/html", "Bad Request");
    Serial.println("Bad Request: Missing hour or minute");
  }
}

// Функция для инициализации частиц пыли
void initializeParticles() {
  ipText = "IP: " + serverIP;
  int len = ipText.length();
  int startX = 10;
  int startY = 40;

  int spacing = 8; // Расстояние между символами

  for (int i = 0; i < len && i < maxParticles; i++) {
    Particle p;
    p.x = startX + i * spacing;
    p.y = startY;
    p.vx = random(-1, 2); // -1, 0, 1
    p.vy = random(-2, 0); // -2, -1, 0
    p.active = true;
    particles[i] = p;
  }
}

// Функция для обновления частиц
void updateParticles() {
  for (int i = 0; i < maxParticles; i++) {
    if (particles[i].active) {
      particles[i].x += particles[i].vx;
      particles[i].y += particles[i].vy;

      if (particles[i].x < 0 || particles[i].x >= SCREEN_WIDTH || particles[i].y < 0 || particles[i].y >= SCREEN_HEIGHT) {
        particles[i].active = false;
      }
    }
  }
}

// Функция для отображения частиц на OLED
void drawParticles() {
  for (int i = 0; i < maxParticles; i++) {
    if (particles[i].active) {
      display.drawPixel(particles[i].x, particles[i].y, SSD1306_WHITE);
    }
  }
}

// Функция для отображения сообщения о срабатывании будильника
void displayAlarmTriggered(String currentTime) {
  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(SSD1306_WHITE);
  int16_t x1, y1;
  uint16_t width, height;
  String message = "Alarm Triggered!";
  display.getTextBounds(message, 0, 0, &x1, &y1, &width, &height);
  int centeredX = (SCREEN_WIDTH - width) / 2;
  display.setCursor(centeredX, 0);
  display.println(message);
  message = "Time: " + currentTime;
  display.getTextBounds(message, 0, 0, &x1, &y1, &width, &height);
  centeredX = (SCREEN_WIDTH - width) / 2;
  display.setCursor(centeredX, 16);
  display.println(message);
  display.display();
}

// Функция для отображения сообщения о сбросе будильника
void displayAlarmReset() {
  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(SSD1306_WHITE);
  int16_t x1, y1;
  uint16_t width, height;
  String message = "Alarm Reset";
  display.getTextBounds(message, 0, 0, &x1, &y1, &width, &height);
  int centeredX = (SCREEN_WIDTH - width) / 2;
  display.setCursor(centeredX, 0);
  display.println(message);
  message = "No Active Alarm";
  display.getTextBounds(message, 0, 0, &x1, &y1, &width, &height);
  centeredX = (SCREEN_WIDTH - width) / 2;
  display.setCursor(centeredX, 16);
  display.println(message);
  display.display();
}

void setup() {
  // Инициализация последовательного порта для отладки
  Serial.begin(115200);
  delay(10);

  // Инициализация зуммера
  pinMode(buzzerPin, OUTPUT);
  digitalWrite(buzzerPin, LOW);

  // Инициализация OLED-дисплея
  if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { // Адрес 0x3C для большинства дисплеев
    Serial.println(F("Failed to initialize OLED display"));
    while (true);
  }

  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(SSD1306_WHITE);

  // Централизуем текст
  String message = "Starting device...";
  int16_t x1, y1;
  uint16_t width, height;
  display.getTextBounds(message, 0, 0, &x1, &y1, &width, &height);
  int centeredX = (SCREEN_WIDTH - width) / 2;
  int centeredY = (SCREEN_HEIGHT - height) / 2;

  display.setCursor(centeredX, centeredY);
  display.println(message);
  display.display();
  delay(1000); // 1 секунда

  Serial.println("Starting device...");

  // Подключение к WiFi
  Serial.print("Connecting to ");
  Serial.println(ssid);
  WiFi.begin(ssid, password);

  int dotCount = 0;
  const int maxDots = 3;

  // OLED лог подключения с анимацией точек
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
    dotCount = (dotCount + 1) % (maxDots + 1); // 0,1,2,3 повторяются

    display.clearDisplay();
    display.setTextSize(1);
    display.setTextColor(SSD1306_WHITE);
    String connectingMessage = "Connecting to WiFi";
    display.getTextBounds(connectingMessage, 0, 0, &x1, &y1, &width, &height);
    centeredX = (SCREEN_WIDTH - width) / 2;
    centeredY = (SCREEN_HEIGHT - height) / 2;

    display.setCursor(centeredX, centeredY);
    display.print(connectingMessage);
    for (int i = 0; i < dotCount; i++) {
      display.print(".");
    }
    display.display();
  }

  Serial.println("");
  Serial.println("WiFi connected");
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());

  // Сохранение IP-адреса
  serverIP = WiFi.localIP().toString();

  // Отображение успешного подключения на OLED
  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(SSD1306_WHITE);
  String wifiConnectedMessage = "WiFi connected";
  display.getTextBounds(wifiConnectedMessage, 0, 0, &x1, &y1, &width, &height);
  centeredX = (SCREEN_WIDTH - width) / 2;
  centeredY = (SCREEN_HEIGHT - height) / 3;

  display.setCursor(centeredX, centeredY);
  display.println(wifiConnectedMessage);
  String ipMessage = "IP: " + serverIP;
  display.getTextBounds(ipMessage, 0, 0, &x1, &y1, &width, &height);
  centeredX = (SCREEN_WIDTH - width) / 2;
  centeredY = 2 * (SCREEN_HEIGHT - height) / 3;

  display.setCursor(centeredX, centeredY);
  display.println(ipMessage);
  display.display();
  delay(2000); // 2 секунды

  // Короткий звуковой сигнал зуммера
  digitalWrite(buzzerPin, HIGH);
  delay(200);
  digitalWrite(buzzerPin, LOW);
  delay(200);

  // Инициализация NTP клиента
  timeClient.begin();
  timeClient.update();

  if (timeClient.getEpochTime() > 0) {
    Serial.println("Time successfully obtained");
    digitalWrite(buzzerPin, HIGH);
    delay(200);
    digitalWrite(buzzerPin, LOW);
  } else {
    Serial.println("Failed to obtain time");
  }

  server.on("/", handleRootWithAuth);
  server.on("/time", handleTimeWithAuth);
  server.on("/setAlarm", handleSetAlarmWithAuth);

  server.onNotFound([]() {
    if (!server.authenticate("admin", "35539900")) {
      server.requestAuthentication();
      return;
    }
    server.send(404, "text/plain", "404: Not found");
  });

  server.begin();
  Serial.println("HTTP server started");

  // Запись времени завершения загрузки
  bootFinishTime = millis();
}

void loop() {
  server.handleClient();
  timeClient.update();

  unsigned long epochTime = timeClient.getEpochTime();
  time_t rawTime = epochTime;
  struct tm* ptm = gmtime(&rawTime);

  String formattedDate = String(ptm->tm_mday) + "/" +
                         String(ptm->tm_mon + 1) + "/" +
                         String(ptm->tm_year + 1900);
  String formattedTime = formatTwoDigits(ptm->tm_hour) + ":" +
                         formatTwoDigits(ptm->tm_min) + ":" +
                         formatTwoDigits(ptm->tm_sec);

  unsigned long currentMillis = millis();

  // Если прошло менее 15 секунд после загрузки, продолжаем показывать IP
  if (currentMillis - bootFinishTime < 15000 || ipAnimationComplete) {
    if (currentMillis - previousDisplayMillis >= displayInterval) {
      previousDisplayMillis = currentMillis;

      display.clearDisplay();

      display.setTextSize(2);
      display.setTextColor(SSD1306_WHITE);
      int16_t x1, y1;
      uint16_t width, height;
      display.getTextBounds(formattedTime, 0, 0, &x1, &y1, &width, &height);
      int centeredX = (SCREEN_WIDTH - width) / 2;
      display.setCursor(centeredX, 0);
      display.println(formattedTime);

      display.setTextSize(1);
      display.getTextBounds(formattedDate, 0, 0, &x1, &y1, &width, &height);
      centeredX = (SCREEN_WIDTH - width) / 2;
      display.setCursor(centeredX, 24);
      display.println(formattedDate);

      if (!ipAnimationComplete) {
        String ipMessage = "IP: " + serverIP;
        display.getTextBounds(ipMessage, 0, 0, &x1, &y1, &width, &height);
        centeredX = (SCREEN_WIDTH - width) / 2;
        display.setCursor(centeredX, 40);
        display.println(ipMessage);
      }

      display.display();
    }
  } else if (!animationStarted && !ipAnimationComplete) {
    // После 15 секунд запускаем анимацию
    animationStarted = true;
    initializeParticles();
  }

  if (animationStarted && !ipAnimationComplete) {
    updateParticles();

    display.clearDisplay();
    display.setTextSize(2);
    display.setTextColor(SSD1306_WHITE);
    int16_t x1, y1;
    uint16_t width, height;
    display.getTextBounds(formattedTime, 0, 0, &x1, &y1, &width, &height);
    int centeredX = (SCREEN_WIDTH - width) / 2;
    display.setCursor(centeredX, 0);
    display.println(formattedTime);

    display.setTextSize(1);
    display.getTextBounds(formattedDate, 0, 0, &x1, &y1, &width, &height);
    centeredX = (SCREEN_WIDTH - width) / 2;
    display.setCursor(centeredX, 24);
    display.println(formattedDate);

    drawParticles();

    display.display();

    if (!anyParticlesActive()) {
      animationStarted = false;
      ipAnimationComplete = true; // Флаг указывает, что IP анимация завершена
    }
  }

  if (alarmHour != -1 && alarmMinute != -1) {
    if (currentMillis - previousAlarmCheckMillis >= alarmCheckInterval) {
      previousAlarmCheckMillis = currentMillis;

      if (ptm->tm_hour == alarmHour && ptm->tm_min == alarmMinute && ptm->tm_sec == 0) {
        if (!alarmTriggered) {
          digitalWrite(buzzerPin, HIGH);
          Serial.println("Alarm Triggered!");
          displayAlarmTriggered(formattedTime);
          alarmTriggered = true;
        }
      } else {
        alarmTriggered = false;
      }
    }
  }

  delay(10);
} добавь в код анимацию времени имитирующую работу счетчика 

bool anyParticlesActive() {
  for (int i = 0; i < maxParticles; i++) {
    if (particles[i].active) {
      return true;
    }
  }
  return false;
}