Давайте рассмотрим полностью рабочую систему с раздельными файлами. Суть системы кратко сводится к следующему. У нас есть устройство ESP32. В него заливается скетч, о нем далее. В итоге устройство через заданную в коде и пароль сеть WiFi обращается к URL файлы json.
Для того чтобы изменить логику на пинах ESP, мы переходим на организованную HTML страничку, меняем и сохраняем конфигурацию, это осуществляется посредством AJAX и отработки PHP. Записывается конфигурационный файл json.
В итоге ESP каждый установленный период времени прописанный в коде обращается к конфигурации, "забирает ее к себе", считывает и тем самым меняет логику на пинах.
Проект работает по ссылке https://smartmatter.ru/ESP32/index.html. То есть вы можете залить код в вашу ESP и используя страницу поуправлять устройством! Не забудьте в коде ESP поставить вашу Wifi сеть и пароль. Сами понимаете, что в один момент времени на сайте и на странице может находится несколько пользователей, что собственно будет являться неким хаосом, ведь все ESP, который будут стучаться на страницу, будут управляться опять же всеми кто сейчас здесь же, то есть будет полный бардак...
Если вам надо свой условно кабинет для управления ESP, то можно завести логин и уже там по паролю заходить условно в кабинет и управлять своим ESP
https://smartmatter.ru/ESP32/auth.php
Если вам нужен "защищенный"
const char* ssid = "YOUR_WIFI_SSID";
const char* password = "YOUR_WIFI_PASS";
...по факту все серверные траблы здесь уже реализованы, за счет нашего сайта!
Если же вы хотите создать свой обособленный проект, то надо будет осуществить следующие действия.
Итак:
1. Создайте файл api.php для обработки запросов
Файл: api.php
<?php // Включим CORS заголовки header('Access-Control-Allow-Origin: *'); header('Access-Control-Allow-Methods: GET, POST, OPTIONS'); header('Access-Control-Allow-Headers: Content-Type'); header('Content-Type: application/json'); // Если это предварительный запрос OPTIONS, просто завершаем if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; } // Путь к файлу конфигурации $config_file = 'esp32_config.json'; // Функция для чтения конфигурации function read_config() { global $config_file; if (!file_exists($config_file)) { // Создаем дефолтную конфигурацию $default_config = [ 'timestamp' => time(), 'device' => 'ESP32 Controller', 'version' => '1.0', 'pins' => [ ['pin' => 2, 'state' => 'LOW', 'name' => 'Светодиод 1'], ['pin' => 4, 'state' => 'LOW', 'name' => 'Реле 1'], ['pin' => 5, 'state' => 'LOW', 'name' => 'Реле 2'], ['pin' => 12, 'state' => 'LOW', 'name' => 'Мотор'], ['pin' => 13, 'state' => 'LOW', 'name' => 'Насос'], ['pin' => 14, 'state' => 'LOW', 'name' => 'Вентилятор'], ['pin' => 15, 'state' => 'LOW', 'name' => 'Резерв 1'], ['pin' => 18, 'state' => 'LOW', 'name' => 'Резерв 2'] ] ]; file_put_contents($config_file, json_encode($default_config, JSON_PRETTY_PRINT)); return $default_config; } $content = file_get_contents($config_file); return json_decode($content, true); } // Функция для сохранения конфигурации function save_config($config) { global $config_file; $config['timestamp'] = time(); $result = file_put_contents($config_file, json_encode($config, JSON_PRETTY_PRINT)); return $result !== false; } // Обработка GET запроса - вернуть конфигурацию if ($_SERVER['REQUEST_METHOD'] === 'GET') { $config = read_config(); echo json_encode($config); exit; } // Обработка POST запроса - сохранить конфигурацию if ($_SERVER['REQUEST_METHOD'] === 'POST') { // Получаем сырые POST данные $input = file_get_contents('php://input'); $data = json_decode($input, true); if ($data === null) { http_response_code(400); echo json_encode(['error' => 'Invalid JSON data']); exit; } // Проверяем наличие необходимых полей if (!isset($data['config']) || !is_array($data['config'])) { http_response_code(400); echo json_encode(['error' => 'Missing config data']); exit; } // Сохраняем конфигурацию $success = save_config($data['config']); if ($success) { echo json_encode([ 'success' => true, 'message' => 'Configuration saved successfully', 'timestamp' => time() ]); } else { http_response_code(500); echo json_encode(['error' => 'Failed to save configuration']); } exit; } // Если метод не поддерживается http_response_code(405); echo json_encode(['error' => 'Method not allowed']); ?>
2. Создайте простой HTML интерфейс
Файл: index.html
<!DOCTYPE html> <html lang="ru"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Управление ESP32</title> <style> body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; background-color: #f5f5f5; } .container { background: white; padding: 20px; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); } h1 { color: #333; text-align: center; } .pins-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 15px; margin: 20px 0; } .pin-card { background: #f8f9fa; padding: 15px; border-radius: 8px; border-left: 4px solid #007bff; } .pin-number { font-size: 18px; font-weight: bold; color: #007bff; } .status { margin: 10px 0; font-weight: bold; } .status.on { color: #28a745; } .status.off { color: #dc3545; } .controls button { margin: 5px; padding: 8px 15px; border: none; border-radius: 4px; cursor: pointer; } .btn-on { background: #28a745; color: white; } .btn-off { background: #dc3545; color: white; } .controls-bottom { margin-top: 20px; display: flex; gap: 10px; flex-wrap: wrap; } .btn { padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; } .btn-primary { background: #007bff; color: white; } .btn-success { background: #28a745; color: white; } .btn-warning { background: #ffc107; color: black; } .status-message { padding: 10px; margin: 10px 0; border-radius: 5px; display: none; } .success { background: #d4edda; color: #155724; border: 1px solid #c3e6cb; } .error { background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; } pre { background: #2d2d2d; color: #f8f8f2; padding: 15px; border-radius: 5px; overflow-x: auto; } </style> </head> <body> <div class="container"> <h1>Управление ESP32</h1> <div class="status-message" id="statusMessage"></div> <div class="pins-grid" id="pinsContainer"> <!-- Пины будут сгенерированы JavaScript --> </div> <div class="controls-bottom"> <button class="btn btn-primary" onclick="loadConfig()">🔄 Загрузить с сервера</button> <button class="btn btn-success" onclick="saveConfig()">💾 Сохранить на сервер</button> <button class="btn" onclick="resetConfig()" style="background: #6c757d; color: white;">🔄 Сбросить все</button> <button class="btn btn-warning" onclick="copyJson()">📋 Копировать JSON</button> </div> <div style="margin-top: 30px;"> <h3>JSON конфигурация:</h3> <pre id="jsonOutput">Загрузка...</pre> <p>URL для ESP32: <code id="jsonUrl"></code></p> </div> </div> <script> // Конфигурация пинов const pinsConfig = [ { pin: 2, name: "Светодиод 1", defaultState: "LOW" }, { pin: 4, name: "Реле 1", defaultState: "LOW" }, { pin: 5, name: "Реле 2", defaultState: "LOW" }, { pin: 12, name: "Мотор", defaultState: "LOW" }, { pin: 13, name: "Насос", defaultState: "LOW" }, { pin: 14, name: "Вентилятор", defaultState: "LOW" }, { pin: 15, name: "Резерв 1", defaultState: "LOW" }, { pin: 18, name: "Резерв 2", defaultState: "LOW" } ]; let pinStates = {}; const apiUrl = 'api.php'; // URL к API const jsonFileUrl = window.location.pathname.replace('index.html', 'esp32_config.json'); // Инициализация function init() { // Устанавливаем URL для ESP32 document.getElementById('jsonUrl').textContent = window.location.origin + jsonFileUrl; // Загружаем конфигурацию с сервера loadConfig(); } // Загрузить конфигурацию с сервера function loadConfig() { showStatus('Загрузка конфигурации...', 'info'); fetch(apiUrl) .then(response => { if (!response.ok) { throw new Error('HTTP error: ' + response.status); } return response.json(); }) .then(data => { // Заполняем состояния пинов из сервера if (data.pins && Array.isArray(data.pins)) { data.pins.forEach(pin => { pinStates[pin.pin] = pin.state; }); } // Если данных нет, используем значения по умолчанию pinsConfig.forEach(pin => { if (!pinStates.hasOwnProperty(pin.pin)) { pinStates[pin.pin] = pin.defaultState; } }); renderPins(); updateJson(); showStatus('Конфигурация загружена с сервера', 'success'); }) .catch(error => { console.error('Ошибка загрузки:', error); // Используем значения по умолчанию pinsConfig.forEach(pin => { pinStates[pin.pin] = pin.defaultState; }); renderPins(); updateJson(); showStatus('Используются значения по умолчанию', 'warning'); }); } // Сохранить конфигурацию на сервер function saveConfig() { const config = getCurrentConfig(); showStatus('Сохранение конфигурации...', 'info'); fetch(apiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ config: config }) }) .then(response => { if (!response.ok) { return response.json().then(data => { throw new Error(data.error || 'HTTP error: ' + response.status); }); } return response.json(); }) .then(data => { if (data.success) { showStatus('Конфигурация успешно сохранена на сервере!', 'success'); updateJson(); } else { throw new Error(data.error || 'Unknown error'); } }) .catch(error => { console.error('Ошибка сохранения:', error); showStatus('Ошибка сохранения: ' + error.message, 'error'); }); } // Получить текущую конфигурацию function getCurrentConfig() { const pins = []; pinsConfig.forEach(pin => { pins.push({ pin: pin.pin, state: pinStates[pin.pin] || pin.defaultState, name: pin.name }); }); return { timestamp: new Date().getTime(), device: 'ESP32 Controller', version: '1.0', pins: pins }; } // Отобразить пины function renderPins() { const container = document.getElementById('pinsContainer'); container.innerHTML = ''; pinsConfig.forEach(pin => { const state = pinStates[pin.pin] || pin.defaultState; const card = document.createElement('div'); card.className = 'pin-card'; card.innerHTML = ` <div class="pin-number">GPIO ${pin.pin}</div> <div>${pin.name}</div> <div class="status ${state === 'HIGH' ? 'on' : 'off'}"> Статус: ${state === 'HIGH' ? 'ВКЛ' : 'ВЫКЛ'} </div> <div class="controls"> <button class="btn-on" onclick="setPinState(${pin.pin}, 'HIGH')">Включить</button> <button class="btn-off" onclick="setPinState(${pin.pin}, 'LOW')">Выключить</button> </div> `; container.appendChild(card); }); } // Установить состояние пина function setPinState(pin, state) { pinStates[pin] = state; renderPins(); updateJson(); showStatus(`Пин ${pin} установлен в ${state}`, 'success'); } // Обновить JSON отображение function updateJson() { const config = getCurrentConfig(); document.getElementById('jsonOutput').textContent = JSON.stringify(config, null, 2); } // Сбросить конфигурацию function resetConfig() { if (confirm('Сбросить все пины в состояние LOW?')) { pinsConfig.forEach(pin => { pinStates[pin.pin] = 'LOW'; }); renderPins(); updateJson(); showStatus('Все пины сброшены', 'success'); } } // Копировать JSON function copyJson() { const jsonText = document.getElementById('jsonOutput').textContent; navigator.clipboard.writeText(jsonText) .then(() => { showStatus('JSON скопирован в буфер обмена!', 'success'); }) .catch(err => { showStatus('Ошибка копирования: ' + err, 'error'); }); } // Показать статус function showStatus(message, type) { const status = document.getElementById('statusMessage'); status.textContent = message; status.className = `status-message ${type}`; status.style.display = 'block'; setTimeout(() => { status.style.display = 'none'; }, 3000); } // Инициализация при загрузке document.addEventListener('DOMContentLoaded', init); </script> </body> </html>
3. Код для ESP32 (HTTP версия)
Файл: esp32_controller.ino
#include <WiFi.h> #include <HTTPClient.h> #include <ArduinoJson.h> const char* ssid = "YOUR_WIFI_SSID"; const char* password = "YOUR_WIFI_PASS"; // URL к вашему JSON файлу на сервере const char* configUrl = "http://smartmatter.ru/ESP32/esp32_config.json"; // Пины для управления const int pins[] = {2, 4, 5, 12, 13, 14, 15, 18}; const int pinCount = sizeof(pins) / sizeof(pins[0]); // Для отладки unsigned long lastUpdateTime = 0; const long updateInterval = 10000; // 10 секунд void setup() { Serial.begin(115200); delay(3000); Serial.println("\n=== ESP32 Контроллер ==="); // Инициализация пинов for (int i = 0; i < pinCount; i++) { pinMode(pins[i], OUTPUT); digitalWrite(pins[i], LOW); Serial.printf("Пин %d: OUTPUT, LOW\n", pins[i]); } // Подключение к WiFi Serial.print("Подключение к WiFi..."); WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) { delay(1000); Serial.print("."); } Serial.println("\nWiFi подключен!"); Serial.print("IP адрес: "); Serial.println(WiFi.localIP()); // Первое обновление updateFromServer(); } void loop() { // Проверяем WiFi соединение if (WiFi.status() != WL_CONNECTED) { Serial.println("WiFi отключен, переподключение..."); WiFi.reconnect(); delay(5000); return; } // Периодическое обновление конфигурации if (millis() - lastUpdateTime >= updateInterval) { updateFromServer(); lastUpdateTime = millis(); } delay(1000); } void updateFromServer() { Serial.println("\n--- Запрос конфигурации с сервера ---"); Serial.print("URL: "); Serial.println(configUrl); HTTPClient http; http.begin(configUrl); http.setTimeout(10000); int httpCode = http.GET(); Serial.print("HTTP код: "); Serial.println(httpCode); if (httpCode == HTTP_CODE_OK) { String payload = http.getString(); // Парсим JSON DynamicJsonDocument doc(2048); DeserializationError error = deserializeJson(doc, payload); if (!error) { JsonArray pinsArray = doc["pins"]; bool changesMade = false; Serial.println("Применяем конфигурацию:"); for (JsonObject pinObj : pinsArray) { int pinNumber = pinObj["pin"]; const char* state = pinObj["state"]; const char* name = pinObj["name"] | "Unknown"; // Ищем пин в массиве bool pinFound = false; for (int i = 0; i < pinCount; i++) { if (pins[i] == pinNumber) { pinFound = true; // Определяем текущее состояние int currentState = digitalRead(pinNumber); bool shouldBeHigh = (strcmp(state, "HIGH") == 0); // Применяем, если нужно изменить if ((shouldBeHigh && currentState == LOW) || (!shouldBeHigh && currentState == HIGH)) { digitalWrite(pinNumber, shouldBeHigh ? HIGH : LOW); changesMade = true; Serial.printf(" Пин %d (%s): %s -> %s\n", pinNumber, name, currentState == HIGH ? "HIGH" : "LOW", shouldBeHigh ? "HIGH" : "LOW" ); } else { Serial.printf(" Пин %d (%s): %s (без изменений)\n", pinNumber, name, shouldBeHigh ? "HIGH" : "LOW" ); } break; } } if (!pinFound) { Serial.printf(" Пин %d (%s): не настроен в коде ESP32\n", pinNumber, name); } } if (changesMade) { Serial.println("✓ Изменения применены"); } else { Serial.println("ℹ Изменений не требуется"); } // Выводим timestamp if (doc.containsKey("timestamp")) { unsigned long timestamp = doc["timestamp"]; Serial.printf("Конфигурация от: %lu\n", timestamp); } } else { Serial.print("✗ Ошибка парсинга JSON: "); Serial.println(error.c_str()); } } else { Serial.print("✗ Ошибка HTTP: "); Serial.println(http.errorToString(httpCode)); } http.end(); // Вычисляем время до следующего обновления unsigned long nextUpdate = updateInterval - (millis() - lastUpdateTime); if (nextUpdate > updateInterval) nextUpdate = 0; Serial.printf("Следующее обновление через: %lu секунд\n", nextUpdate / 1000); Serial.println("------------------------\n"); }
4. Структура файлов на сервере
У вас должно быть 3 файла в папке ESP32:
/var/www/admin/data/www/smartmatter.ru/ESP32/ ├── api.php (обработчик запросов) ├── index.html (веб-интерфейс) └── esp32_config.json (конфигурационный файл, создается автоматически)
По итого имеем примитивное устройство, которым можно управлять с любой точки мира используя доступ к сайте, если ESP также имеет доступ к нему!