Skip to main content

🌐 ESP32 КАК СОБСТВЕННЫЙ ВЕБ-СЕРВЕР

От локального интеллекта к глобальной доступности


🎯 МЕТОДОЛОГИЧЕСКАЯ КОНЦЕПЦИЯ СПРИНТА

Философия перехода:

БЫЛО: Умная система работает локально (только для тех, кто рядом)
СТАЛО: Умная система доступна всему миру через интернет

Ключевая идея: Дети превращают свой ESP32 в “цифрового посла” своей умной системы, который может общаться с любым человеком на планете.

Концептуальная революция:

  • Локальное → Глобальное: Система становится частью всемирной сети
  • Устройство → Сервис: ESP32 не просто “железка”, а сервер
  • Код → Веб-приложение: Программа становится сайтом

🧠 ПЕДАГОГИЧЕСКИЕ ЦЕЛИ СПРИНТА

Концептуальные цели:

  • Понимание “клиент-серверной архитектуры” как основы интернета
  • Осознание “протоколов общения” между устройствами
  • Введение понятия “API” как языка машин
  • Понимание “распределенных систем” - когда мозг и тело в разных местах

Технические цели:

  • HTTP протокол и веб-сервер на ESP32
  • HTML/CSS как язык интерфейсов
  • RESTful API для взаимодействия с датчиками
  • WebSocket для real-time коммуникации

Метакогнитивные цели:

  • “Архитектурное мышление” - как построить масштабируемую систему
  • “Интерфейсное мышление” - как люди и машины общаются
  • “Сетевое мышление” - понимание распределенности

📚 СТРУКТУРА СПРИНТА (4 занятия)

Занятие 1: “Рождение веб-сервера” 🌍

Длительность: 90 минут

Фаза 1: Философское введение (20 мин)

Метод: Историческо-философский анализ

Ключевая история: “Эволюция общения”

Пещерные люди → Рисунки на стенах → Письма → Телефон → Интернет → IoT

Сократический диалог:

  • “Как бы вы рассказали другу в Японии о температуре в нашем классе?”
  • “Что если ваша умная система могла бы ‘отвечать на вопросы’ людей из других стран?”
  • “Чем веб-сайт отличается от программы на компьютере?”

Практическое упражнение: “Человеческий веб-сервер”

  • Один ученик = “сервер” (знает информацию о классе)
  • Другие = “клиенты” (задают вопросы)
  • Формат: “GET /temperature” → “22.5°C”
  • Открытие: “Веб-сервер = программа, которая отвечает на вопросы по сети”

Фаза 2: Анатомия веб-сервера (25 мин)

Метод: Анатомический анализ

Биологическая аналогия: ESP32 как “цифровая нервная система”

Мозг (Процессор) → обрабатывает запросы
Органы чувств (Датчики) → собирают данные  
Рот (WiFi передатчик) → отвечает на вопросы
Память → помнит настройки и данные

Практическая демонстрация:

// Минимальный веб-сервер - "Привет, мир!"
#include <WiFi.h>
#include <WebServer.h>

WebServer server(80);

void handleRoot() {
    server.send(200, "text/html", 
        "<h1>🤖 Привет! Я - умная система класса!</h1>"
        "<p>Спроси меня о температуре: /temperature</p>");
}

void setup() {
    WiFi.begin("School_WiFi", "password");
    server.on("/", handleRoot);
    server.begin();
    
    Serial.println("🌐 Веб-сервер запущен!");
    Serial.println("Адрес: http://" + WiFi.localIP().toString());
}

void loop() {
    server.handleClient(); // Отвечаем на запросы
}

Педагогический момент:

  • Дети видят, как их ESP32 превращается в “мини-сайт”
  • Могут зайти на него с телефона/планшета
  • Волшебный момент: “Наше устройство стало частью интернета!”

Фаза 3: Протоколы как язык машин (30 мин)

Концепция: “HTTP как универсальный язык”

Лингвистическая аналогия:

Люди говорят на разных языках, но есть "lingua franca"
Устройства работают по-разному, но говорят на HTTP

Практическое изучение HTTP:

Клиент говорит: "GET /temperature HTTP/1.1"
Сервер отвечает: "HTTP/1.1 200 OK\nContent-Type: text/plain\n\n22.5"

Интерактивное упражнение: “Переводчики HTTP”

  • Дети переводят человеческие запросы в HTTP:
    • “Какая температура?” → GET /temperature
    • “Включи свет” → POST /light {"state": "on"}
    • “Покажи все данные” → GET /sensors

Фаза 4: Планирование веб-интерфейса (15 мин)

Метод: User Experience Design

Вопросы для дизайна:

  • “Кто будет пользоваться нашим сайтом?” (учителя, ученики, родители)
  • “Что им нужно узнать?” (температура, освещение, безопасность)
  • “Как сделать это красиво и понятно?”

Скетчинг интерфейса: Дети рисуют, как должен выглядеть их “сайт умной системы”


Занятие 2: “Создание веб-интерфейса” 🎨

Длительность: 90 минут

Фаза 1: HTML как структура мыслей (25 мин)

