Skip to main content

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

 1// Минимальный веб-сервер - "Привет, мир!"
 2#include <WiFi.h>
 3#include <WebServer.h>
 4
 5WebServer server(80);
 6
 7void handleRoot() {
 8    server.send(200, "text/html", 
 9        "<h1>🤖 Привет! Я - умная система класса!</h1>"
10        "<p>Спроси меня о температуре: /temperature</p>");
11}
12
13void setup() {
14    WiFi.begin("School_WiFi", "password");
15    server.on("/", handleRoot);
16    server.begin();
17    
18    Serial.println("🌐 Веб-сервер запущен!");
19    Serial.println("Адрес: http://" + WiFi.localIP().toString());
20}
21
22void loop() {
23    server.handleClient(); // Отвечаем на запросы
24}

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

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

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

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

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

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

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

1Клиент говорит: "GET /temperature HTTP/1.1"
2Сервер отвечает: "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 = скелет веб-страницы”

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

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

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

  1<!DOCTYPE html>
  2<html>
  3<head>
  4    <title>🏫 Умный класс 5Б</title>
  5    <meta charset="UTF-8">
  6    <meta name="viewport" content="width=device-width, initial-scale=1.0">
  7    <style>
  8        body { 
  9            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
 10            margin: 0;
 11            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
 12            color: white;
 13        }
 14        
 15        .container {
 16            max-width: 800px;
 17            margin: 0 auto;
 18            padding: 20px;
 19        }
 20        
 21        .header {
 22            text-align: center;
 23            margin-bottom: 30px;
 24        }
 25        
 26        .sensor-grid {
 27            display: grid;
 28            grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
 29            gap: 20px;
 30            margin-bottom: 30px;
 31        }
 32        
 33        .sensor-card {
 34            background: rgba(255,255,255,0.1);
 35            backdrop-filter: blur(10px);
 36            border-radius: 15px;
 37            padding: 20px;
 38            border: 1px solid rgba(255,255,255,0.2);
 39            transition: transform 0.3s ease;
 40        }
 41        
 42        .sensor-card:hover {
 43            transform: translateY(-5px);
 44        }
 45        
 46        .sensor-icon {
 47            font-size: 3rem;
 48            margin-bottom: 10px;
 49        }
 50        
 51        .sensor-value {
 52            font-size: 2rem;
 53            font-weight: bold;
 54            margin: 10px 0;
 55        }
 56        
 57        .sensor-status {
 58            font-size: 0.9rem;
 59            opacity: 0.8;
 60        }
 61        
 62        .controls {
 63            display: grid;
 64            grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
 65            gap: 15px;
 66        }
 67        
 68        .control-button {
 69            background: rgba(255,255,255,0.2);
 70            border: none;
 71            border-radius: 10px;
 72            padding: 15px;
 73            color: white;
 74            font-size: 1rem;
 75            cursor: pointer;
 76            transition: all 0.3s ease;
 77        }
 78        
 79        .control-button:hover {
 80            background: rgba(255,255,255,0.3);
 81            transform: scale(1.05);
 82        }
 83        
 84        .status-bar {
 85            margin-top: 20px;
 86            padding: 15px;
 87            background: rgba(0,0,0,0.2);
 88            border-radius: 10px;
 89            text-align: center;
 90        }
 91    </style>
 92</head>
 93<body>
 94    <div class="container">
 95        <!-- Заголовок -->
 96        <div class="header">
 97            <h1>🤖 ALEX - Умная система класса</h1>
 98            <p>Статус: <span id="systemStatus">🟢 Активна и заботится о вас</span></p>
 99        </div>
