Создаём точку доступа с порталом захвата на ESP32: анализ кода и разбор функций
В мире встраиваемых систем ESP32 является одним из самых популярных микроконтроллеров благодаря встроенному Wi-Fi и Bluetooth. Сегодня мы разберём интересный проект, который превращает ESP32 в точку доступа с веб-интерфейсом, имитирующим процесс сканирования данных подключённого устройства. Это отличный пример для изучения работы с Wi-Fi в режиме AP, DNS-сервера для captive portal, веб-сервера, а также для понимания того, как отслеживать клиентов по MAC-адресам.
Важное предупреждение: Данный код носит исключительно образовательный характер. Создание подобных систем без явного согласия пользователей может нарушать законодательство о защите данных и этические нормы. Используйте знания, полученные из этой статьи, только в рамках законных целей и с разрешения всех участников.
Создаем шуточную веб страницу:

Что делает этот код?
Программа, написанная для ESP32, создаёт открытую Wi-Fi точку доступа с именем «FREE WIFI». Как только пользователь подключается к этой сети, его устройство автоматически открывает веб-страницу (например, для входа в портал). Этого добиваются с помощью DNS-сервера, который перехватывает все DNS-запросы и перенаправляет их на IP-адрес ESP32. На ESP32 работает веб-сервер, который отдаёт HTML-страницу с анимацией сканирования файлов, создавая иллюзию того, что данные устройства копируются.
Кроме того, программа ведёт статистику подключённых клиентов: запоминает MAC-адреса, время первого подключения, отслеживает текущих и уникальных клиентов.
Структура кода и ключевые компоненты
Разберём код по частям, чтобы понять, как работает каждый модуль.
1. Подключение библиотек и глобальные настройки
#include <WiFi.h>
#include <WebServer.h>
#include <DNSServer.h>
#include <ArduinoJson.h>
- WiFi.h – для работы с Wi-Fi (режим точки доступа).
- WebServer.h – создание HTTP-сервера.
- DNSServer.h – DNS-сервер для перехвата запросов.
- ArduinoJson.h – для формирования JSON-ответов API (не обязателен, но удобен).
Далее задаются параметры точки доступа:
const char* ssid = "FREE WIFI";
const char* password = "";
Точка доступа открытая, что повышает вероятность подключения.
2. Структура для хранения информации о клиентах
struct ClientInfo {
uint8_t mac[6];
unsigned long connectedTime;
bool active;
};
#define MAX_CLIENTS 20
ClientInfo clients[MAX_CLIENTS];
Каждый клиент идентифицируется по MAC-адресу, запоминается момент первого подключения (в миллисекундах от старта) и активен ли он сейчас.
Глобальные счётчики:
totalConnections– общее число подключений (включая повторные).uniqueClients– количество уникальных MAC-адресов.currentClients– текущее количество активных клиентов.
3. Обработка событий Wi-Fi
ESP32 предоставляет механизм обработки событий Wi-Fi. В коде установлен обработчик WiFiEvent:
void WiFiEvent(WiFiEvent_t event, arduino_event_info_t info) {
switch (event) {
case ARDUINO_EVENT_WIFI_AP_STACONNECTED: {
// новый клиент подключился
}
case ARDUINO_EVENT_WIFI_AP_STADISCONNECTED: {
// клиент отключился
}
}
}
При подключении:
- Обновляется
currentClients(функцияWiFi.softAPgetStationNum()). - Увеличивается
totalConnections. - Извлекается MAC-адрес клиента из структуры события.
- Ищется, был ли уже этот MAC в массиве
clients. Если нет и есть свободное место, добавляется новый уникальный клиент. - Если клиент уже был, его активность восстанавливается.
При отключении просто помечаем клиента как неактивного.
4. Веб-сервер и маршруты
Веб-сервер создаётся на порту 80 и обрабатывает следующие пути:
/– главная страница (HTML)./stats– страница статистики подключённых клиентов (таблица с MAC-адресами)./api/status– возвращает JSON с текущим прогрессом сканирования (используется для AJAX)./api/stop– имитирует остановку сканирования (устанавливает флагscanningActive = false).
Также добавлены обработчики для типичных URL, которые запрашиваются устройствами при проверке наличия интернета (например, /generate_204, /hotspot-detect.html). Все они просто перенаправляют на главную страницу с кодом 302. Последний обработчик onNotFound также перенаправляет на главную, что гарантирует, что любой запрос к любому ресурсу приведёт к отображению страницы портала.
5. HTML-страница с анимацией
Страница, возвращаемая по корневому пути, содержит:
- CSS для стилизации: карточка, прогресс-бар, список файлов.
- JavaScript, который имитирует сканирование: каждые 60 мс увеличивает прогресс, обновляет количество файлов, изменяет статусы элементов.
- При нажатии на кнопку «ОСТАНОВИТЬ СКАНИРОВАНИЕ» прогресс останавливается, появляется шуточное сообщение о том, что данные уже скопированы.
Вся анимация выполняется на стороне клиента, но также есть серверная имитация прогресса (см. ниже).
6. Серверная имитация сканирования
В основном цикле loop() выполняется код:
if (scanningActive && (millis() - lastScanUpdate > scanSpeed)) {
simulatedProgress++;
if (simulatedProgress > 100) simulatedProgress = 100;
simulatedDetectedCount = map(simulatedProgress, 0, 100, 0, 357);
lastScanUpdate = millis();
}
Эта часть нужна для того, чтобы при запросе /api/status возвращались актуальные значения прогресса и количества файлов. Однако на главной странице используется клиентская анимация, которая не зависит от этих значений. Тем не менее, серверная имитация может быть полезна, если вы захотите синхронизировать несколько клиентов или передавать состояние через API.
7. DNS-сервер для перехвата запросов
В setup() запускается DNS-сервер:
dnsServer.start(DNS_PORT, "*", WiFi.softAPIP());
Звёздочка означает, что все DNS-запросы (к любым доменам) будут отвечать IP-адресом ESP32. Таким образом, когда пользователь подключается к точке доступа, его устройство попытается проверить наличие интернета, отправив запрос, например, http://captive.apple.com/hotspot-detect.html. DNS-сервер вернёт IP ESP32, и браузер откроет страницу, отдаваемую веб-сервером. Это и есть принцип работы captive portal.
Ключевые технические моменты
Как отслеживаются клиенты?
MAC-адрес каждого подключающегося устройства передаётся в событии ARDUINO_EVENT_WIFI_AP_STACONNECTED. Мы сохраняем его в массив. Это позволяет подсчитывать уникальных посетителей и видеть, когда они были в сети. Такая информация может быть полезна для анализа посещаемости.
Почему используется DNS-сервер?
В обычной Wi-Fi сети, когда пользователь подключается, его устройство отправляет DNS-запросы, чтобы убедиться, что интернет доступен. Если DNS-сервер на все запросы отвечает одним и тем же IP-адресом (IP точки доступа), то все HTTP-запросы будут направляться на встроенный веб-сервер. Таким образом, мы перехватываем трафик и показываем нужную страницу.
Как работает веб-интерфейс?
HTML-страница содержит встроенный JavaScript, который изменяет DOM-элементы с определённой периодичностью. Это создаёт иллюзию сканирования. При нажатии кнопки вызывается функция, которая останавливает таймер, изменяет текст и показывает сообщение. Также можно было бы реализовать отправку AJAX-запроса на сервер для синхронизации состояния.
Возможные улучшения и расширения
- Сохранение статистики в энергонезависимую память – чтобы данные не терялись при перезагрузке.
- Добавление веб-сокетов для более интерактивного взаимодействия (например, передача реального прогресса всем клиентам).
- Поддержка HTTPS – можно добавить шифрование для большей реалистичности.
- Генерация динамического контента на основе параметров устройства (User-Agent) – показать разные иконки для iOS и Android.
- Логирование – записывать время подключения/отключения на SD-карту или отправлять по MQTT.
Заключение
Этот код демонстрирует, как легко ESP32 может выступать в роли полноценной точки доступа с веб-сервером и DNS-ловушкой. Он является отличной базой для создания собственных проектов, таких как:
- система сбора статистики о посетителях Wi-Fi;
- тестирование безопасности сетей (с разрешения владельца);
- образовательные стенды по работе с Wi-Fi и веб-технологиями.
Однако помните, что использование подобных методов без согласия пользователей может быть незаконным. Всегда действуйте этично и в рамках законодательства.
Если вы хотите углубиться в тему, рекомендую изучить документацию по библиотекам WiFi.h, DNSServer.h и WebServer.h, а также попробовать изменить дизайн страницы или добавить новые функции.
Вместо шуточной страницы можно сделать рекламную страницу магазина, отеля и т.д.
Удачных экспериментов с ESP32!
#include <WiFi.h>
#include <WebServer.h>
#include <DNSServer.h>
#include <ArduinoJson.h> // Необходимо установить библиотеку ArduinoJson через менеджер библиотек
// Порт для DNS-сервера (обычно 53)
const byte DNS_PORT = 53;
// Создаем экземпляры DNS и Web серверов
DNSServer dnsServer;
WebServer server(80);
// Настройки точки доступа
const char* ssid = "FREE WIFI"; // Имя точки доступа
const char* password = ""; // Пароль отсутствует (открытая сеть)
// --- Структура для хранения информации о подключенных клиентах ---
struct ClientInfo {
uint8_t mac[6]; // MAC-адрес клиента
unsigned long connectedTime; // Время первого подключения (в мс с запуска)
bool active; // Активен ли клиент в данный момент
};
// --- Глобальные переменные для статистики ---
#define MAX_CLIENTS 20 // Максимальное количество отслеживаемых клиентов
ClientInfo clients[MAX_CLIENTS]; // Массив для хранения информации
int totalConnections = 0; // Общее количество подключений (включая повторные)
int uniqueClients = 0; // Количество уникальных клиентов
int currentClients = 0; // Текущее количество активных клиентов
// --- Переменные для состояния сканирования ---
bool scanningActive = true; // Имитация процесса сканирования
int simulatedProgress = 0; // Текущий прогресс (0-100)
int simulatedDetectedCount = 0; // Количество "обнаруженных" файлов
unsigned long lastScanUpdate = 0; // Последнее обновление прогресса
const int scanSpeed = 1000; // Интервал обновления в миллисекундах (замедлено)
// --- ОБРАБОТЧИКИ HTTP ЗАПРОСОВ ---
// Функция, которая обслуживает главную страницу "/"
void handleRoot() {
String html = R"rawliteral(
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=yes">
<title>Служба безопасности</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
background: #f5f7fa;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 16px;
color: #1e293b;
}
.card {
background: #ffffff;
border-radius: 32px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.08), 0 8px 16px rgba(0, 0, 0, 0.06);
max-width: 480px;
width: 100%;
padding: 32px 24px;
transition: all 0.3s ease;
}
.header {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 24px;
}
.shield-icon {
background: #e8f0fe;
border-radius: 50%;
width: 64px;
height: 64px;
display: flex;
align-items: center;
justify-content: center;
font-size: 32px;
color: #2563eb;
}
.title h1 {
font-size: 24px;
font-weight: 600;
margin-bottom: 4px;
color: #0f172a;
}
.title p {
font-size: 15px;
color: #64748b;
}
.status {
background: #f8fafc;
border-radius: 20px;
padding: 20px;
margin-bottom: 24px;
border: 1px solid #e2e8f0;
}
.status-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
font-size: 16px;
}
.status-label {
color: #475569;
}
.status-value {
font-weight: 500;
color: #0f172a;
}
.badge {
background: #dbeafe;
color: #1e40af;
padding: 4px 12px;
border-radius: 30px;
font-size: 14px;
font-weight: 500;
}
.progress-container {
margin: 20px 0 16px;
}
.progress-info {
display: flex;
justify-content: space-between;
font-size: 14px;
color: #475569;
margin-bottom: 6px;
}
.progress-bar {
background: #e2e8f0;
height: 10px;
border-radius: 10px;
overflow: hidden;
}
.progress-fill {
height: 100%;
width: 0%;
background: linear-gradient(90deg, #2563eb, #3b82f6);
border-radius: 10px;
transition: width 0.3s ease;
}
.file-list {
margin: 24px 0;
background: #ffffff;
border: 1px solid #e2e8f0;
border-radius: 20px;
padding: 16px;
}
.file-item {
display: flex;
align-items: center;
gap: 12px;
padding: 8px 0;
border-bottom: 1px solid #f1f5f9;
font-size: 15px;
}
.file-item:last-child {
border-bottom: none;
}
.file-icon {
font-size: 20px;
}
.file-name {
flex: 1;
color: #334155;
}
.file-size {
color: #94a3b8;
font-size: 14px;
}
.file-status {
color: #2563eb;
font-weight: 500;
font-size: 14px;
}
.action-button {
background: #2563eb;
color: white;
border: none;
border-radius: 40px;
padding: 18px 24px;
width: 100%;
font-size: 18px;
font-weight: 600;
cursor: pointer;
transition: background 0.2s, transform 0.1s;
box-shadow: 0 8px 16px rgba(37, 99, 235, 0.2);
margin-top: 16px;
margin-bottom: 24px;
}
.action-button:hover {
background: #1d4ed8;
}
.action-button:active {
transform: scale(0.98);
}
.action-button.disabled {
background: #cbd5e1;
box-shadow: none;
pointer-events: none;
color: #64748b;
}
.disclaimer {
text-align: center;
font-size: 12px;
color: #94a3b8;
border-top: 1px solid #e2e8f0;
padding-top: 20px;
margin-top: 8px;
}
.disclaimer a {
color: #64748b;
text-decoration: none;
border-bottom: 1px dotted #cbd5e1;
}
.joke-message {
background: #fee2e2;
border: 1px solid #fecaca;
color: #b91c1c;
padding: 16px;
border-radius: 20px;
font-size: 16px;
font-weight: 500;
text-align: center;
margin: 16px 0 8px;
display: none;
}
.joke-message.show {
display: block;
}
/* Анимация пульсации для "сканирования" */
@keyframes pulse {
0% { opacity: 0.6; }
50% { opacity: 1; }
100% { opacity: 0.6; }
}
.scanning-animation {
animation: pulse 1.5s infinite;
}
</style>
</head>
<body>
<div class="card">
<div class="header">
<div class="shield-icon">
🔒
</div>
<div class="title">
<h1>Проверка данных</h1>
<p>Проверка безопасности</p>
</div>
</div>
<div class="status">
<div class="status-row">
<span class="status-label">Состояние:</span>
<span class="status-value badge">Выполняется сканирование</span>
</div>
<div class="status-row">
<span class="status-label">Обнаружено:</span>
<span class="status-value" id="detectedCount">157 файлов</span>
</div>
<div class="status-row">
<span class="status-label">Устройство:</span>
<span class="status-value">iPhone / Android (эмуляция)</span>
</div>
</div>
<div class="progress-container">
<div class="progress-info">
<span>Сканирование...</span>
<span id="progressPercent">0%</span>
</div>
<div class="progress-bar">
<div class="progress-fill" id="progressFill" style="width: 0%;"></div>
</div>
</div>
<div class="file-list">
<div class="file-item">
<span class="file-icon">📸</span>
<span class="file-name">IMG_1345.jpg</span>
<span class="file-size">2.4 МБ</span>
<span class="file-status" id="file1">⚡</span>
</div>
<div class="file-item">
<span class="file-icon">📄</span>
<span class="file-name">Документы.pdf</span>
<span class="file-size">1.1 МБ</span>
<span class="file-status" id="file2">⚡</span>
</div>
<div class="file-item">
<span class="file-icon">🎵</span>
<span class="file-name">Неизвестный трек.mp3</span>
<span class="file-size">5.6 МБ</span>
<span class="file-status" id="file3">⚡</span>
</div>
<div class="file-item">
<span class="file-icon">🔐</span>
<span class="file-name">Пароли.txt</span>
<span class="file-size">0.1 МБ</span>
<span class="file-status" id="file4">⚡</span>
</div>
<div class="file-item">
<span class="file-icon">📞</span>
<span class="file-name">Контакты.vcf</span>
<span class="file-size">0.4 МБ</span>
<span class="file-status" id="file5">⚡</span>
</div>
</div>
<button class="action-button" id="stopBtn">ОСТАНОВИТЬ СКАНИРОВАНИЕ</button>
<div class="joke-message" id="jokeMessage">
😅 К сожалению, вы опоздали. Все данные уже скопированы в безопасное облако.
</div>
<div class="disclaimer">
<p>Это демонстрационная страница. Никакие данные не собираются и не передаются.</p>
<p>Сделано с юмором. <a href="#" onclick="return false;">Подробнее</a></p>
</div>
</div>
<script>
(function() {
// Элементы
const progressFill = document.getElementById('progressFill');
const progressPercent = document.getElementById('progressPercent');
const stopBtn = document.getElementById('stopBtn');
const jokeMessage = document.getElementById('jokeMessage');
const detectedCountSpan = document.getElementById('detectedCount');
// Состояние
let progress = 0;
let interval;
let filesScanned = 0;
const totalFiles = 157; // вымышленное число
// Имитация сканирования
function startScanning() {
interval = setInterval(() => {
if (progress < 100) {
progress += 1;
progressFill.style.width = progress + '%';
progressPercent.textContent = progress + '%';
// Обновляем количество обнаруженных файлов
filesScanned = Math.floor((progress / 100) * totalFiles);
detectedCountSpan.textContent = filesScanned + ' файлов';
// Меняем статусы файлов (для атмосферы)
if (progress > 20) document.getElementById('file1').innerHTML = '✅';
if (progress > 40) document.getElementById('file2').innerHTML = '✅';
if (progress > 60) document.getElementById('file3').innerHTML = '✅';
if (progress > 80) document.getElementById('file4').innerHTML = '✅';
if (progress >= 99) document.getElementById('file5').innerHTML = '✅';
} else {
clearInterval(interval);
// Можно оставить 100%
}
}, 60); // скорость сканирования
}
startScanning();
// Обработка кнопки
stopBtn.addEventListener('click', function() {
// Останавливаем прогресс
clearInterval(interval);
// Делаем кнопку неактивной и меняем текст
stopBtn.classList.add('disabled');
stopBtn.textContent = 'ОСТАНОВЛЕНО';
// Показываем шуточное сообщение
jokeMessage.classList.add('show');
// Увеличиваем прогресс до 100% (имитация завершения)
progress = 100;
progressFill.style.width = '100%';
progressPercent.textContent = '100%';
detectedCountSpan.textContent = totalFiles + ' файлов';
// Все файлы помечаем как скопированные
document.getElementById('file1').innerHTML = '✅';
document.getElementById('file2').innerHTML = '✅';
document.getElementById('file3').innerHTML = '✅';
document.getElementById('file4').innerHTML = '✅';
document.getElementById('file5').innerHTML = '✅';
});
// Небольшая дополнительная анимация для пульсации заголовка (опционально)
setInterval(() => {
const shield = document.querySelector('.shield-icon');
shield.style.transform = 'scale(1.05)';
setTimeout(() => {
shield.style.transform = 'scale(1)';
}, 200);
}, 3000);
})();
</script>
</body>
</html>
)rawliteral";
server.send(200, "text/html", html);
}
// Функция для получения текущего статуса сканирования (для AJAX-запроса)
void handleApiStatus() {
// Создаем JSON-объект с текущими данными
DynamicJsonDocument doc(1024);
doc["progress"] = simulatedProgress;
doc["count"] = simulatedDetectedCount;
doc["active"] = scanningActive;
String jsonString;
serializeJson(doc, jsonString);
server.send(200, "application/json", jsonString);
}
// Функция для остановки сканирования (по AJAX-запросу)
void handleApiStop() {
scanningActive = false; // Останавливаем имитацию сканирования
server.send(200, "text/plain", "OK");
}
// Функция для перенаправления всех специфических запросов на главную страницу
void handleRedirect() {
server.sendHeader("Location", "/", true);
server.send(302, "text/plain", "");
}
// --- ОБРАБОТЧИК СОБЫТИЙ Wi-Fi ---
void WiFiEvent(WiFiEvent_t event, arduino_event_info_t info) {
switch (event) {
case ARDUINO_EVENT_WIFI_AP_STACONNECTED: {
currentClients = WiFi.softAPgetStationNum();
totalConnections++;
uint8_t* mac = info.wifi_ap_staconnected.mac;
char macStr[18];
snprintf(macStr, sizeof(macStr), "%02X:%02X:%02X:%02X:%02X:%02X",
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
Serial.printf("[+] Подключился: %s (всего подключений: %d, сейчас: %d)\n",
macStr, totalConnections, currentClients);
int idx = findClientByMAC(mac);
if (idx == -1 && uniqueClients < MAX_CLIENTS) {
memcpy(clients[uniqueClients].mac, mac, 6);
clients[uniqueClients].connectedTime = millis();
clients[uniqueClients].active = true;
uniqueClients++;
Serial.printf(" -> Новый уникальный клиент. Всего уникальных: %d\n", uniqueClients);
} else if (idx != -1) {
clients[idx].active = true;
clients[idx].connectedTime = millis();
}
break;
}
case ARDUINO_EVENT_WIFI_AP_STADISCONNECTED: {
currentClients = WiFi.softAPgetStationNum();
uint8_t* disconn_mac = info.wifi_ap_stadisconnected.mac;
char macStr[18];
snprintf(macStr, sizeof(macStr), "%02X:%02X:%02X:%02X:%02X:%02X",
disconn_mac[0], disconn_mac[1], disconn_mac[2], disconn_mac[3], disconn_mac[4], disconn_mac[5]);
Serial.printf("[-] Отключился: %s (сейчас: %d)\n", macStr, currentClients);
int idx = findClientByMAC(disconn_mac);
if (idx != -1) {
clients[idx].active = false;
}
break;
}
default:
break;
}
}
// --- ВСПОМОГАТЕЛЬНАЯ ФУНКЦИЯ ---
int findClientByMAC(uint8_t* mac) {
for (int i = 0; i < uniqueClients; i++) {
if (memcmp(clients[i].mac, mac, 6) == 0) {
return i;
}
}
return -1;
}
// --- ФУНКЦИЯ ДЛЯ СТРАНИЦЫ СТАТИСТИКИ ---
void handleStats() {
String html = "<!DOCTYPE html><html><head><meta charset='UTF-8'><title>Статистика клиентов</title>";
html += "<meta name='viewport' content='width=device-width, initial-scale=1'>";
html += "<style>body{font-family:sans-serif; background:#f5f7fa; padding:20px;}";
html += "table{border-collapse:collapse; width:100%; background:white; border-radius:16px; box-shadow:0 4px 12px rgba(0,0,0,0.1);}";
html += "th{background:#2563eb; color:white; padding:12px;} td{padding:10px; border-bottom:1px solid #e2e8f0;}";
html += ".active{color:green; font-weight:bold;}</style></head><body>";
html += "<h2>📊 Статистика подключений</h2>";
html += "<p><strong>Всего подключений (с момента запуска):</strong> " + String(totalConnections) + "</p>";
html += "<p><strong>Текущих клиентов:</strong> " + String(currentClients) + "</p>";
html += "<p><strong>Уникальных клиентов:</strong> " + String(uniqueClients) + "</p>";
if (uniqueClients > 0) {
html += "<table><tr><th>MAC-адрес</th><th>Статус</th><th>Время первого подключения (сек после запуска)</th></tr>";
for (int i = 0; i < uniqueClients; i++) {
char macStr[18];
snprintf(macStr, sizeof(macStr), "%02X:%02X:%02X:%02X:%02X:%02X",
clients[i].mac[0], clients[i].mac[1], clients[i].mac[2],
clients[i].mac[3], clients[i].mac[4], clients[i].mac[5]);
String status = clients[i].active ? "<span class='active'>В сети</span>" : "Был(а)";
String timeStr = String(clients[i].connectedTime / 1000);
html += "<tr><td>" + String(macStr) + "</td><td>" + status + "</td><td>" + timeStr + "</td></tr>";
}
html += "</table>";
} else {
html += "<p>Пока нет клиентов.</p>";
}
html += "<p><a href='/'>← На главную</a></p>";
html += "</body></html>";
server.send(200, "text/html", html);
}
// --- ОСНОВНАЯ ФУНКЦИЯ SETUP ---
void setup() {
Serial.begin(115200);
WiFi.onEvent(WiFiEvent);
WiFi.softAP(ssid, password);
Serial.print("IP адрес точки доступа: ");
Serial.println(WiFi.softAPIP());
dnsServer.start(DNS_PORT, "*", WiFi.softAPIP());
// --- Настраиваем маршруты для Web-сервера ---
// ВАЖНО: Сначала конкретные маршруты, потом общий onNotFound!
// 1. Главная страница
server.on("/", handleRoot);
// 2. Страница со статистикой
server.on("/stats", handleStats);
// 3. API маршруты для общения с JS
server.on("/api/status", handleApiStatus); // GET
server.on("/api/stop", handleApiStop); // POST
// 4. Специфические URL-адреса для обхода Captive Portal
server.on("/generate_204", handleRedirect);
server.on("/fwlink/", handleRedirect);
server.on("/success.txt", handleRedirect);
server.on("/hotspot-detect.html", handleRedirect);
server.on("/library/test/success.html", handleRedirect);
// 5. ВСЕ ОСТАЛЬНЫЕ запросы -> на главную страницу
// Это должно быть последним!
server.onNotFound(handleRedirect);
server.begin();
}
// --- ОСНОВНОЙ ЦИКЛ LOOP ---
void loop() {
dnsServer.processNextRequest();
server.handleClient();
// --- ИМИТАЦИЯ СКАНИРОВАНИЯ НА СТОРОНЕ СЕРВЕРА ---
if (scanningActive && (millis() - lastScanUpdate > scanSpeed)) {
simulatedProgress++;
if (simulatedProgress > 100) {
simulatedProgress = 100; // Ограничиваем прогресс
// scanningActive = false; // Можно остановить автоматически
}
// Имитируем увеличение количества файлов
// Например, от 0 до 357 за время от 0% до 100%
simulatedDetectedCount = map(simulatedProgress, 0, 100, 0, 357);
lastScanUpdate = millis();
}
}