Концепция: “HTML = скелет веб-страницы”

Анатомическая аналогия:

HTML = скелет (структура)
CSS = кожа и внешность (стиль)  
JavaScript = мышцы (движение и интерактивность)

Практика: “Умная панель управления”

<!DOCTYPE html>
<html>
<head>
    <title>🏫 Умный класс 5Б</title>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <style>
        body { 
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            margin: 0;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
        }
        
        .container {
            max-width: 800px;
            margin: 0 auto;
            padding: 20px;
        }
        
        .header {
            text-align: center;
            margin-bottom: 30px;
        }
        
        .sensor-grid {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
            gap: 20px;
            margin-bottom: 30px;
        }
        
        .sensor-card {
            background: rgba(255,255,255,0.1);
            backdrop-filter: blur(10px);
            border-radius: 15px;
            padding: 20px;
            border: 1px solid rgba(255,255,255,0.2);
            transition: transform 0.3s ease;
        }
        
        .sensor-card:hover {
            transform: translateY(-5px);
        }
        
        .sensor-icon {
            font-size: 3rem;
            margin-bottom: 10px;
        }
        
        .sensor-value {
            font-size: 2rem;
            font-weight: bold;
            margin: 10px 0;
        }
        
        .sensor-status {
            font-size: 0.9rem;
            opacity: 0.8;
        }
        
        .controls {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
            gap: 15px;
        }
        
        .control-button {
            background: rgba(255,255,255,0.2);
            border: none;
            border-radius: 10px;
            padding: 15px;
            color: white;
            font-size: 1rem;
            cursor: pointer;
            transition: all 0.3s ease;
        }
        
        .control-button:hover {
            background: rgba(255,255,255,0.3);
            transform: scale(1.05);
        }
        
        .status-bar {
            margin-top: 20px;
            padding: 15px;
            background: rgba(0,0,0,0.2);
            border-radius: 10px;
            text-align: center;
        }
    </style>
</head>
<body>
    <div class="container">
        <!-- Заголовок -->
        <div class="header">
            <h1>🤖 ALEX - Умная система класса</h1>
            <p>Статус: <span id="systemStatus">🟢 Активна и заботится о вас</span></p>
        </div>
        
        <!-- Датчики -->
        <div class="sensor-grid">
            <div class="sensor-card">
                <div class="sensor-icon">🌡️</div>
                <div class="sensor-name">Температура</div>
                <div class="sensor-value" id="temperature">--°C</div>
                <div class="sensor-status" id="tempStatus">Загрузка...</div>
            </div>
            
            <div class="sensor-card">
                <div class="sensor-icon">💡</div>
                <div class="sensor-name">Освещенность</div>
                <div class="sensor-value" id="light">--%</div>
                <div class="sensor-status" id="lightStatus">Загрузка...</div>
            </div>
            
            <div class="sensor-card">
                <div class="sensor-icon">🔊</div>
                <div class="sensor-name">Уровень шума</div>
                <div class="sensor-value" id="sound">--дБ</div>
                <div class="sensor-status" id="soundStatus">Загрузка...</div>
            </div>
            
            <div class="sensor-card">
                <div class="sensor-icon">🚶</div>
                <div class="sensor-name">Движение</div>
                <div class="sensor-value" id="motion">--</div>
                <div class="sensor-status" id="motionStatus">Загрузка...</div>
            </div>
        </div>
        
        <!-- Управление -->
        <div class="controls">
            <button class="control-button" onclick="toggleHeater()">
                🔥 Обогреватель
            </button>
            <button class="control-button" onclick="toggleLight()">
                💡 Освещение
            </button>
            <button class="control-button" onclick="setComfortMode()">
                😌 Комфортный режим
            </button>
            <button class="control-button" onclick="setEcoMode()">
                🌱 Эко-режим
            </button>
        </div>
        
        <!-- Статус системы -->
        <div class="status-bar">
            <strong>Последнее решение:</strong> 
            <span id="lastDecision">Система запускается...</span>
        </div>
    </div>

    <script>
        // JavaScript для интерактивности
        let sensorData = {};
        
        // Обновление данных каждые 2 секунды
        setInterval(updateSensorData, 2000);
        
        async function updateSensorData() {
            try {
                // Запрашиваем данные с ESP32
                const response = await fetch('/api/sensors');
                sensorData = await response.json();
                
                // Обновляем интерфейс
                document.getElementById('temperature').textContent = sensorData.temperature + '°C';
                document.getElementById('light').textContent = sensorData.light + '%';
                document.getElementById('sound').textContent = sensorData.sound + 'дБ';
                document.getElementById('motion').textContent = sensorData.motion ? 'Есть' : 'Нет';
                
                // Обновляем статусы
                updateSensorStatuses();
                
            } catch (error) {
                console.error('Ошибка получения данных:', error);
                document.getElementById('systemStatus').textContent = '🔴 Нет связи с системой';
            }
        }
        
        function updateSensorStatuses() {
            // Умные статусы на основе данных
            const temp = sensorData.temperature;
            if (temp < 18) {
                document.getElementById('tempStatus').textContent = '🥶 Холодно, включаю обогрев';
            } else if (temp > 25) {
                document.getElementById('tempStatus').textContent = '🔥 Жарко, нужно охлаждение';
            } else {
                document.getElementById('tempStatus').textContent = '😌 Комфортная температура';
            }
            
            const light = sensorData.light;
            if (light < 30) {
                document.getElementById('lightStatus').textContent = '🌙 Темно, включаю свет';
            } else if (light > 80) {
                document.getElementById('lightStatus').textContent = '☀️ Очень светло';
            } else {
                document.getElementById('lightStatus').textContent = '👍 Хорошее освещение';
            }
            
            const motion = sensorData.motion;
            document.getElementById('motionStatus').textContent = 
                motion ? '👥 Люди в классе' : '🏫 Класс пустой';
        }
        
        async function toggleHeater() {
            try {
                const response = await fetch('/api/heater', { method: 'POST' });
                const result = await response.json();
                alert('🔥 ' + result.message);
            } catch (error) {
                alert('❌ Ошибка управления обогревателем');
            }
        }
        
        async function toggleLight() {
            try {
                const response = await fetch('/api/light', { method: 'POST' });
                const result = await response.json();
                alert('💡 ' + result.message);
            } catch (error) {
                alert('❌ Ошибка управления освещением');
            }
        }
        
        async function setComfortMode() {
            try {
                const response = await fetch('/api/mode/comfort', { method: 'POST' });
                const result = await response.json();
                alert('😌 Включен комфортный режим');
                document.getElementById('lastDecision').textContent = result.decision;
            } catch (error) {
                alert('❌ Ошибка переключения режима');
            }
        }
        
        async function setEcoMode() {
            try {
                const response = await fetch('/api/mode/eco', { method: 'POST' });
                const result = await response.json();
                alert('🌱 Включен эко-режим');
                document.getElementById('lastDecision').textContent = result.decision;
            } catch (error) {
                alert('❌ Ошибка переключения режима');
            }
        }
        
        // Первоначальная загрузка данных
        updateSensorData();
    </script>