100        
101        <!-- Датчики -->
102        <div class="sensor-grid">
103            <div class="sensor-card">
104                <div class="sensor-icon">🌡️</div>
105                <div class="sensor-name">Температура</div>
106                <div class="sensor-value" id="temperature">--°C</div>
107                <div class="sensor-status" id="tempStatus">Загрузка...</div>
108            </div>
109            
110            <div class="sensor-card">
111                <div class="sensor-icon">💡</div>
112                <div class="sensor-name">Освещенность</div>
113                <div class="sensor-value" id="light">--%</div>
114                <div class="sensor-status" id="lightStatus">Загрузка...</div>
115            </div>
116            
117            <div class="sensor-card">
118                <div class="sensor-icon">🔊</div>
119                <div class="sensor-name">Уровень шума</div>
120                <div class="sensor-value" id="sound">--дБ</div>
121                <div class="sensor-status" id="soundStatus">Загрузка...</div>
122            </div>
123            
124            <div class="sensor-card">
125                <div class="sensor-icon">🚶</div>
126                <div class="sensor-name">Движение</div>
127                <div class="sensor-value" id="motion">--</div>
128                <div class="sensor-status" id="motionStatus">Загрузка...</div>
129            </div>
130        </div>
131        
132        <!-- Управление -->
133        <div class="controls">
134            <button class="control-button" onclick="toggleHeater()">
135                🔥 Обогреватель
136            </button>
137            <button class="control-button" onclick="toggleLight()">
138                💡 Освещение
139            </button>
140            <button class="control-button" onclick="setComfortMode()">
141                😌 Комфортный режим
142            </button>
143            <button class="control-button" onclick="setEcoMode()">
144                🌱 Эко-режим
145            </button>
146        </div>
147        
148        <!-- Статус системы -->
149        <div class="status-bar">
150            <strong>Последнее решение:</strong> 
151            <span id="lastDecision">Система запускается...</span>
152        </div>
153    </div>
154
155    <script>
156        // JavaScript для интерактивности
157        let sensorData = {};
158        
159        // Обновление данных каждые 2 секунды
160        setInterval(updateSensorData, 2000);
161        
162        async function updateSensorData() {
163            try {
164                // Запрашиваем данные с ESP32
165                const response = await fetch('/api/sensors');
166                sensorData = await response.json();
167                
168                // Обновляем интерфейс
169                document.getElementById('temperature').textContent = sensorData.temperature + '°C';
170                document.getElementById('light').textContent = sensorData.light + '%';
171                document.getElementById('sound').textContent = sensorData.sound + 'дБ';
172                document.getElementById('motion').textContent = sensorData.motion ? 'Есть' : 'Нет';
173                
174                // Обновляем статусы
175                updateSensorStatuses();
176                
177            } catch (error) {
178                console.error('Ошибка получения данных:', error);
179                document.getElementById('systemStatus').textContent = '🔴 Нет связи с системой';
180            }
181        }
182        
183        function updateSensorStatuses() {
184            // Умные статусы на основе данных
185            const temp = sensorData.temperature;
186            if (temp < 18) {
187                document.getElementById('tempStatus').textContent = '🥶 Холодно, включаю обогрев';
188            } else if (temp > 25) {
189                document.getElementById('tempStatus').textContent = '🔥 Жарко, нужно охлаждение';
190            } else {
191                document.getElementById('tempStatus').textContent = '😌 Комфортная температура';
192            }
193            
194            const light = sensorData.light;
195            if (light < 30) {
196                document.getElementById('lightStatus').textContent = '🌙 Темно, включаю свет';
197            } else if (light > 80) {
198                document.getElementById('lightStatus').textContent = '☀️ Очень светло';
199            } else {
200                document.getElementById('lightStatus').textContent = '👍 Хорошее освещение';
201            }
202            
203            const motion = sensorData.motion;
204            document.getElementById('motionStatus').textContent = 
205                motion ? '👥 Люди в классе' : '🏫 Класс пустой';
206        }
207        
208        async function toggleHeater() {
209            try {
210                const response = await fetch('/api/heater', { method: 'POST' });
211                const result = await response.json();
212                alert('🔥 ' + result.message);
213            } catch (error) {
214                alert('❌ Ошибка управления обогревателем');
215            }
216        }
217        
218        async function toggleLight() {
219            try {
220                const response = await fetch('/api/light', { method: 'POST' });
221                const result = await response.json();
222                alert('💡 ' + result.message);
223            } catch (error) {
224                alert('❌ Ошибка управления освещением');
225            }
226        }
227        
228        async function setComfortMode() {
229            try {
230                const response = await fetch('/api/mode/comfort', { method: 'POST' });
231                const result = await response.json();
232                alert('😌 Включен комфортный режим');
233                document.getElementById('lastDecision').textContent = result.decision;
234            } catch (error) {
235                alert('❌ Ошибка переключения режима');
236            }
237        }
238        
239        async function setEcoMode() {
240            try {
241                const response = await fetch('/api/mode/eco', { method: 'POST' });
242                const result = await response.json();
243                alert('🌱 Включен эко-режим');
244                document.getElementById('lastDecision').textContent = result.decision;
245            } catch (error) {
246                alert('❌ Ошибка переключения режима');
247            }
248        }
249        
250        // Первоначальная загрузка данных
251        updateSensorData();
252    </script>
253</body>
254</html>

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

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

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

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

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

  1#include <WiFi.h>
  2#include <WebServer.h>
  3#include <ArduinoJson.h>
  4#include <DHT.h>
  5#include <SPIFFS.h>
  6
  7// === КОНФИГУРАЦИЯ ===
  8const char* ssid = "School_WiFi";
  9const char* password = "school_password";
 10
 11// Датчики (из предыдущих спринтов)
 12DHT dht(2, DHT22);
 13const int lightPin = A0;
 14const int soundPin = A1; 
 15const int pirPin = 3;
 16
 17// Исполнители
 18const int heaterPin = 4;
 19const int lampPin = 5;
 20
 21// Веб-сервер
 22WebServer server(80);
 23
 24// Состояние системы
 25struct SystemState {
 26    float temperature;
 27    int light;
 28    int sound;
 29    bool motion;
 30    bool heaterOn;
 31    bool lampOn;
 32    String mode;
 33    String lastDecision;
 34    unsigned long lastUpdate;
 35};
 36
 37SystemState currentState;
 38
 39void setup() {
 40    Serial.begin(115200);
 41    
 42    // Инициализация датчиков
 43    dht.begin();
 44    pinMode(pirPin, INPUT);
 45    pinMode(heaterPin, OUTPUT);
 46    pinMode(lampPin, OUTPUT);
 47    
 48    // Инициализация файловой системы для HTML
 49    if (!SPIFFS.begin(true)) {
 50        Serial.println("❌ Ошибка инициализации SPIFFS");
 51        return;
 52    }
 53    
 54    // Подключение к WiFi
 55    connectToWiFi();
 56    
 57    // Настройка маршрутов веб-сервера
 58    setupWebRoutes();
 59    
 60    // Запуск сервера
 61    server.begin();
 62    
 63    Serial.println("🌐 Веб-сервер запущен!");
 64    Serial.println("📱 Адрес: http://" + WiFi.localIP().toString());
 65    Serial.println("🎉 Система готова к работе!");
 66    
 67    // Инициализация состояния
 68    currentState.mode = "comfort";
 69    currentState.lastDecision = "Система запущена";
 70    currentState.heaterOn = false;
 71    currentState.lampOn = false;
 72}
 73
 74void loop() {
 75    server.handleClient(); // Обрабатываем веб-запросы
 76    updateSensors();       // Обновляем данные датчиков
 77    makeAutomaticDecisions(); // Автоматическая логика
 78    delay(100);
 79}
 80
 81void connectToWiFi() {
 82    Serial.println("🔗 Подключаюсь к WiFi...");
 83    WiFi.begin(ssid, password);
 84    
 85    int attempts = 0;
 86    while (WiFi.status() != WL_CONNECTED && attempts < 30) {
 87        delay(1000);
 88        Serial.print(".");
 89        attempts++;
 90    }
 91    
 92    if (WiFi.status() == WL_CONNECTED) {
 93        Serial.println("\n✅ WiFi подключен!");
 94        Serial.println("🌍 IP адрес: " + WiFi.localIP().toString());
 95    } else {
 96        Serial.println("\n❌ Не удалось подключиться к WiFi");
 97    }
 98}
 99
