Давайте рассмотрим полностью рабочую систему с раздельными файлами. Суть системы кратко сводится к следующему. У нас есть устройство ESP32. В него заливается скетч, о нем далее. В итоге устройство через заданную в коде и пароль сеть WiFi обращается к URL файлы json.
 Для того чтобы изменить логику на пинах ESP, мы переходим на организованную HTML страничку, меняем и сохраняем конфигурацию, это осуществляется посредством AJAX и отработки PHP. Записывается конфигурационный файл json.
 В итоге ESP каждый установленный период времени прописанный в коде обращается к конфигурации, "забирает ее к себе", считывает и тем самым меняет логику на пинах. 

Проект работает по ссылке https://smartmatter.ru/ESP32/index.html. То есть вы можете залить код в вашу ESP и используя страницу поуправлять устройством! Не забудьте в коде ESP поставить вашу Wifi сеть и пароль.

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

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 также имеет доступ к нему!