</body>
</html>

Педагогический момент:

  • Дети видят, как HTML создает структуру
  • CSS делает красиво и современно
  • JavaScript добавляет “живость” и интерактивность

Фаза 2: Серверная логика ESP32 (40 мин)

Концепция: “API как мозг веб-сервера”

Полный код ESP32 с веб-сервером:

#include <WiFi.h>
#include <WebServer.h>
#include <ArduinoJson.h>
#include <DHT.h>
#include <SPIFFS.h>

// === КОНФИГУРАЦИЯ ===
const char* ssid = "School_WiFi";
const char* password = "school_password";

// Датчики (из предыдущих спринтов)
DHT dht(2, DHT22);
const int lightPin = A0;
const int soundPin = A1; 
const int pirPin = 3;

// Исполнители
const int heaterPin = 4;
const int lampPin = 5;

// Веб-сервер
WebServer server(80);

// Состояние системы
struct SystemState {
    float temperature;
    int light;
    int sound;
    bool motion;
    bool heaterOn;
    bool lampOn;
    String mode;
    String lastDecision;
    unsigned long lastUpdate;
};

SystemState currentState;

void setup() {
    Serial.begin(115200);
    
    // Инициализация датчиков
    dht.begin();
    pinMode(pirPin, INPUT);
    pinMode(heaterPin, OUTPUT);
    pinMode(lampPin, OUTPUT);
    
    // Инициализация файловой системы для HTML
    if (!SPIFFS.begin(true)) {
        Serial.println("❌ Ошибка инициализации SPIFFS");
        return;
    }
    
    // Подключение к WiFi
    connectToWiFi();
    
    // Настройка маршрутов веб-сервера
    setupWebRoutes();
    
    // Запуск сервера
    server.begin();
    
    Serial.println("🌐 Веб-сервер запущен!");
    Serial.println("📱 Адрес: http://" + WiFi.localIP().toString());
    Serial.println("🎉 Система готова к работе!");
    
    // Инициализация состояния
    currentState.mode = "comfort";
    currentState.lastDecision = "Система запущена";
    currentState.heaterOn = false;
    currentState.lampOn = false;
}

void loop() {
    server.handleClient(); // Обрабатываем веб-запросы
    updateSensors();       // Обновляем данные датчиков
    makeAutomaticDecisions(); // Автоматическая логика
    delay(100);
}

void connectToWiFi() {
    Serial.println("🔗 Подключаюсь к WiFi...");
    WiFi.begin(ssid, password);
    
    int attempts = 0;
    while (WiFi.status() != WL_CONNECTED && attempts < 30) {
        delay(1000);
        Serial.print(".");
        attempts++;
    }
    
    if (WiFi.status() == WL_CONNECTED) {
        Serial.println("\n✅ WiFi подключен!");
        Serial.println("🌍 IP адрес: " + WiFi.localIP().toString());
    } else {
        Serial.println("\n❌ Не удалось подключиться к WiFi");
    }
}