100void setupWebRoutes() {
101    // Главная страница
102    server.on("/", HTTP_GET, handleRoot);
103    
104    // API для получения данных датчиков
105    server.on("/api/sensors", HTTP_GET, handleSensorsAPI);
106    
107    // API для управления обогревателем
108    server.on("/api/heater", HTTP_POST, handleHeaterAPI);
109    
110    // API для управления освещением
111    server.on("/api/light", HTTP_POST, handleLightAPI);
112    
113    // API для переключения режимов
114    server.on("/api/mode/comfort", HTTP_POST, handleComfortModeAPI);
115    server.on("/api/mode/eco", HTTP_POST, handleEcoModeAPI);
116    
117    // API для получения статуса системы
118    server.on("/api/status", HTTP_GET, handleStatusAPI);
119    
120    // Обработка несуществующих страниц
121    server.onNotFound(handleNotFound);
122}
123
124void handleRoot() {
125    // Отдаем HTML страницу (код выше)
126    String html = getMainPageHTML();
127    server.send(200, "text/html", html);
128}
129
130void handleSensorsAPI() {
131    // Создаем JSON с данными датчиков
132    DynamicJsonDocument doc(1024);
133    
134    doc["temperature"] = currentState.temperature;
135    doc["light"] = map(currentState.light, 0, 4095, 0, 100);
136    doc["sound"] = map(currentState.sound, 0, 4095, 0, 100);
137    doc["motion"] = currentState.motion;
138    doc["timestamp"] = millis();
139    doc["status"] = "online";
140    
141    String response;
142    serializeJson(doc, response);
143    
144    server.send(200, "application/json", response);
145    
146    Serial.println("📤 Отправил данные датчиков: " + response);
147}
148
149void handleHeaterAPI() {
150    currentState.heaterOn = !currentState.heaterOn;
151    digitalWrite(heaterPin, currentState.heaterOn ? HIGH : LOW);
152    
153    DynamicJsonDocument doc(512);
154    doc["success"] = true;
155    doc["heaterOn"] = currentState.heaterOn;
156    doc["message"] = currentState.heaterOn ? "Обогреватель включен" : "Обогреватель выключен";
157    
158    String response;
159    serializeJson(doc, response);
160    
161    server.send(200, "application/json", response);
162    
163    currentState.lastDecision = "Пользователь " + String(currentState.heaterOn ? "включил" : "выключил") + " обогреватель";
164    Serial.println("🔥 " + currentState.lastDecision);
165}
166
167void handleLightAPI() {
168    currentState.lampOn = !currentState.lampOn;
169    digitalWrite(lampPin, currentState.lampOn ? HIGH : LOW);
170    
171    DynamicJsonDocument doc(512);
172    doc["success"] = true;
173    doc["lampOn"] = currentState.lampOn;
174    doc["message"] = currentState.lampOn ? "Освещение включено" : "Освещение выключено";
175    
176    String response;
177    serializeJson(doc, response);
178    
179    server.send(200, "application/json", response);
180    
181    currentState.lastDecision = "Пользователь " + String(currentState.lampOn ? "включил" : "выключил") + " освещение";
182    Serial.println("💡 " + currentState.lastDecision);
183}
184
185void handleComfortModeAPI() {
186    currentState.mode = "comfort";
187    
188    // Автоматическая настройка для комфорта
189    if (currentState.temperature < 22) {
190        currentState.heaterOn = true;
191        digitalWrite(heaterPin, HIGH);
192    }
193    
194    if (currentState.light < 30) {
195        currentState.lampOn = true;
196        digitalWrite(lampPin, HIGH);
197    }
198    
199    DynamicJsonDocument doc(512);
200    doc["success"] = true;
201    doc["mode"] = "comfort";
202    doc["decision"] = "Настроил комфортные условия: тепло и светло";
203    
204    String response;
205    serializeJson(doc, response);
206    
207    server.send(200, "application/json", response);
208    
209    currentState.lastDecision = "Включен комфортный режим";
210    Serial.println("😌 " + currentState.lastDecision);
211}
212
213void handleEcoModeAPI() {
214    currentState.mode = "eco";
215    
216    // Энергосберегающие настройки
217    if (!currentState.motion) {
218        currentState.heaterOn = false;
219        currentState.lampOn = false;
220        digitalWrite(heaterPin, LOW);
221        digitalWrite(lampPin, LOW);
222    }
223    
224    DynamicJsonDocument doc(512);
225    doc["success"] = true;
226    doc["mode"] = "eco";
227    doc["decision"] = "Экономлю энергию: выключил ненужные устройства";
228    
229    String response;
230    serializeJson(doc, response);
231    
232    server.send(200, "application/json", response);
233    
234    currentState.lastDecision = "Включен эко-режим";
235    Serial.println("🌱 " + currentState.lastDecision);
236}
237
238void handleStatusAPI() {
239    DynamicJsonDocument doc(1024);
240    
241    doc["mode"] = currentState.mode;
242    doc["lastDecision"] = currentState.lastDecision;
243    doc["uptime"] = millis();
244    doc["heaterOn"] = currentState.heaterOn;
245    doc["lampOn"] = currentState.lampOn;
246    doc["wifiStrength"] = WiFi.RSSI();
247    doc["freeMemory"] = ESP.getFreeHeap();
248    
249    String response;
250    serializeJson(doc, response);
251    
252    server.send(200, "application/json", response);
253}
254
255void handleNotFound() {
256    server.send(404, "text/plain", "🤖 Страница не найдена!\nЯ умная система, но не всемогущая 😅");
257}
258
259void updateSensors() {
260    // Обновляем данные каждую секунду
261    static unsigned long lastUpdate = 0;
262    if (millis() - lastUpdate > 1000) {
263        currentState.temperature = dht.readTemperature();
264        currentState.light = analogRead(lightPin);
265        currentState.sound = analogRead(soundPin);
266        currentState.motion = digitalRead(pirPin);
267        currentState.lastUpdate = millis();
268        
269        lastUpdate = millis();
270    }
271}
272
273void makeAutomaticDecisions() {
274    // Автоматическая логика (из спринта 16)
275    static unsigned long lastDecision = 0;
276    if (millis() - lastDecision > 5000) { // Принимаем решения каждые 5 секунд
277        
278        if (currentState.mode == "comfort") {
279            // Логика комфортного режима
280            if (currentState.temperature < 20 && currentState.motion) {
281                if (!currentState.heaterOn) {
282                    currentState.heaterOn = true;
283                    digitalWrite(heaterPin, HIGH);
284                    currentState.lastDecision = "Автоматически включил обогрев - пришли люди";
285                }
286            }
287            
288            if (currentState.light < 20 && currentState.motion) {
289                if (!currentState.lampOn) {
290                    currentState.lampOn = true;
291                    digitalWrite(lampPin, HIGH);
292                    currentState.lastDecision = "Автоматически включил свет - стало темно";
293                }
294            }
295        }
296        
297        lastDecision = millis();
298    }
299}
300
301String getMainPageHTML() {
302    // Возвращаем HTML код (тот, что был выше)
303    return R"(
304    <!DOCTYPE html>
305    <html>
306    <!-- Здесь весь HTML код -->
307    </html>
308    )";
309}

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

