diff --git a/.gitignore b/.gitignore index f7f92e3..3e594b0 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,4 @@ Thumbs.db # Build artifacts *.bin *.elf -*.map +*.map \ No newline at end of file diff --git a/README.md b/README.md index 3b70e74..cf80be1 100644 --- a/README.md +++ b/README.md @@ -258,6 +258,18 @@ Procédure recommandée (sans secret en clair dans les fichiers): 2. En mode AP, l'adresse est toujours `192.168.4.1` 3. En mode Station, trouvez l'IP dans les logs du moniteur série 4. Désactivez le cache du navigateur (Ctrl+F5) +5. En réseau entreprise (PEAP), un filtrage « client isolation » peut bloquer ping/HTTP entre clients WiFi +6. Utilisez l'AP de secours `ESP32-Webapp-setup` (ou `-setup`) avec l'IP `192.168.4.1` + +### Diagnostic rapide réseau (2 minutes) + +1. Ouvrez le moniteur série et vérifiez `Serveur web démarré sur le port 80` +2. Depuis votre PC, testez l'endpoint de santé: + - en STA: `curl -v http:///api/health` + - en AP de secours: `curl -v http://192.168.4.1/api/health` +3. Vérifiez les logs série: + - si vous voyez `HTTP GET /api/health depuis ...`, le trafic arrive bien + - si rien n'apparaît, le réseau filtre la communication vers l'ESP32 ### Erreur lors de la mise à jour OTA diff --git a/data/css/styles.css.gz b/data/css/styles.css.gz deleted file mode 100644 index abef4f7..0000000 Binary files a/data/css/styles.css.gz and /dev/null differ diff --git a/data/index.html.gz b/data/index.html.gz deleted file mode 100644 index 33b346f..0000000 Binary files a/data/index.html.gz and /dev/null differ diff --git a/data/js/api.js.gz b/data/js/api.js.gz deleted file mode 100644 index d8dc0d8..0000000 Binary files a/data/js/api.js.gz and /dev/null differ diff --git a/data/js/app.js.gz b/data/js/app.js.gz deleted file mode 100644 index c841169..0000000 Binary files a/data/js/app.js.gz and /dev/null differ diff --git a/data/js/crypto-js.min.js.gz b/data/js/crypto-js.min.js.gz deleted file mode 100644 index 0222474..0000000 Binary files a/data/js/crypto-js.min.js.gz and /dev/null differ diff --git a/data/login.html.gz b/data/login.html.gz deleted file mode 100644 index 884711b..0000000 Binary files a/data/login.html.gz and /dev/null differ diff --git a/data/settings.html.gz b/data/settings.html.gz deleted file mode 100644 index 1796723..0000000 Binary files a/data/settings.html.gz and /dev/null differ diff --git a/data/update.html.gz b/data/update.html.gz deleted file mode 100644 index ec683db..0000000 Binary files a/data/update.html.gz and /dev/null differ diff --git a/platformio.ini b/platformio.ini index 69f41ae..1916328 100644 --- a/platformio.ini +++ b/platformio.ini @@ -12,7 +12,27 @@ lib_deps = ESP32Async/AsyncTCP bblanchon/ArduinoJson@^7.0.0 +#build_flags = +# -DBOARD_HAS_PSRAM +upload_speed = 921600 + +[env:esp32-s3-supermini] +platform = espressif32 +board = esp32-s3-devkitc-1 +framework = arduino +board_upload.flash_size = 4MB +board_build.filesystem = littlefs +board_build.partitions = default.csv +monitor_speed = 115200 +lib_deps = + ESP32Async/ESPAsyncWebServer + ESP32Async/AsyncTCP + bblanchon/ArduinoJson@^7.0.0 build_flags = + -DARDUINO_USB_CDC_ON_BOOT=1 -DBOARD_HAS_PSRAM - -upload_speed = 921600 +; Configuration ArduinoOTA pour firmware et filesystem +;upload_protocol = espota +;upload_port = 192.168.0.xx +;upload_flags = +; --auth=password \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index b4d87c1..1bcb327 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -23,6 +23,26 @@ String wifiEapUsername = ""; String wifiEapPassword = ""; String apPassword = "webapp123"; +void logNetworkInfo() { + logMessage(LOG_INFO, "Adresse IP: " + WiFi.localIP().toString()); + logMessage(LOG_INFO, "Passerelle: " + WiFi.gatewayIP().toString()); + logMessage(LOG_INFO, "Masque: " + WiFi.subnetMask().toString()); + logMessage(LOG_INFO, "DNS 1: " + WiFi.dnsIP(0).toString()); + logMessage(LOG_INFO, "DNS 2: " + WiFi.dnsIP(1).toString()); + logMessage(LOG_INFO, "MAC STA: " + WiFi.macAddress()); +} + +void startFallbackAP() { + String fallbackSsid = hostname + "-setup"; + WiFi.mode(WIFI_AP_STA); + if (WiFi.softAP(fallbackSsid.c_str(), apPassword.c_str())) { + logMessage(LOG_INFO, "AP de secours actif: " + fallbackSsid); + logMessage(LOG_INFO, "IP AP de secours: " + WiFi.softAPIP().toString()); + } else { + logMessage(LOG_WARN, "Échec du démarrage de l'AP de secours"); + } +} + void createDefaultConfig() { logMessage(LOG_INFO, "Création de la configuration par défaut"); @@ -132,7 +152,7 @@ bool waitForWiFiConnection() { if (WiFi.status() == WL_CONNECTED) { logMessage(LOG_INFO, "WiFi connecté"); - logMessage(LOG_INFO, "Adresse IP: " + WiFi.localIP().toString()); + logNetworkInfo(); return true; } @@ -146,19 +166,11 @@ bool connectWPA2PEAP() { } logMessage(LOG_INFO, "Connexion WPA2-PEAP au WiFi: " + wifiSSID); + logMessage(LOG_INFO, "Profil PEAP: sans certificat, domaine vide, MSCHAPv2 interne"); -#if defined(WPA2_AUTH_PEAP) - WiFi.begin( - wifiSSID.c_str(), - WPA2_AUTH_PEAP, - wifiEapUsername.c_str(), - wifiEapUsername.c_str(), - wifiEapPassword.c_str(), - nullptr - ); - return waitForWiFiConnection(); -#elif HAS_LEGACY_WPA2_ENTERPRISE - WiFi.begin(wifiSSID.c_str()); +#if HAS_LEGACY_WPA2_ENTERPRISE + WiFi.disconnect(true, true); + delay(100); esp_wifi_sta_wpa2_ent_set_identity((uint8_t*)wifiEapUsername.c_str(), wifiEapUsername.length()); esp_wifi_sta_wpa2_ent_set_username((uint8_t*)wifiEapUsername.c_str(), wifiEapUsername.length()); @@ -169,6 +181,19 @@ bool connectWPA2PEAP() { #endif esp_wifi_sta_wpa2_ent_enable(); + WiFi.begin(wifiSSID.c_str()); + return waitForWiFiConnection(); +#elif defined(WPA2_AUTH_PEAP) + WiFi.disconnect(true, true); + delay(100); + WiFi.begin( + wifiSSID.c_str(), + WPA2_AUTH_PEAP, + wifiEapUsername.c_str(), + wifiEapUsername.c_str(), + wifiEapPassword.c_str(), + nullptr + ); return waitForWiFiConnection(); #else logMessage(LOG_ERROR, "WPA2-PEAP non supporté par cette version du framework"); @@ -185,6 +210,9 @@ bool connectWPA2PSK() { void setupWiFi() { if (wifiSSID.length() > 0) { WiFi.mode(WIFI_STA); + WiFi.setSleep(false); + WiFi.setAutoReconnect(true); + WiFi.persistent(false); WiFi.setHostname(hostname.c_str()); bool connected = false; @@ -195,6 +223,7 @@ void setupWiFi() { } if (connected) { + startFallbackAP(); return; } @@ -271,9 +300,34 @@ void setup() { } unsigned long lastCleanup = 0; +static unsigned long lastReconnectAttempt = 0; +static const unsigned long RECONNECT_INTERVAL = 30000; + +void checkWiFiConnection() { + if (wifiSSID.isEmpty()) return; + if (WiFi.status() == WL_CONNECTED) return; + + unsigned long now = millis(); + if (now - lastReconnectAttempt < RECONNECT_INTERVAL) return; + lastReconnectAttempt = now; + + logMessage(LOG_WARN, "WiFi déconnecté, tentative de reconnexion..."); + WiFi.mode(WIFI_STA); + + bool connected = (wifiSecurity == "wpa2_peap") ? connectWPA2PEAP() : connectWPA2PSK(); + + if (connected) { + logMessage(LOG_INFO, "WiFi reconnecté"); + startFallbackAP(); + } else { + logMessage(LOG_WARN, "Échec de reconnexion WiFi"); + } +} void loop() { ArduinoOTA.handle(); + + checkWiFiConnection(); // Nettoyer les sessions expirées toutes les minutes unsigned long now = millis(); diff --git a/src/server.cpp b/src/server.cpp index 1307b92..9917d18 100644 --- a/src/server.cpp +++ b/src/server.cpp @@ -8,6 +8,23 @@ WebServerManager::WebServerManager() { server = new AsyncWebServer(80); } +void WebServerManager::logRequest(AsyncWebServerRequest* request, const String& routeTag) { + String method = "OTHER"; + if (request->method() == HTTP_GET) method = "GET"; + else if (request->method() == HTTP_POST) method = "POST"; + else if (request->method() == HTTP_PUT) method = "PUT"; + else if (request->method() == HTTP_DELETE) method = "DELETE"; + else if (request->method() == HTTP_OPTIONS) method = "OPTIONS"; + else if (request->method() == HTTP_PATCH) method = "PATCH"; + + String remoteIp = "unknown"; + if (request->client()) { + remoteIp = request->client()->remoteIP().toString(); + } + + logMessage(LOG_INFO, "HTTP " + method + " " + request->url() + " depuis " + remoteIp + " (" + routeTag + ")"); +} + String WebServerManager::getSessionIdFromRequest(AsyncWebServerRequest* request) { if (request->hasHeader("Cookie")) { String cookie = request->header("Cookie"); @@ -54,16 +71,25 @@ void WebServerManager::sendJsonSuccess(AsyncWebServerRequest* request, const Str void WebServerManager::setupRoutes() { // Route de login server->on("/login", HTTP_POST, [this](AsyncWebServerRequest* request) { + logRequest(request, "login"); this->handleLogin(request); }); + + // Endpoint de diagnostic réseau (sans authentification) + server->on("/api/health", HTTP_GET, [this](AsyncWebServerRequest* request) { + logRequest(request, "health"); + this->handleHealth(request); + }); // Route de logout server->on("/logout", HTTP_GET, [this](AsyncWebServerRequest* request) { + logRequest(request, "logout"); this->handleLogout(request); }); // API - Configuration server->on("/api/config", HTTP_GET, [this](AsyncWebServerRequest* request) { + logRequest(request, "get_config"); if (!isAuthenticated(request)) { sendJsonError(request, 401, "Non authentifié"); return; @@ -72,6 +98,7 @@ void WebServerManager::setupRoutes() { }); server->on("/api/config", HTTP_POST, [this](AsyncWebServerRequest* request) { + logRequest(request, "set_config"); if (!isAuthenticated(request)) { sendJsonError(request, 401, "Non authentifié"); return; @@ -81,6 +108,7 @@ void WebServerManager::setupRoutes() { // API - Changement de mot de passe server->on("/api/password", HTTP_POST, [this](AsyncWebServerRequest* request) { + logRequest(request, "change_password"); if (!isAuthenticated(request)) { sendJsonError(request, 401, "Non authentifié"); return; @@ -90,6 +118,7 @@ void WebServerManager::setupRoutes() { // API - Statistiques server->on("/api/stats", HTTP_GET, [this](AsyncWebServerRequest* request) { + logRequest(request, "get_stats"); if (!isAuthenticated(request)) { sendJsonError(request, 401, "Non authentifié"); return; @@ -100,6 +129,7 @@ void WebServerManager::setupRoutes() { // OTA Update server->on("/api/update", HTTP_POST, [this](AsyncWebServerRequest* request) { + logRequest(request, "ota_update"); if (!isAuthenticated(request)) { sendJsonError(request, 401, "Non authentifié"); return; @@ -120,7 +150,8 @@ void WebServerManager::setupRoutes() { ); // 404 - server->onNotFound([](AsyncWebServerRequest* request) { + server->onNotFound([this](AsyncWebServerRequest* request) { + logRequest(request, "not_found"); request->send(404, "text/plain", "Page non trouvée"); }); @@ -128,6 +159,19 @@ void WebServerManager::setupRoutes() { server->serveStatic("/", LittleFS, "/").setDefaultFile("login.html"); } +void WebServerManager::handleHealth(AsyncWebServerRequest* request) { + JsonDocument doc; + doc["ok"] = true; + doc["ip_sta"] = WiFi.localIP().toString(); + doc["ip_ap"] = WiFi.softAPIP().toString(); + doc["wifi_status"] = WiFi.status(); + doc["ssid"] = WiFi.SSID(); + + String response; + serializeJson(doc, response); + request->send(200, "application/json", response); +} + void WebServerManager::handleLogin(AsyncWebServerRequest* request) { if (!request->hasParam("username", true) || !request->hasParam("password", true)) { sendJsonError(request, 400, "Paramètres manquants"); diff --git a/src/server.h b/src/server.h index f665a92..bb430c3 100644 --- a/src/server.h +++ b/src/server.h @@ -13,6 +13,7 @@ private: AsyncWebServer* server; String getSessionIdFromRequest(AsyncWebServerRequest* request); + void logRequest(AsyncWebServerRequest* request, const String& routeTag); bool isAuthenticated(AsyncWebServerRequest* request); void sendJsonError(AsyncWebServerRequest* request, int code, const String& message); void sendJsonSuccess(AsyncWebServerRequest* request, const String& message); @@ -28,6 +29,7 @@ public: void handleGetConfig(AsyncWebServerRequest* request); void handleSetConfig(AsyncWebServerRequest* request); void handleChangePassword(AsyncWebServerRequest* request); + void handleHealth(AsyncWebServerRequest* request); void handleGetStats(AsyncWebServerRequest* request); void handleOTAUpdate(AsyncWebServerRequest* request, String filename, size_t index, uint8_t* data, size_t len, bool final); };