void setupWebRoutes() {
    // Главная страница
    server.on("/", HTTP_GET, handleRoot);
    
    // API для получения данных датчиков
    server.on("/api/sensors", HTTP_GET, handleSensorsAPI);
    
    // API для управления обогревателем
    server.on("/api/heater", HTTP_POST, handleHeaterAPI);
    
    // API для управления освещением
    server.on("/api/light", HTTP_POST, handleLightAPI);
    
    // API для переключения режимов
    server.on("/api/mode/comfort", HTTP_POST, handleComfortModeAPI);
    server.on("/api/mode/eco", HTTP_POST, handleEcoModeAPI);
    
    // API для получения статуса системы
    server.on("/api/status", HTTP_GET, handleStatusAPI);
    
    // Обработка несуществующих страниц
    server.onNotFound(handleNotFound);
}

void handleRoot() {
    // Отдаем HTML страницу (код выше)
    String html = getMainPageHTML();
    server.send(200, "text/html", html);
}

void handleSensorsAPI() {
    // Создаем JSON с данными датчиков
    DynamicJsonDocument doc(1024);
    
    doc["temperature"] = currentState.temperature;
    doc["light"] = map(currentState.light, 0, 4095, 0, 100);
    doc["sound"] = map(currentState.sound, 0, 4095, 0, 100);
    doc["motion"] = currentState.motion;
    doc["timestamp"] = millis();
    doc["status"] = "online";
    
    String response;
    serializeJson(doc, response);
    
    server.send(200, "application/json", response);
    
    Serial.println("📤 Отправил данные датчиков: " + response);
}

void handleHeaterAPI() {
    currentState.heaterOn = !currentState.heaterOn;
    digitalWrite(heaterPin, currentState.heaterOn ? HIGH : LOW);
    
    DynamicJsonDocument doc(512);
    doc["success"] = true;
    doc["heaterOn"] = currentState.heaterOn;
    doc["message"] = currentState.heaterOn ? "Обогреватель включен" : "Обогреватель выключен";
    
    String response;
    serializeJson(doc, response);
    
    server.send(200, "application/json", response);
    
    currentState.lastDecision = "Пользователь " + String(currentState.heaterOn ? "включил" : "выключил") + " обогреватель";
    Serial.println("🔥 " + currentState.lastDecision);
}

void handleLightAPI() {
    currentState.lampOn = !currentState.lampOn;
    digitalWrite(lampPin, currentState.lampOn ? HIGH : LOW);
    
    DynamicJsonDocument doc(512);
    doc["success"] = true;
    doc["lampOn"] = currentState.lampOn;
    doc["message"] = currentState.lampOn ? "Освещение включено" : "Освещение выключено";
    
    String response;
    serializeJson(doc, response);
    
    server.send(200, "application/json", response);
    
    currentState.lastDecision = "Пользователь " + String(currentState.lampOn ? "включил" : "выключил") + " освещение";
    Serial.println("💡 " + currentState.lastDecision);
}

void handleComfortModeAPI() {
    currentState.mode = "comfort";
    
    // Автоматическая настройка для комфорта
    if (currentState.temperature < 22) {
        currentState.heaterOn = true;
        digitalWrite(heaterPin, HIGH);
    }
    
    if (currentState.light < 30) {
        currentState.lampOn = true;
        digitalWrite(lampPin, HIGH);
    }
    
    DynamicJsonDocument doc(512);
    doc["success"] = true;
    doc["mode"] = "comfort";
    doc["decision"] = "Настроил комфортные условия: тепло и светло";
    
    String response;
    serializeJson(doc, response);
    
    server.send(200, "application/json", response);
    
    currentState.lastDecision = "Включен комфортный режим";
    Serial.println("😌 " + currentState.lastDecision);
}

void handleEcoModeAPI() {
    currentState.mode = "eco";
    
    // Энергосберегающие настройки
    if (!currentState.motion) {
        currentState.heaterOn = false;
        currentState.lampOn = false;
        digitalWrite(heaterPin, LOW);
        digitalWrite(lampPin, LOW);
    }
    
    DynamicJsonDocument doc(512);
    doc["success"] = true;
    doc["mode"] = "eco";
    doc["decision"] = "Экономлю энергию: выключил ненужные устройства";
    
    String response;
    serializeJson(doc, response);
    
    server.send(200, "application/json", response);
    
    currentState.lastDecision = "Включен эко-режим";
    Serial.println("🌱 " + currentState.lastDecision);
}

void handleStatusAPI() {
    DynamicJsonDocument doc(1024);
    
    doc["mode"] = currentState.mode;
    doc["lastDecision"] = currentState.lastDecision;
    doc["uptime"] = millis();
    doc["heaterOn"] = currentState.heaterOn;
    doc["lampOn"] = currentState.lampOn;
    doc["wifiStrength"] = WiFi.RSSI();
    doc["freeMemory"] = ESP.getFreeHeap();
    
    String response;
    serializeJson(doc, response);
    
    server.send(200, "application/json", response);
}

void handleNotFound() {
    server.send(404, "text/plain", "🤖 Страница не найдена!\nЯ умная система, но не всемогущая 😅");
}

