код #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;

// Переменные для анимации
bool isAnimating = false;
unsigned long animationStartTime = 0;
const unsigned long animationDuration = 5000; // Продолжительность анимации 5 секунд
int animationFrame = 0;

// Структура для частиц пыли
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() {
  // Разбиваем IP-адрес на символы и распределяем частицы
  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);
  display.setCursor(0, 0);
  display.println("Alarm Triggered!");
  display.setCursor(0, 16);
  display.println("Time: " + currentTime);
  display.display();
}

// Функция для отображения сообщения о сбросе будильника
void displayAlarmReset() {
  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(SSD1306_WHITE);
  display.setCursor(0, 0);
  display.println("Alarm Reset");
  display.setCursor(0, 16);
  display.println("No Active Alarm");
  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);
  display.setCursor(0, 0);
  display.println("Starting device...");
  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);
    display.setCursor(0, 0);
    display.print("Connecting to WiFi");
    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);
  display.setCursor(0, 0);
  display.println("WiFi connected");
  display.println("IP: " + serverIP);
  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");

  // Обновление OLED с информацией после запуска сервера
  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(SSD1306_WHITE);
  display.setCursor(0, 0);
  display.println("HTTP server started");
  display.println("IP: " + serverIP);
  display.display();
  
  // Запись времени завершения загрузки
  bootFinishTime = millis();
}

void loop() {
  // Обработка веб-запросов
  server.handleClient();

  // Обновление времени
  timeClient.update();

  // Получение текущего времени
  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);

  unsigned long currentMillis = millis();

  // Запуск анимации через 10 секунд после загрузки
  if (!animationStarted && (currentMillis - bootFinishTime >= 10000)) {
    animationStarted = true;
    isAnimating = true;
    animationStartTime = currentMillis;
    initializeParticles(); // Инициализация частиц
  }

  // Если анимация запущена, выполняем её
  if (isAnimating) {
    // Проверяем, не закончилась ли анимация
    if (currentMillis - animationStartTime <= animationDuration) {
      // Обновляем частицы
      updateParticles();

      // Отображаем частиц
      display.clearDisplay();
      
      // Перерисовываем основные элементы экрана
      // Время
      display.setTextSize(2);
      display.setTextColor(SSD1306_WHITE);
      display.setCursor(0, 0);
      display.println(formattedTime);
      
      // Дата
      display.setTextSize(1);
      display.setCursor(0, 24);
      display.println(formattedDate);
      
      // Отображаем активные частицы (пыль)
      drawParticles();
      
      display.display();
    } else {
      // Заканчиваем анимацию
      isAnimating = false;
      Serial.println("Animation completed");
    }
  } else {
    // Если анимация не запущена, продолжаем обычное отображение времени, даты и IP
    // Обновляем дисплей только раз в секунду
    if (currentMillis - previousDisplayMillis >= displayInterval) {
      previousDisplayMillis = currentMillis;

      display.clearDisplay();

      // Отображение времени
      display.setTextSize(2);
      display.setTextColor(SSD1306_WHITE);
      display.setCursor(0, 0);
      display.println(formattedTime);

      // Отображение даты
      display.setTextSize(1);
      display.setCursor(0, 24);
      display.println(formattedDate);

      // Отображение IP-адреса
      display.setTextSize(1);
      display.setCursor(0, 40);
      display.println("IP: " + serverIP);

      display.display();
    }
  }

  // Проверка будильника
  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);
}

// Функции для инициализации, обновления и отображения частиц пыли

void initializeParticles() {
  // Разбиваем IP-адрес на символы и распределяем частицы
  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(-2, 3) / 2.0; // -1, -0.5, 0, 0.5, 1
    p.vy = random(-3, 0) / 2.0; // -1.5, -1, -0.5, 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;
      }
    }
  }
}

void drawParticles() {
  for(int i=0; i<maxParticles; i++) {
    if(particles[i].active) {
      display.drawPixel(particles[i].x, particles[i].y, SSD1306_WHITE);
    }
  }
} ошибка /Users/pavelmakov/Documents/Arduino/S_C_ESP_with_logo/S_C_ESP_with_logo.ino:539:6: error: redefinition of 'void initializeParticles()'
/Users/pavelmakov/Documents/Arduino/S_C_ESP_with_logo/S_C_ESP_with_logo.ino:233:6: note: 'void initializeParticles()' previously defined here
  233 |   WiFi.begin(ssid, password);
      |      ^~~~~~~~~~~~~~~~~~~
/Users/pavelmakov/Documents/Arduino/S_C_ESP_with_logo/S_C_ESP_with_logo.ino:560:6: error: redefinition of 'void updateParticles()'
/Users/pavelmakov/Documents/Arduino/S_C_ESP_with_logo/S_C_ESP_with_logo.ino:255:6: note: 'void updateParticles()' previously defined here
  255 | 
      |      ^              
/Users/pavelmakov/Documents/Arduino/S_C_ESP_with_logo/S_C_ESP_with_logo.ino:574:6: error: redefinition of 'void drawParticles()'
/Users/pavelmakov/Documents/Arduino/S_C_ESP_with_logo/S_C_ESP_with_logo.ino:270:6: note: 'void drawParticles()' previously defined here
  270 |   display.println("IP: " + serverIP);
      |      ^~~~~~~~~~~~~