Compare commits

..

No commits in common. '758bc9524403a20df9b829a04482b15b7baf19f8' and '3b095016fe418aedaab8706a6a52456f0bd0d7b2' have entirely different histories.

  1. 2
      .gitignore
  2. 21
      README.md
  3. BIN
      data/css/styles.css.gz
  4. BIN
      data/index.html.gz
  5. BIN
      data/js/api.js.gz
  6. BIN
      data/js/app.js.gz
  7. BIN
      data/js/crypto-js.min.js.gz
  8. BIN
      data/login.html.gz
  9. BIN
      data/settings.html.gz
  10. BIN
      data/update.html.gz
  11. 24
      platformio.ini
  12. 80
      src/main.cpp
  13. 46
      src/server.cpp
  14. 2
      src/server.h

2
.gitignore vendored

@ -23,4 +23,4 @@ Thumbs.db @@ -23,4 +23,4 @@ Thumbs.db
# Build artifacts
*.bin
*.elf
*.map
*.map

21
README.md

@ -7,7 +7,6 @@ Squelette d'application web pour ESP32 avec interface de gestion via dashboard w @@ -7,7 +7,6 @@ Squelette d'application web pour ESP32 avec interface de gestion via dashboard w
- **Dashboard web responsive** - Interface utilisateur moderne et adaptative
- **Authentification sécurisée** - Système de login avec sessions et hash SHA-256
- **Configuration WiFi** - Mode Station et Point d'Accès automatique
- **Reconnexion WiFi automatique** - Détection de perte de connexion et reconnexion toutes les 30 secondes (WPA2-PSK et WPA2-PEAP)
- **WPA2-Enterprise PEAP** - Connexion PEAP (sans certificat) avec utilisateur/mot de passe (MSCHAPv2)
- **API REST** - Endpoints pour configuration, statistiques et changement de mot de passe
- **Gestion des mots de passe** - Changement sécurisé avec validation de l'ancien mot de passe
@ -90,13 +89,6 @@ platformio device monitor --environment esp32 @@ -90,13 +89,6 @@ platformio device monitor --environment esp32
Si la connexion WiFi échoue ou n'est pas configurée, l'ESP32 démarre automatiquement en mode Point d'Accès (AP).
### Reconnexion automatique
Lorsque la connexion WiFi STA est perdue, l'ESP32 tente automatiquement de se reconnecter toutes les **30 secondes** :
- **WPA2-PSK** : `WiFi.setAutoReconnect(true)` gère la reconnexion en premier lieu ; la vérification périodique sert de fallback
- **WPA2-PEAP** : les credentials entreprise (`esp_wifi_sta_wpa2_ent_*`) sont réappliqués manuellement à chaque tentative
- En cas de succès, l'AP de secours (`<hostname>-setup`) est restauré automatiquement
## 📱 Interface Web
### Pages disponibles
@ -259,7 +251,6 @@ Procédure recommandée (sans secret en clair dans les fichiers): @@ -259,7 +251,6 @@ Procédure recommandée (sans secret en clair dans les fichiers):
2. En mode WPA2-PEAP, vérifiez l'utilisateur/mot de passe PEAP
2. Assurez-vous que le réseau WiFi est en 2.4 GHz (l'ESP32 ne supporte pas le 5 GHz)
3. L'ESP32 passera automatiquement en mode AP si la connexion échoue
4. En cas de perte de connexion après démarrage, l'ESP32 retente automatiquement toutes les 30 secondes (visible dans les logs série : `WiFi déconnecté, tentative de reconnexion...`)
### Impossible de se connecter à l'interface web
@ -267,18 +258,6 @@ Procédure recommandée (sans secret en clair dans les fichiers): @@ -267,18 +258,6 @@ 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 `<hostname>-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://<ip_sta>/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

BIN
data/css/styles.css.gz

Binary file not shown.

BIN
data/index.html.gz

Binary file not shown.

BIN
data/js/api.js.gz

Binary file not shown.

BIN
data/js/app.js.gz

Binary file not shown.

BIN
data/js/crypto-js.min.js.gz

Binary file not shown.

BIN
data/login.html.gz

Binary file not shown.

BIN
data/settings.html.gz

Binary file not shown.

BIN
data/update.html.gz

Binary file not shown.

24
platformio.ini

@ -12,27 +12,7 @@ lib_deps = @@ -12,27 +12,7 @@ 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
; Configuration ArduinoOTA pour firmware et filesystem
;upload_protocol = espota
;upload_port = 192.168.0.xx
;upload_flags =
; --auth=password
upload_speed = 921600

80
src/main.cpp

@ -23,26 +23,6 @@ String wifiEapUsername = ""; @@ -23,26 +23,6 @@ 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");
@ -152,7 +132,7 @@ bool waitForWiFiConnection() { @@ -152,7 +132,7 @@ bool waitForWiFiConnection() {
if (WiFi.status() == WL_CONNECTED) {
logMessage(LOG_INFO, "WiFi connecté");
logNetworkInfo();
logMessage(LOG_INFO, "Adresse IP: " + WiFi.localIP().toString());
return true;
}
@ -166,11 +146,19 @@ bool connectWPA2PEAP() { @@ -166,11 +146,19 @@ bool connectWPA2PEAP() {
}
logMessage(LOG_INFO, "Connexion WPA2-PEAP au WiFi: " + wifiSSID);
logMessage(LOG_INFO, "Profil PEAP: sans certificat, domaine vide, MSCHAPv2 interne");
#if HAS_LEGACY_WPA2_ENTERPRISE
WiFi.disconnect(true, true);
delay(100);
#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());
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());
@ -181,19 +169,6 @@ bool connectWPA2PEAP() { @@ -181,19 +169,6 @@ 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");
@ -210,9 +185,6 @@ bool connectWPA2PSK() { @@ -210,9 +185,6 @@ 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;
@ -223,7 +195,6 @@ void setupWiFi() { @@ -223,7 +195,6 @@ void setupWiFi() {
}
if (connected) {
startFallbackAP();
return;
}
@ -300,34 +271,9 @@ void setup() { @@ -300,34 +271,9 @@ 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();

46
src/server.cpp

@ -8,23 +8,6 @@ WebServerManager::WebServerManager() { @@ -8,23 +8,6 @@ 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");
@ -71,25 +54,16 @@ void WebServerManager::sendJsonSuccess(AsyncWebServerRequest* request, const Str @@ -71,25 +54,16 @@ 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;
@ -98,7 +72,6 @@ void WebServerManager::setupRoutes() { @@ -98,7 +72,6 @@ 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;
@ -108,7 +81,6 @@ void WebServerManager::setupRoutes() { @@ -108,7 +81,6 @@ 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;
@ -118,7 +90,6 @@ void WebServerManager::setupRoutes() { @@ -118,7 +90,6 @@ 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;
@ -129,7 +100,6 @@ void WebServerManager::setupRoutes() { @@ -129,7 +100,6 @@ 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;
@ -150,8 +120,7 @@ void WebServerManager::setupRoutes() { @@ -150,8 +120,7 @@ void WebServerManager::setupRoutes() {
);
// 404
server->onNotFound([this](AsyncWebServerRequest* request) {
logRequest(request, "not_found");
server->onNotFound([](AsyncWebServerRequest* request) {
request->send(404, "text/plain", "Page non trouvée");
});
@ -159,19 +128,6 @@ void WebServerManager::setupRoutes() { @@ -159,19 +128,6 @@ 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");

2
src/server.h

@ -13,7 +13,6 @@ private: @@ -13,7 +13,6 @@ 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);
@ -29,7 +28,6 @@ public: @@ -29,7 +28,6 @@ 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);
};

Loading…
Cancel
Save