void updateSensors() {
    // Обновляем данные каждую секунду
    static unsigned long lastUpdate = 0;
    if (millis() - lastUpdate > 1000) {
        currentState.temperature = dht.readTemperature();
        currentState.light = analogRead(lightPin);
        currentState.sound = analogRead(soundPin);
        currentState.motion = digitalRead(pirPin);
        currentState.lastUpdate = millis();
        
        lastUpdate = millis();
    }
}

void makeAutomaticDecisions() {
    // Автоматическая логика (из спринта 16)
    static unsigned long lastDecision = 0;
    if (millis() - lastDecision > 5000) { // Принимаем решения каждые 5 секунд
        
        if (currentState.mode == "comfort") {
            // Логика комфортного режима
            if (currentState.temperature < 20 && currentState.motion) {
                if (!currentState.heaterOn) {
                    currentState.heaterOn = true;
                    digitalWrite(heaterPin, HIGH);
                    currentState.lastDecision = "Автоматически включил обогрев - пришли люди";
                }
            }
            
            if (currentState.light < 20 && currentState.motion) {
                if (!currentState.lampOn) {
                    currentState.lampOn = true;
                    digitalWrite(lampPin, HIGH);
                    currentState.lastDecision = "Автоматически включил свет - стало темно";
                }
            }
        }
        
        lastDecision = millis();
    }
}

String getMainPageHTML() {
    // Возвращаем HTML код (тот, что был выше)
    return R"(
    <!DOCTYPE html>
    <html>
    <!-- Здесь весь HTML код -->
    </html>
    )";
}

Фаза 3: Тестирование интерфейса (25 мин)

Метод: User Testing

Практическое тестирование:

  1. Дети подключаются к веб-серверу ESP32 с разных устройств
  2. Тестируют все кнопки и функции
  3. Наблюдают real-time обновление данных
  4. Проверяют отзывчивость интерфейса

Педагогические наблюдения:

  • “Наша система теперь доступна всему миру!”
  • “Мы можем управлять классом из любой точки школы!”
  • “Система работает как настоящий сайт!”

Занятие 3: “API и протоколы взаимодействия” 🔌

Длительность: 90 минут

Фаза 1: API как язык машин (30 мин)

Концепция: “API = Application Programming Interface = Интерфейс для программ”

Лингвистическая аналогия:

Люди общаются словами
Программы общаются через API
API = словарь понятий между программами

Практическое изучение:

// Примеры API запросов
GET /api/sensors           Получить все данные датчиков
GET /api/temperature       Получить только температуру  
POST /api/heater          Переключить обогреватель
PUT /api/mode/comfort     Установить комфортный режим
DELETE /api/history       Очистить историю

Интерактивное упражнение: “API переводчик”

  • Дети переводят человеческие команды в API запросы
  • “Включи свет” → POST /api/light {"state": "on"}
  • “Какая температура?” → GET /api/temperature
  • “Сделай тепло и светло” → POST /api/mode/comfort

Фаза 2: RESTful архитектура (25 мин)

Концепция: “REST = Representational State Transfer”

Философская основа REST:

Каждая "вещь" имеет адрес (URL)
Каждое действие имеет тип (GET, POST, PUT, DELETE)
Каждый ответ содержит всю нужную информацию

Практический REST API для умной системы:

// RESTful endpoints для нашей системы

// === РЕСУРСЫ (СУЩЕСТВИТЕЛЬНЫЕ) ===
GET    /sensors           // Все датчики
GET    /sensors/temperature // Конкретный датчик
GET    /devices           // Все устройства
GET    /devices/heater    // Конкретное устройство

// === ДЕЙСТВИЯ (ГЛАГОЛЫ) ===  
POST   /devices/heater/on     // Включить
POST   /devices/heater/off    // Выключить
PUT    /sensors/temperature/calibrate // Калибровать
DELETE /history               // Очистить историю

// === СОСТОЯНИЯ (ПРИЛАГАТЕЛЬНЫЕ) ===
GET    /status               // Общий статус
GET    /mode                // Текущий режим
PUT    /mode/comfort        // Изменить режим

Педагогический момент:

  • “REST = как организовать разговор программ”
  • “URL = адрес информации в интернете”
  • “HTTP методы = что мы хотим сделать”

Фаза 3: WebSocket для real-time (35 мин)

Концепция: “WebSocket = постоянная связь”

Аналогия с коммуникацией:

HTTP = почта (вопрос → ответ → конец связи)
WebSocket = телефонный разговор (постоянная связь)

Практическая реализация WebSocket:

#include <WebSocketsServer.h>

WebSocketsServer webSocket = WebSocketsServer(81);

void setup() {
    // ... другая инициализация ...
    
    // Запуск WebSocket сервера
    webSocket.begin();
    webSocket.onEvent(webSocketEvent);
    
    Serial.println("🔗 WebSocket сервер запущен на порту 81");
}

