🌐 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
Практическое тестирование:
- Дети подключаются к веб-серверу ESP32 с разных устройств
- Тестируют все кнопки и функции
- Наблюдают real-time обновление данных
- Проверяют отзывчивость интерфейса
Педагогические наблюдения:
- “Наша система теперь доступна всему миру!”
- “Мы можем управлять классом из любой точки школы!”
- “Система работает как настоящий сайт!”
Занятие 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 завершен! 🌐
Дети превратили локальную умную систему в глобально доступный веб-сервис!