Метод: User Testing

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

 1// RESTful endpoints для нашей системы
 2
 3// === РЕСУРСЫ (СУЩЕСТВИТЕЛЬНЫЕ) ===
 4GET    /sensors           // Все датчики
 5GET    /sensors/temperature // Конкретный датчик
 6GET    /devices           // Все устройства
 7GET    /devices/heater    // Конкретное устройство
 8
 9// === ДЕЙСТВИЯ (ГЛАГОЛЫ) ===  
10POST   /devices/heater/on     // Включить
11POST   /devices/heater/off    // Выключить
12PUT    /sensors/temperature/calibrate // Калибровать
13DELETE /history               // Очистить историю
14
15// === СОСТОЯНИЯ (ПРИЛАГАТЕЛЬНЫЕ) ===
16GET    /status               // Общий статус
17GET    /mode                // Текущий режим
18PUT    /mode/comfort        // Изменить режим

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

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

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

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

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

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

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

 1#include <WebSocketsServer.h>
 2
 3WebSocketsServer webSocket = WebSocketsServer(81);
 4
 5void setup() {
 6    // ... другая инициализация ...
 7    
 8    // Запуск WebSocket сервера
 9    webSocket.begin();
10    webSocket.onEvent(webSocketEvent);
11    
12    Serial.println("🔗 WebSocket сервер запущен на порту 81");
13}
14
15void loop() {
16    webSocket.loop(); // Обработка WebSocket соединений
17    
18    // Отправляем данные всем подключенным клиентам каждые 2 секунды
19    static unsigned long lastBroadcast = 0;
20    if (millis() - lastBroadcast > 2000) {
21        broadcastSensorData();
22        lastBroadcast = millis();
23    }
24}
25
26void webSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t length) {
27    switch(type) {
28        case WStype_DISCONNECTED:
29            Serial.printf("👋 Клиент %u отключился\n", num);
30            break;
31            
32        case WStype_CONNECTED: {
33            IPAddress ip = webSocket.remoteIP(num);
34            Serial.printf("🤝 Новый клиент %u подключился с IP: %s\n", num, ip.toString().c_str());
35            
36            // Отправляем приветствие
37            webSocket.sendTXT(num, "🤖 Привет! Я - система умного класса!");
38            break;
39        }
40        
41        case WStype_TEXT:
42            Serial.printf("📨 Получил сообщение от клиента %u: %s\n", num, payload);
43            handleWebSocketMessage(num, (char*)payload);
44            break;
45    }
46}
47
48void handleWebSocketMessage(uint8_t clientNum, String message) {
49    // Обработка команд от клиента
50    if (message == "get_sensors") {
51        sendSensorDataToClient(clientNum);
52    } 
53    else if (message == "heater_on") {
54        currentState.heaterOn = true;
55        digitalWrite(heaterPin, HIGH);
56        webSocket.sendTXT(clientNum, "🔥 Обогреватель включен");
57        // Уведомляем всех остальных клиентов
58        webSocket.broadcastTXT("📢 Обогреватель включен пользователем");
59    }
60    else if (message == "heater_off") {
61        currentState.heaterOn = false;
62        digitalWrite(heaterPin, LOW);
63        webSocket.sendTXT(clientNum, "❄️ Обогреватель выключен");
64        webSocket.broadcastTXT("📢 Обогреватель выключен пользователем");
65    }
66}
67
68void broadcastSensorData() {
69    // Создаем JSON с данными
70    DynamicJsonDocument doc(512);
71    doc["type"] = "sensor_update";
72    doc["temperature"] = currentState.temperature;
73    doc["light"] = map(currentState.light, 0, 4095, 0, 100);
74    doc["sound"] = map(currentState.sound, 0, 4095, 0, 100);
75    doc["motion"] = currentState.motion;
76    doc["timestamp"] = millis();
77    
78    String jsonString;
79    serializeJson(doc, jsonString);
80    
81    // Отправляем всем подключенным клиентам
82    webSocket.broadcastTXT(jsonString);
83}
84
85void sendSensorDataToClient(uint8_t clientNum) {
86    DynamicJsonDocument doc(512);
87    doc["type"] = "sensor_data";
88    doc["temperature"] = currentState.temperature;
89    doc["light"] = map(currentState.light, 0, 4095, 0, 100);
90    doc["sound"] = map(currentState.sound, 0, 4095, 0, 100);
91    doc["motion"] = currentState.motion;
92    
93    String jsonString;
94    serializeJson(doc, jsonString);
95    
96    webSocket.sendTXT(clientNum, jsonString);
97}

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

 1// Подключение к WebSocket серверу
 2const socket = new WebSocket('ws://' + window.location.hostname + ':81');
 3
 4socket.onopen = function(event) {
 5    console.log('🔗 Подключен к системе в реальном времени');
 6    addLogMessage('🟢 Подключение к системе установлено');
 7};
 8
 9socket.onmessage = function(event) {
10    console.log('📨 Получено сообщение:', event.data);
11    
12    try {
13        const data = JSON.parse(event.data);
14        
15        if (data.type === 'sensor_update') {
16            // Обновляем интерфейс в реальном времени
17            updateSensorDisplay(data);
18            addLogMessage('📊 Обновлены данные датчиков');
19        }
20    } catch (e) {
21        // Обычное текстовое сообщение
22        addLogMessage('💬 ' + event.data);
23    }
24};
25
26socket.onerror = function(error) {
27    console.error('❌ Ошибка WebSocket:', error);
28    addLogMessage('🔴 Ошибка соединения');
29};
30
31socket.onclose = function(event) {
32    console.log('👋 Соединение закрыто');
33    addLogMessage('🔴 Соединение разорвано');
34};
35
36// Функция для отправки команд
37function sendCommand(command) {
38    if (socket.readyState === WebSocket.OPEN) {
39        socket.send(command);
40        addLogMessage('📤 Отправлена команда: ' + command);
41    } else {
42        addLogMessage('❌ Нет соединения с системой');
43    }
44}
45
46// Добавляем лог сообщений на страницу
47function addLogMessage(message) {
48    const logContainer = document.getElementById('systemLog');
49    const logEntry = document.createElement('div');
50    logEntry.className = 'log-entry';
51    logEntry.textContent = new Date().toLocaleTimeString() + ' - ' + message;
52    logContainer.appendChild(logEntry);
53    
54    // Автопрокрутка вниз
55    logContainer.scrollTop = logContainer.scrollHeight;
56}
57
58// Обновление интерфейса в реальном времени
59function updateSensorDisplay(data) {
60    document.getElementById('temperature').textContent = data.temperature + '°C';
61    document.getElementById('light').textContent = data.light + '%';
62    document.getElementById('sound').textContent = data.sound + 'дБ';
63    document.getElementById('motion').textContent = data.motion ? 'Есть' : 'Нет';
64    
65    // Визуальные эффекты при изменении
66    animateValueChange('temperature');
67    animateValueChange('light');
68    animateValueChange('sound');
69    animateValueChange('motion');
70}
71
72function animateValueChange(elementId) {
73    const element = document.getElementById(elementId);
74    element.style.transform = 'scale(1.1)';
75    element.style.color = '#ffff00';
76    
77    setTimeout(() => {
78        element.style.transform = 'scale(1)';
79        element.style.color = '';
80    }, 200);
81}

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

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

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

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

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

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

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

 1// Простая аутентификация
 2const String ADMIN_PASSWORD = "school2024";
 3bool isAuthenticated = false;
 4
 5void handleLogin() {
 6    if (server.hasArg("password")) {
 7        String password = server.arg("password");
 8        
 9        if (password == ADMIN_PASSWORD) {
10            isAuthenticated = true;
11            server.send(200, "application/json", "{\"success\": true, \"message\": \"Добро пожаловать!\"}");
12            Serial.println("✅ Успешная авторизация");
13        } else {
14            server.send(401, "application/json", "{\"success\": false, \"message\": \"Неверный пароль\"}");
15            Serial.println("❌ Неудачная попытка входа");
16        }
17    }
18}
19
20void handleControlAPI() {
21    if (!isAuthenticated) {
22        server.send(401, "application/json", "{\"error\": \"Необходима авторизация\"}");
23        return;
24    }
25    
26    // Только аутентифицированные пользователи могут управлять
27    // ... код управления ...
28}

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

 1#include <mbedtls/md5.h>
 2
 3String hashPassword(String password) {
 4    // Простое хеширование пароля
 5    unsigned char hash[16];
 6    mbedtls_md5_context ctx;
 7    
 8    mbedtls_md5_init(&ctx);
 9    mbedtls_md5_starts(&ctx);
10    mbedtls_md5_update(&ctx, (unsigned char*)password.c_str(), password.length());
11    mbedtls_md5_finish(&ctx, hash);
12    mbedtls_md5_free(&ctx);
13    
14    String hashString = "";
15    for (int i = 0; i < 16; i++) {
16        if (hash[i] < 16) hashString += "0";
17        hashString += String(hash[i], HEX);
18    }
19    
20    return hashString;
21}

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

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

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

 1#include <SD.h>
 2
 3struct LogEntry {
 4    unsigned long timestamp;
 5    String level;      // INFO, WARNING, ERROR
 6    String message;
 7    String user;
 8    float sensorData[4]; // температура, свет, звук, движение
 9};