void loop() {
    webSocket.loop(); // Обработка WebSocket соединений
    
    // Отправляем данные всем подключенным клиентам каждые 2 секунды
    static unsigned long lastBroadcast = 0;
    if (millis() - lastBroadcast > 2000) {
        broadcastSensorData();
        lastBroadcast = millis();
    }
}

void webSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t length) {
    switch(type) {
        case WStype_DISCONNECTED:
            Serial.printf("👋 Клиент %u отключился\n", num);
            break;
            
        case WStype_CONNECTED: {
            IPAddress ip = webSocket.remoteIP(num);
            Serial.printf("🤝 Новый клиент %u подключился с IP: %s\n", num, ip.toString().c_str());
            
            // Отправляем приветствие
            webSocket.sendTXT(num, "🤖 Привет! Я - система умного класса!");
            break;
        }
        
        case WStype_TEXT:
            Serial.printf("📨 Получил сообщение от клиента %u: %s\n", num, payload);
            handleWebSocketMessage(num, (char*)payload);
            break;
    }
}

void handleWebSocketMessage(uint8_t clientNum, String message) {
    // Обработка команд от клиента
    if (message == "get_sensors") {
        sendSensorDataToClient(clientNum);
    } 
    else if (message == "heater_on") {
        currentState.heaterOn = true;
        digitalWrite(heaterPin, HIGH);
        webSocket.sendTXT(clientNum, "🔥 Обогреватель включен");
        // Уведомляем всех остальных клиентов
        webSocket.broadcastTXT("📢 Обогреватель включен пользователем");
    }
    else if (message == "heater_off") {
        currentState.heaterOn = false;
        digitalWrite(heaterPin, LOW);
        webSocket.sendTXT(clientNum, "❄️ Обогреватель выключен");
        webSocket.broadcastTXT("📢 Обогреватель выключен пользователем");
    }
}

void broadcastSensorData() {
    // Создаем JSON с данными
    DynamicJsonDocument doc(512);
    doc["type"] = "sensor_update";
    doc["temperature"] = currentState.temperature;
    doc["light"] = map(currentState.light, 0, 4095, 0, 100);
    doc["sound"] = map(currentState.sound, 0, 4095, 0, 100);
    doc["motion"] = currentState.motion;
    doc["timestamp"] = millis();
    
    String jsonString;
    serializeJson(doc, jsonString);
    
    // Отправляем всем подключенным клиентам
    webSocket.broadcastTXT(jsonString);
}

void sendSensorDataToClient(uint8_t clientNum) {
    DynamicJsonDocument doc(512);
    doc["type"] = "sensor_data";
    doc["temperature"] = currentState.temperature;
    doc["light"] = map(currentState.light, 0, 4095, 0, 100);
    doc["sound"] = map(currentState.sound, 0, 4095, 0, 100);
    doc["motion"] = currentState.motion;
    
    String jsonString;
    serializeJson(doc, jsonString);
    
    webSocket.sendTXT(clientNum, jsonString);
}

Клиентская часть JavaScript для WebSocket:

// Подключение к WebSocket серверу
const socket = new WebSocket('ws://' + window.location.hostname + ':81');

socket.onopen = function(event) {
    console.log('🔗 Подключен к системе в реальном времени');
    addLogMessage('🟢 Подключение к системе установлено');
};

socket.onmessage = function(event) {
    console.log('📨 Получено сообщение:', event.data);
    
    try {
        const data = JSON.parse(event.data);
        
        if (data.type === 'sensor_update') {
            // Обновляем интерфейс в реальном времени
            updateSensorDisplay(data);
            addLogMessage('📊 Обновлены данные датчиков');
        }
    } catch (e) {
        // Обычное текстовое сообщение
        addLogMessage('💬 ' + event.data);
    }
};

socket.onerror = function(error) {
    console.error('❌ Ошибка WebSocket:', error);
    addLogMessage('🔴 Ошибка соединения');
};

socket.onclose = function(event) {
    console.log('👋 Соединение закрыто');
    addLogMessage('🔴 Соединение разорвано');
};

// Функция для отправки команд
function sendCommand(command) {
    if (socket.readyState === WebSocket.OPEN) {
        socket.send(command);
        addLogMessage('📤 Отправлена команда: ' + command);
    } else {
        addLogMessage('❌ Нет соединения с системой');
    }
}

// Добавляем лог сообщений на страницу
function addLogMessage(message) {
    const logContainer = document.getElementById('systemLog');
    const logEntry = document.createElement('div');
    logEntry.className = 'log-entry';
    logEntry.textContent = new Date().toLocaleTimeString() + ' - ' + message;
    logContainer.appendChild(logEntry);
    
    // Автопрокрутка вниз
    logContainer.scrollTop = logContainer.scrollHeight;
}

// Обновление интерфейса в реальном времени
function updateSensorDisplay(data) {
    document.getElementById('temperature').textContent = data.temperature + '°C';
    document.getElementById('light').textContent = data.light + '%';
    document.getElementById('sound').textContent = data.sound + 'дБ';
    document.getElementById('motion').textContent = data.motion ? 'Есть' : 'Нет';
    
    // Визуальные эффекты при изменении
    animateValueChange('temperature');
    animateValueChange('light');
    animateValueChange('sound');
    animateValueChange('motion');
}

