🌐 ESP32 КАК СОБСТВЕННЫЙ ВЕБ-СЕРВЕР
БЫЛО: Умная система работает локально (только для тех, кто рядом)
СТАЛО: Умная система доступна всему миру через интернет
Ключевая идея: Дети превращают свой ESP32 в “цифрового посла” своей умной системы, который может общаться с любым человеком на планете.
- Локальное → Глобальное: Система становится частью всемирной сети
- Устройство → Сервис: ESP32 не просто “железка”, а сервер
- Код → Веб-приложение: Программа становится сайтом
- Понимание “клиент-серверной архитектуры” как основы интернета
- Осознание “протоколов общения” между устройствами
- Введение понятия “API” как языка машин
- Понимание “распределенных систем” - когда мозг и тело в разных местах
- HTTP протокол и веб-сервер на ESP32
- HTML/CSS как язык интерфейсов
- RESTful API для взаимодействия с датчиками
- WebSocket для real-time коммуникации
- “Архитектурное мышление” - как построить масштабируемую систему
- “Интерфейсное мышление” - как люди и машины общаются
- “Сетевое мышление” - понимание распределенности
Длительность: 90 минут
Метод: Историческо-философский анализ
Ключевая история: “Эволюция общения”
Пещерные люди → Рисунки на стенах → Письма → Телефон → Интернет → IoT
Сократический диалог:
- “Как бы вы рассказали другу в Японии о температуре в нашем классе?”
- “Что если ваша умная система могла бы ‘отвечать на вопросы’ людей из других стран?”
- “Чем веб-сайт отличается от программы на компьютере?”
Практическое упражнение: “Человеческий веб-сервер”
- Один ученик = “сервер” (знает информацию о классе)
- Другие = “клиенты” (задают вопросы)
- Формат: “GET /temperature” → “22.5°C”
- Открытие: “Веб-сервер = программа, которая отвечает на вопросы по сети”
Метод: Анатомический анализ
Биологическая аналогия: 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 превращается в “мини-сайт”
- Могут зайти на него с телефона/планшета
- Волшебный момент: “Наше устройство стало частью интернета!”
Концепция: “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
- “Какая температура?” →
Метод: User Experience Design
Вопросы для дизайна:
- “Кто будет пользоваться нашим сайтом?” (учителя, ученики, родители)
- “Что им нужно узнать?” (температура, освещение, безопасность)
- “Как сделать это красиво и понятно?”
Скетчинг интерфейса: Дети рисуют, как должен выглядеть их “сайт умной системы”
Длительность: 90 минут
Концепция: “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 добавляет “живость” и интерактивность
Концепция: “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>
)";
}
Метод: User Testing
Практическое тестирование:
- Дети подключаются к веб-серверу ESP32 с разных устройств
- Тестируют все кнопки и функции
- Наблюдают real-time обновление данных
- Проверяют отзывчивость интерфейса
Педагогические наблюдения:
- “Наша система теперь доступна всему миру!”
- “Мы можем управлять классом из любой точки школы!”
- “Система работает как настоящий сайт!”
Длительность: 90 минут
Концепция: “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
Концепция: “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 методы = что мы хотим сделать”
Концепция: “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);
}
Длительность: 90 минут
Концепция: “С большой силой приходит большая ответственность”
Этические вопросы:
- “Кто должен иметь доступ к нашей системе?”
- “Что если кто-то злой получит управление нашим классом?”
- “Как защитить личные данные учеников?”
Практические меры безопасности:
// Простая аутентификация
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;
}
Концепция: “Система должна помнить всё”
Система логирования:
#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\": \"Не удалось прочитать логи\"}");
}
}
Концепция: “От одного класса к умной школе”
Архитектура масштабируемой системы:
Уровень 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);
}
✅ Понимание веб-архитектуры - клиент-сервер модель
✅ API как язык машин - стандартизированное взаимодействие
✅ Real-time коммуникация - WebSocket vs HTTP
✅ Безопасность IoT - аутентификация и авторизация
✅ Масштабируемость - от устройства к системе систем
✅ Создание веб-сервера на микроконтроллере
✅ RESTful API проектирование и реализация
✅ HTML/CSS/JavaScript для IoT интерфейсов
✅ WebSocket для real-time приложений
✅ Основы безопасности и логирования
✅ Архитектурное мышление - как спроектировать систему
✅ Пользовательский опыт - дизайн интерфейсов
✅ Системная безопасность - защита от угроз
✅ Масштабное мышление - от прототипа к продукту
“Наша система умеет отвечать на вопросы, но как сделать общение с ней ещё более живым и интуитивным?”
- ✅ Веб-технологии как основа современных интерфейсов
- ✅ API как стандарт взаимодействия систем
- ✅ Real-time коммуникация для интерактивности
- ✅ Безопасность как обязательный компонент
Дети больше не видят ESP32 как “железку” - теперь это “цифровой амбассадор” их умной системы, который может общаться со всем миром!
Спринт 17 завершен! 🌐
Дети превратили локальную умную систему в глобально доступный веб-сервис!