10
11void logEvent(String level, String message, String user = "system") {
12    LogEntry entry;
13    entry.timestamp = millis();
14    entry.level = level;
15    entry.message = message;
16    entry.user = user;
17    entry.sensorData[0] = currentState.temperature;
18    entry.sensorData[1] = currentState.light;
19    entry.sensorData[2] = currentState.sound;
20    entry.sensorData[3] = currentState.motion;
21    
22    // Сохранение в файл
23    File logFile = SD.open("/system.log", FILE_APPEND);
24    if (logFile) {
25        String logLine = String(entry.timestamp) + "," + 
26                        entry.level + "," + 
27                        entry.message + "," + 
28                        entry.user + "," +
29                        String(entry.sensorData[0]) + "," +
30                        String(entry.sensorData[1]) + "," +
31                        String(entry.sensorData[2]) + "," +
32                        String(entry.sensorData[3]);
33        
34        logFile.println(logLine);
35        logFile.close();
36        
37        Serial.println("📝 Записал в лог: " + logLine);
38    }
39    
40    // Отправка критических событий администратору
41    if (level == "ERROR" || level == "WARNING") {
42        notifyAdministrator(entry);
43    }
44}
45
46void notifyAdministrator(LogEntry entry) {
47    // Отправка уведомления через WebSocket всем подключенным админам
48    DynamicJsonDocument doc(512);
49    doc["type"] = "alert";
50    doc["level"] = entry.level;
51    doc["message"] = entry.message;
52    doc["timestamp"] = entry.timestamp;
53    
54    String alertJson;
55    serializeJson(doc, alertJson);
56    
57    webSocket.broadcastTXT("🚨 ALERT: " + alertJson);
58    
59    Serial.println("🚨 Отправил уведомление администратору");
60}
61
62// API для получения логов
63void handleLogsAPI() {
64    if (!isAuthenticated) {
65        server.send(401, "application/json", "{\"error\": \"Доступ запрещен\"}");
66        return;
67    }
68    
69    File logFile = SD.open("/system.log", FILE_READ);
70    if (logFile) {
71        String logs = "";
72        while (logFile.available()) {
73            logs += logFile.readStringUntil('\n') + "\n";
74        }
75        logFile.close();
76        
77        server.send(200, "text/plain", logs);
78    } else {
79        server.send(500, "application/json", "{\"error\": \"Не удалось прочитать логи\"}");
80    }
81}

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

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

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

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

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

 1// Конфигурация для масштабирования
 2struct SystemConfig {
 3    String classroomId;      // "5B", "6A", "physics_lab"
 4    String schoolId;         // "school_42_moscow"
 5    String centralServerURL; // "http://school-server.local"
 6    int reportingInterval;   // как часто отправлять данные
 7};
 8
 9SystemConfig config = {
10    .classroomId = "5B",
11    .schoolId = "school_42_moscow", 
12    .centralServerURL = "http://192.168.1.100:3000",
13    .reportingInterval = 30000 // 30 секунд
14};
15
16void reportToCentralServer() {
17    if (WiFi.status() == WL_CONNECTED) {
18        HTTPClient http;
19        http.begin(config.centralServerURL + "/api/classrooms/" + config.classroomId + "/report");
20        http.addHeader("Content-Type", "application/json");
21        
22        // Создаем отчет
23        DynamicJsonDocument report(1024);
24        report["classroomId"] = config.classroomId;
25        report["schoolId"] = config.schoolId;
26        report["timestamp"] = millis();
27        report["temperature"] = currentState.temperature;
28        report["light"] = currentState.light;
29        report["sound"] = currentState.sound;
30        report["motion"] = currentState.motion;
31        report["heaterOn"] = currentState.heaterOn;
32        report["lampOn"] = currentState.lampOn;
33        report["mode"] = currentState.mode;
34        report["lastDecision"] = currentState.lastDecision;
35        
36        String reportJson;
37        serializeJson(report, reportJson);
38        
39        int httpResponseCode = http.POST(reportJson);
40        
41        if (httpResponseCode == 200) {
42            String response = http.getString();
43            Serial.println("📡 Отчет отправлен в центральную систему");
44            logEvent("INFO", "Отчет отправлен в центральную систему");
45            
46            // Обработка команд от центральной системы
47            DynamicJsonDocument responseDoc(512);
48            deserializeJson(responseDoc, response);
49            
50            if (responseDoc["command"]) {
51                executeRemoteCommand(responseDoc["command"]);
52            }
53        } else {
54            Serial.println("❌ Ошибка отправки отчета: " + String(httpResponseCode));
55            logEvent("ERROR", "Ошибка отправки отчета в центральную систему");
56        }
57        
58        http.end();
59    }
60}
61
62void executeRemoteCommand(String command) {
63    logEvent("INFO", "Получена команда от центральной системы: " + command);
64    
65    if (command == "emergency_heating") {
66        // Экстренное включение отопления во всех классах
67        currentState.heaterOn = true;
68        digitalWrite(heaterPin, HIGH);
69        currentState.lastDecision = "Экстренное отопление по команде администрации";
70        
71    } else if (command == "energy_save_mode") {
72        // Энергосберегающий режим для всей школы
73        currentState.mode = "eco";
74        currentState.heaterOn = false;
75        currentState.lampOn = false;
76        digitalWrite(heaterPin, LOW);
77        digitalWrite(lampPin, LOW);
78        currentState.lastDecision = "Энергосберегающий режим по команде администрации";
79        
80    } else if (command == "security_lockdown") {
81        // Режим безопасности
82        isAuthenticated = false; // Блокируем локальное управление
83        currentState.mode = "security";
84        currentState.lastDecision = "Режим безопасности активирован";
85    }
86    
87    // Уведомляем пользователей через WebSocket
88    webSocket.broadcastTXT("📢 Система получила команду: " + command);
89}

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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