function animateValueChange(elementId) {
    const element = document.getElementById(elementId);
    element.style.transform = 'scale(1.1)';
    element.style.color = '#ffff00';
    
    setTimeout(() => {
        element.style.transform = 'scale(1)';
        element.style.color = '';
    }, 200);
}

Занятие 4: “Масштабирование и безопасность” 🔒

Длительность: 90 минут

Фаза 1: Безопасность IoT систем (30 мин)

Концепция: “С большой силой приходит большая ответственность”

Этические вопросы:

  • “Кто должен иметь доступ к нашей системе?”
  • “Что если кто-то злой получит управление нашим классом?”
  • “Как защитить личные данные учеников?”

Практические меры безопасности:

// Простая аутентификация
const String ADMIN_PASSWORD = "school2024";
bool isAuthenticated = false;

void handleLogin() {
    if (server.hasArg("password")) {
        String password = server.arg("password");
        
        if (password == ADMIN_PASSWORD) {
            isAuthenticated = true;
            server.send(200, "application/json", "{\"success\": true, \"message\": \"Добро пожаловать!\"}");
            Serial.println("✅ Успешная авторизация");
        } else {
            server.send(401, "application/json", "{\"success\": false, \"message\": \"Неверный пароль\"}");
            Serial.println("❌ Неудачная попытка входа");
        }
    }
}

void handleControlAPI() {
    if (!isAuthenticated) {
        server.send(401, "application/json", "{\"error\": \"Необходима авторизация\"}");
        return;
    }
    
    // Только аутентифицированные пользователи могут управлять
    // ... код управления ...
}

Шифрование данных:

#include <mbedtls/md5.h>

String hashPassword(String password) {
    // Простое хеширование пароля
    unsigned char hash[16];
    mbedtls_md5_context ctx;
    
    mbedtls_md5_init(&ctx);
    mbedtls_md5_starts(&ctx);
    mbedtls_md5_update(&ctx, (unsigned char*)password.c_str(), password.length());
    mbedtls_md5_finish(&ctx, hash);
    mbedtls_md5_free(&ctx);
    
    String hashString = "";
    for (int i = 0; i < 16; i++) {
        if (hash[i] < 16) hashString += "0";
        hashString += String(hash[i], HEX);
    }
    
    return hashString;
}

Фаза 2: Логирование и мониторинг (30 мин)

Концепция: “Система должна помнить всё”

Система логирования:

#include <SD.h>

struct LogEntry {
    unsigned long timestamp;
    String level;      // INFO, WARNING, ERROR
    String message;
    String user;
    float sensorData[4]; // температура, свет, звук, движение
};

void logEvent(String level, String message, String user = "system") {
    LogEntry entry;
    entry.timestamp = millis();
    entry.level = level;
    entry.message = message;
    entry.user = user;
    entry.sensorData[0] = currentState.temperature;
    entry.sensorData[1] = currentState.light;
    entry.sensorData[2] = currentState.sound;
    entry.sensorData[3] = currentState.motion;
    
    // Сохранение в файл
    File logFile = SD.open("/system.log", FILE_APPEND);
    if (logFile) {
        String logLine = String(entry.timestamp) + "," + 
                        entry.level + "," + 
                        entry.message + "," + 
                        entry.user + "," +
                        String(entry.sensorData[0]) + "," +
                        String(entry.sensorData[1]) + "," +
                        String(entry.sensorData[2]) + "," +
                        String(entry.sensorData[3]);
        
        logFile.println(logLine);
        logFile.close();
        
        Serial.println("📝 Записал в лог: " + logLine);
    }
    
    // Отправка критических событий администратору
    if (level == "ERROR" || level == "WARNING") {
        notifyAdministrator(entry);
    }
}

void notifyAdministrator(LogEntry entry) {
    // Отправка уведомления через WebSocket всем подключенным админам
    DynamicJsonDocument doc(512);
    doc["type"] = "alert";
    doc["level"] = entry.level;
    doc["message"] = entry.message;
    doc["timestamp"] = entry.timestamp;
    
    String alertJson;
    serializeJson(doc, alertJson);
    
    webSocket.broadcastTXT("🚨 ALERT: " + alertJson);
    
    Serial.println("🚨 Отправил уведомление администратору");
}

// API для получения логов
void handleLogsAPI() {
    if (!isAuthenticated) {
        server.send(401, "application/json", "{\"error\": \"Доступ запрещен\"}");
        return;
    }
    
    File logFile = SD.open("/system.log", FILE_READ);
    if (logFile) {
        String logs = "";
        while (logFile.available()) {
            logs += logFile.readStringUntil('\n') + "\n";
        }
        logFile.close();
        
        server.send(200, "text/plain", logs);
    } else {
        server.send(500, "application/json", "{\"error\": \"Не удалось прочитать логи\"}");
    }
}

Фаза 3: Масштабирование системы (30 мин)

Концепция: “От одного класса к умной школе”

Архитектура масштабируемой системы:

Уровень 1: Один класс (ESP32 + датчики)
Уровень 2: Несколько классов (ESP32 → Центральный сервер)
Уровень 3: Вся школа (Центральный сервер → Облако)
Уровень 4: Город (Облако → Городская система)

Практическая реализация:

// Конфигурация для масштабирования
struct SystemConfig {
    String classroomId;      // "5B", "6A", "physics_lab"
    String schoolId;         // "school_42_moscow"
    String centralServerURL; // "http://school-server.local"
    int reportingInterval;   // как часто отправлять данные
};

SystemConfig config = {
    .classroomId = "5B",
    .schoolId = "school_42_moscow", 
    .centralServerURL = "http://192.168.1.100:3000",
    .reportingInterval = 30000 // 30 секунд
};

void reportToCentralServer() {
    if (WiFi.status() == WL_CONNECTED) {
        HTTPClient http;
        http.begin(config.centralServerURL + "/api/classrooms/" + config.classroomId + "/report");
        http.addHeader("Content-Type", "application/json");
        
        // Создаем отчет
        DynamicJsonDocument report(1024);
        report["classroomId"] = config.classroomId;
        report["schoolId"] = config.schoolId;
        report["timestamp"] = millis();
        report["temperature"] = currentState.temperature;
        report["light"] = currentState.light;
        report["sound"] = currentState.sound;
        report["motion"] = currentState.motion;
        report["heaterOn"] = currentState.heaterOn;
        report["lampOn"] = currentState.lampOn;
        report["mode"] = currentState.mode;
        report["lastDecision"] = currentState.lastDecision;
        
        String reportJson;
        serializeJson(report, reportJson);
        
        int httpResponseCode = http.POST(reportJson);
        
        if (httpResponseCode == 200) {
            String response = http.getString();
            Serial.println("📡 Отчет отправлен в центральную систему");
            logEvent("INFO", "Отчет отправлен в центральную систему");
            
            // Обработка команд от центральной системы
            DynamicJsonDocument responseDoc(512);
            deserializeJson(responseDoc, response);
            
            if (responseDoc["command"]) {
                executeRemoteCommand(responseDoc["command"]);
            }
        } else {
            Serial.println("❌ Ошибка отправки отчета: " + String(httpResponseCode));
            logEvent("ERROR", "Ошибка отправки отчета в центральную систему");
        }
        
        http.end();
    }
}

void executeRemoteCommand(String command) {
    logEvent("INFO", "Получена команда от центральной системы: " + command);
    
    if (command == "emergency_heating") {
        // Экстренное включение отопления во всех классах
        currentState.heaterOn = true;
        digitalWrite(heaterPin, HIGH);
        currentState.lastDecision = "Экстренное отопление по команде администрации";
        
    } else if (command == "energy_save_mode") {
        // Энергосберегающий режим для всей школы
        currentState.mode = "eco";
        currentState.heaterOn = false;
        currentState.lampOn = false;
        digitalWrite(heaterPin, LOW);
        digitalWrite(lampPin, LOW);
        currentState.lastDecision = "Энергосберегающий режим по команде администрации";
        
    } else if (command == "security_lockdown") {
        // Режим безопасности
        isAuthenticated = false; // Блокируем локальное управление
        currentState.mode = "security";
        currentState.lastDecision = "Режим безопасности активирован";
    }
    
    // Уведомляем пользователей через WebSocket
    webSocket.broadcastTXT("📢 Система получила команду: " + command);
}

🎯 КЛЮЧЕВЫЕ ДОСТИЖЕНИЯ СПРИНТА 17

Концептуальные прорывы:

Понимание веб-архитектуры - клиент-сервер модель
API как язык машин - стандартизированное взаимодействие
Real-time коммуникация - WebSocket vs HTTP
Безопасность IoT - аутентификация и авторизация
Масштабируемость - от устройства к системе систем

Технические навыки:

✅ Создание веб-сервера на микроконтроллере
✅ RESTful API проектирование и реализация
✅ HTML/CSS/JavaScript для IoT интерфейсов
✅ WebSocket для real-time приложений
✅ Основы безопасности и логирования

Метакогнитивные навыки:

Архитектурное мышление - как спроектировать систему
Пользовательский опыт - дизайн интерфейсов
Системная безопасность - защита от угроз
Масштабное мышление - от прототипа к продукту


🚀 ПОДГОТОВКА К СПРИНТУ 18

Мостик к интерактивному интерфейсу:

“Наша система умеет отвечать на вопросы, но как сделать общение с ней ещё более живым и интуитивным?”

Фундамент для следующих концепций:

  • ✅ Веб-технологии как основа современных интерфейсов
  • ✅ API как стандарт взаимодействия систем
  • ✅ Real-time коммуникация для интерактивности
  • ✅ Безопасность как обязательный компонент

Психологическая трансформация:

Дети больше не видят ESP32 как “железку” - теперь это “цифровой амбассадор” их умной системы, который может общаться со всем миром!

Спринт 17 завершен! 🌐
Дети превратили локальную умную систему в глобально доступный веб-сервис!