feat: gestion WiFi — NVS credentials + scan + UI Gruvbox
- network.h/.cpp : lecture credentials NVS (Preferences) au boot,
fallback config.h si vide ; network_get_ssid() et
network_save_credentials() exposés
- web_server.cpp : 3 nouvelles routes REST
GET /api/wifi/current → SSID/IP/RSSI/modeAP
GET /api/wifi/networks → scan async + polling état
POST /api/wifi/connect → sauvegarde NVS + ESP.restart()
- index.html : modal WiFi (réseau actuel, liste scannée avec
barres signal ▁▂▃▄▅ + cadenas, formulaire SSID/mdp,
bouton œil, message redémarrage avec lien esp_jardin.local)
design Gruvbox Seventies cohérent avec le reste
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+346
@@ -237,6 +237,99 @@ input.fi:focus{border-color:var(--accent)}
|
||||
:root[data-theme="dark"] *::-webkit-scrollbar{width:6px;height:6px}
|
||||
:root[data-theme="dark"] *::-webkit-scrollbar-track{background:var(--bg-2)}
|
||||
:root[data-theme="dark"] *::-webkit-scrollbar-thumb{background:var(--bg-4);border-radius:3px}
|
||||
/* ── Modal WiFi ── */
|
||||
.wifi-modal-backdrop{
|
||||
display:none;position:fixed;inset:0;
|
||||
background:rgba(0,0,0,.7);z-index:200;
|
||||
align-items:flex-start;justify-content:center;
|
||||
padding-top:60px;overflow-y:auto;
|
||||
}
|
||||
.wifi-modal-backdrop.open{display:flex}
|
||||
.wifi-modal{
|
||||
background:var(--bg-2);border:1px solid var(--border-2);
|
||||
border-radius:12px;padding:24px;width:min(96vw,480px);
|
||||
box-shadow:0 8px 40px rgba(0,0,0,.6);
|
||||
display:flex;flex-direction:column;gap:18px;
|
||||
margin-bottom:60px;
|
||||
}
|
||||
.wifi-modal-header{
|
||||
display:flex;justify-content:space-between;align-items:center;
|
||||
}
|
||||
.wifi-modal-titre{font-size:15px;font-weight:700;color:var(--ink-1);letter-spacing:.04em}
|
||||
/* Bloc réseau actuel */
|
||||
.wifi-current{
|
||||
background:var(--bg-3);border-radius:10px;border:1px solid var(--border-1);
|
||||
padding:14px 16px;display:flex;flex-direction:column;gap:6px;
|
||||
}
|
||||
.wifi-current-titre{
|
||||
font-size:11px;font-weight:600;text-transform:uppercase;
|
||||
letter-spacing:.08em;color:var(--ink-3);margin-bottom:4px;
|
||||
}
|
||||
.wifi-current-row{display:flex;justify-content:space-between;align-items:center;gap:8px;}
|
||||
.wifi-current-label{font-size:12px;color:var(--ink-3);font-family:var(--font-ui);}
|
||||
.wifi-current-val{font-family:var(--font-mono);font-size:13px;color:var(--ink-1);}
|
||||
/* Liste réseaux */
|
||||
.wifi-scan-header{display:flex;align-items:center;justify-content:space-between;gap:8px}
|
||||
.wifi-scan-titre{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.08em;color:var(--ink-3)}
|
||||
.wifi-networks-list{
|
||||
display:flex;flex-direction:column;gap:4px;
|
||||
max-height:220px;overflow-y:auto;
|
||||
background:var(--bg-3);border-radius:10px;border:1px solid var(--border-1);
|
||||
padding:6px;
|
||||
}
|
||||
.wifi-net-item{
|
||||
display:flex;align-items:center;gap:10px;
|
||||
padding:8px 10px;border-radius:8px;cursor:pointer;
|
||||
transition:background .15s;border:1px solid transparent;
|
||||
}
|
||||
.wifi-net-item:hover{background:var(--bg-4);border-color:var(--border-2)}
|
||||
.wifi-net-ssid{flex:1;font-size:13px;color:var(--ink-1);font-family:var(--font-ui);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.wifi-net-signal{font-family:var(--font-terminal);font-size:14px;color:var(--blue);letter-spacing:-.05em;flex-shrink:0}
|
||||
.wifi-net-lock{font-size:12px;color:var(--ink-3);flex-shrink:0}
|
||||
.wifi-scan-msg{
|
||||
font-size:12px;color:var(--ink-3);font-family:var(--font-terminal);
|
||||
padding:12px;text-align:center;
|
||||
}
|
||||
/* Formulaire connexion */
|
||||
.wifi-form-titre{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.08em;color:var(--ink-3)}
|
||||
.wifi-form{display:flex;flex-direction:column;gap:10px}
|
||||
.wifi-field{display:flex;flex-direction:column;gap:4px}
|
||||
.wifi-label{font-size:11px;color:var(--ink-3);letter-spacing:.04em}
|
||||
.wifi-input{
|
||||
background:var(--bg-3);border:1px solid var(--border-2);border-radius:8px;
|
||||
color:var(--ink-1);font-family:var(--font-mono);font-size:13px;
|
||||
padding:8px 12px;outline:none;width:100%;
|
||||
transition:border-color .15s;
|
||||
}
|
||||
.wifi-input:focus{border-color:var(--accent)}
|
||||
.wifi-pass-wrap{position:relative;display:flex;align-items:center}
|
||||
.wifi-pass-wrap .wifi-input{padding-right:38px}
|
||||
.wifi-pass-toggle{
|
||||
position:absolute;right:8px;background:none;border:none;
|
||||
cursor:pointer;color:var(--ink-3);font-size:16px;padding:2px;
|
||||
line-height:1;transition:color .15s;
|
||||
}
|
||||
.wifi-pass-toggle:hover{color:var(--ink-1)}
|
||||
.btn-wifi-save{
|
||||
background:var(--accent);color:#1a1a1a;font-weight:700;
|
||||
border:none;border-radius:9px;padding:10px 18px;
|
||||
cursor:pointer;font-size:13px;font-family:var(--font-ui);
|
||||
letter-spacing:.04em;transition:background .15s;margin-top:4px;
|
||||
}
|
||||
.btn-wifi-save:hover{background:var(--accent-soft);color:#fff}
|
||||
.btn-wifi-save:disabled{background:var(--bg-4);color:var(--ink-4);cursor:not-allowed}
|
||||
.wifi-feedback{
|
||||
font-size:12px;font-family:var(--font-terminal);min-height:18px;
|
||||
text-align:center;color:var(--ok);
|
||||
}
|
||||
.btn-scan{
|
||||
background:var(--bg-4);border:1px solid var(--border-2);
|
||||
color:var(--ink-2);cursor:pointer;border-radius:7px;
|
||||
padding:5px 12px;font-size:12px;font-family:var(--font-ui);
|
||||
transition:background .15s,color .15s;
|
||||
}
|
||||
.btn-scan:hover{background:var(--bg-3);color:var(--ink-1)}
|
||||
.btn-scan:disabled{cursor:not-allowed;color:var(--ink-4)}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -248,6 +341,7 @@ input.fi:focus{border-color:var(--accent)}
|
||||
<div id="rssiBadge" class="rssi-badge">RSSI —</div>
|
||||
<div class="header-gap"></div>
|
||||
<button class="btn-icon btn-cfg" id="btnCfgMobile" title="Configuration">⚙</button>
|
||||
<button class="btn-icon" id="btnWifi" title="Gestion WiFi">📶</button>
|
||||
<button class="btn-icon" id="btnTheme" title="Basculer thème">☾</button>
|
||||
</header>
|
||||
|
||||
@@ -394,6 +488,73 @@ input.fi:focus{border-color:var(--accent)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ══ MODAL WIFI ═════════════════════════════════════════════════════ -->
|
||||
<div class="wifi-modal-backdrop" id="wifiModalBackdrop">
|
||||
<div class="wifi-modal">
|
||||
|
||||
<!-- En-tête -->
|
||||
<div class="wifi-modal-header">
|
||||
<span class="wifi-modal-titre">📶 Gestion WiFi</span>
|
||||
<button class="btn-close" id="btnCloseWifi">✕</button>
|
||||
</div>
|
||||
|
||||
<!-- Réseau actuel -->
|
||||
<div class="wifi-current">
|
||||
<div class="wifi-current-titre">Réseau actuel</div>
|
||||
<div class="wifi-current-row">
|
||||
<span class="wifi-current-label">SSID</span>
|
||||
<span class="wifi-current-val" id="wifiCurSsid">—</span>
|
||||
</div>
|
||||
<div class="wifi-current-row">
|
||||
<span class="wifi-current-label">Adresse IP</span>
|
||||
<span class="wifi-current-val" id="wifiCurIp">—</span>
|
||||
</div>
|
||||
<div class="wifi-current-row">
|
||||
<span class="wifi-current-label">RSSI</span>
|
||||
<span class="wifi-current-val" id="wifiCurRssi">—</span>
|
||||
</div>
|
||||
<div class="wifi-current-row">
|
||||
<span class="wifi-current-label">Mode</span>
|
||||
<span class="wifi-current-val" id="wifiCurMode">—</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scanner réseaux -->
|
||||
<div>
|
||||
<div class="wifi-scan-header">
|
||||
<span class="wifi-scan-titre">Réseaux disponibles</span>
|
||||
<button class="btn-scan" id="btnScan">🔎 Scanner</button>
|
||||
</div>
|
||||
<div style="margin-top:8px">
|
||||
<div class="wifi-networks-list" id="wifiNetworksList">
|
||||
<div class="wifi-scan-msg">Appuyer sur "Scanner" pour détecter les réseaux.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Formulaire connexion -->
|
||||
<div>
|
||||
<div class="wifi-form-titre" style="margin-bottom:10px">Connexion</div>
|
||||
<div class="wifi-form">
|
||||
<div class="wifi-field">
|
||||
<label class="wifi-label" for="wifiSsidInput">SSID</label>
|
||||
<input class="wifi-input" id="wifiSsidInput" type="text" placeholder="Nom du réseau WiFi" autocomplete="off">
|
||||
</div>
|
||||
<div class="wifi-field">
|
||||
<label class="wifi-label" for="wifiPassInput">Mot de passe</label>
|
||||
<div class="wifi-pass-wrap">
|
||||
<input class="wifi-input" id="wifiPassInput" type="password" placeholder="Mot de passe" autocomplete="new-password">
|
||||
<button class="wifi-pass-toggle" id="btnTogglePass" type="button" title="Afficher/masquer">👁</button>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn-wifi-save" id="btnWifiSave">Sauvegarder et redémarrer</button>
|
||||
<div class="wifi-feedback" id="wifiFeedback"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ══ SCRIPT ═════════════════════════════════════════════════════════ -->
|
||||
<script>
|
||||
(function(){
|
||||
@@ -831,6 +992,191 @@ function majHorloge(){
|
||||
document.getElementById('sbClock').textContent='esp_jardin | '+h+':'+m+':'+s;
|
||||
}
|
||||
|
||||
/* ──────────────────────────────────────────────────────────
|
||||
GESTION WIFI
|
||||
────────────────────────────────────────────────────────── */
|
||||
var wifiScanInterval = null;
|
||||
|
||||
// Convertit un RSSI en barres Unicode ▁▂▃▄▅
|
||||
function rssiBarres(rssi) {
|
||||
if (rssi >= -55) return '▅▅▅▅▅';
|
||||
if (rssi >= -65) return '▄▄▄▄░';
|
||||
if (rssi >= -75) return '▃▃▃░░';
|
||||
if (rssi >= -85) return '▂▂░░░';
|
||||
return '▁░░░░';
|
||||
}
|
||||
|
||||
function ouvrirModalWifi() {
|
||||
document.getElementById('wifiModalBackdrop').classList.add('open');
|
||||
chargerWifiCurrent();
|
||||
}
|
||||
|
||||
function fermerModalWifi() {
|
||||
document.getElementById('wifiModalBackdrop').classList.remove('open');
|
||||
stopScanPolling();
|
||||
}
|
||||
|
||||
function chargerWifiCurrent() {
|
||||
fetch('/api/wifi/current')
|
||||
.then(function(r){ return r.json(); })
|
||||
.then(function(d) {
|
||||
document.getElementById('wifiCurSsid').textContent = d.ssid || '—';
|
||||
document.getElementById('wifiCurIp').textContent = d.ip || '—';
|
||||
document.getElementById('wifiCurRssi').textContent = d.rssi ? d.rssi + ' dBm' : '—';
|
||||
document.getElementById('wifiCurMode').textContent = d.modeAP ? 'Point d\'accès (AP)' : 'Station (STA)';
|
||||
})
|
||||
.catch(function() {
|
||||
document.getElementById('wifiCurSsid').textContent = 'Erreur réseau';
|
||||
});
|
||||
}
|
||||
|
||||
function stopScanPolling() {
|
||||
if (wifiScanInterval) {
|
||||
clearInterval(wifiScanInterval);
|
||||
wifiScanInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
function demarrerScan() {
|
||||
var btn = document.getElementById('btnScan');
|
||||
var liste = document.getElementById('wifiNetworksList');
|
||||
btn.disabled = true;
|
||||
btn.textContent = '⏳ Scan…';
|
||||
liste.innerHTML = '<div class="wifi-scan-msg">Scan en cours…</div>';
|
||||
stopScanPolling();
|
||||
|
||||
// Premier appel immédiat
|
||||
fetch('/api/wifi/networks')
|
||||
.then(function(r){ return r.json(); })
|
||||
.then(function(d) { traiterResultatScan(d); })
|
||||
.catch(function() {
|
||||
liste.innerHTML = '<div class="wifi-scan-msg" style="color:var(--err)">Erreur réseau.</div>';
|
||||
btn.disabled = false;
|
||||
btn.textContent = '⟳ Réessayer';
|
||||
});
|
||||
|
||||
// Polling toutes les 2s
|
||||
wifiScanInterval = setInterval(function() {
|
||||
fetch('/api/wifi/networks')
|
||||
.then(function(r){ return r.json(); })
|
||||
.then(function(d) { traiterResultatScan(d); })
|
||||
.catch(function() { stopScanPolling(); });
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
function traiterResultatScan(d) {
|
||||
var btn = document.getElementById('btnScan');
|
||||
var liste = document.getElementById('wifiNetworksList');
|
||||
|
||||
if (d.etat === 'scan_en_cours') {
|
||||
// Continuer le polling
|
||||
return;
|
||||
}
|
||||
|
||||
// Scan terminé
|
||||
stopScanPolling();
|
||||
btn.disabled = false;
|
||||
btn.textContent = '↻ Rescanner';
|
||||
|
||||
if (!d.reseaux || d.reseaux.length === 0) {
|
||||
liste.innerHTML = '<div class="wifi-scan-msg">Aucun réseau détecté.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Tri par RSSI décroissant
|
||||
d.reseaux.sort(function(a, b) { return b.rssi - a.rssi; });
|
||||
|
||||
liste.innerHTML = '';
|
||||
d.reseaux.forEach(function(r) {
|
||||
var item = document.createElement('div');
|
||||
item.className = 'wifi-net-item';
|
||||
item.innerHTML =
|
||||
'<span class="wifi-net-ssid">' + escHtml(r.ssid) + '</span>' +
|
||||
'<span class="wifi-net-signal" title="' + r.rssi + ' dBm">' + rssiBarres(r.rssi) + '</span>' +
|
||||
'<span class="wifi-net-lock" title="' + (r.secure ? 'Sécurisé' : 'Ouvert') + '">' +
|
||||
(r.secure ? '🔒' : '🔓') +
|
||||
'</span>';
|
||||
item.addEventListener('click', function() {
|
||||
document.getElementById('wifiSsidInput').value = r.ssid;
|
||||
document.getElementById('wifiPassInput').value = '';
|
||||
document.getElementById('wifiPassInput').focus();
|
||||
});
|
||||
liste.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
function escHtml(str) {
|
||||
return String(str)
|
||||
.replace(/&/g,'&')
|
||||
.replace(/</g,'<')
|
||||
.replace(/>/g,'>')
|
||||
.replace(/"/g,'"');
|
||||
}
|
||||
|
||||
function envoyerWifiConnect() {
|
||||
var ssid = document.getElementById('wifiSsidInput').value.trim();
|
||||
var pass = document.getElementById('wifiPassInput').value;
|
||||
var feedback = document.getElementById('wifiFeedback');
|
||||
var btnSave = document.getElementById('btnWifiSave');
|
||||
|
||||
if (!ssid) {
|
||||
feedback.style.color = 'var(--err)';
|
||||
feedback.textContent = 'Le SSID ne peut pas être vide.';
|
||||
return;
|
||||
}
|
||||
|
||||
btnSave.disabled = true;
|
||||
feedback.style.color = 'var(--warn)';
|
||||
feedback.textContent = 'Envoi en cours…';
|
||||
|
||||
fetch('/api/wifi/connect', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ ssid: ssid, password: pass })
|
||||
})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(d) {
|
||||
if (d.ok) {
|
||||
feedback.style.color = 'var(--ok)';
|
||||
feedback.innerHTML =
|
||||
'Redémarrage en cours… (~5s)<br>' +
|
||||
'<small style="color:var(--ink-3)">Reconnectez-vous sur <a href="http://esp_jardin.local" style="color:var(--blue)">esp_jardin.local</a></small>';
|
||||
} else {
|
||||
feedback.style.color = 'var(--err)';
|
||||
feedback.textContent = d.erreur || 'Erreur inconnue.';
|
||||
btnSave.disabled = false;
|
||||
}
|
||||
})
|
||||
.catch(function() {
|
||||
// L'ESP redémarre et coupe la connexion — c'est normal
|
||||
feedback.style.color = 'var(--ok)';
|
||||
feedback.innerHTML =
|
||||
'Redémarrage en cours… (~5s)<br>' +
|
||||
'<small style="color:var(--ink-3)">Reconnectez-vous sur <a href="http://esp_jardin.local" style="color:var(--blue)">esp_jardin.local</a></small>';
|
||||
});
|
||||
}
|
||||
|
||||
// Événements modal WiFi
|
||||
document.getElementById('btnWifi').addEventListener('click', ouvrirModalWifi);
|
||||
document.getElementById('btnCloseWifi').addEventListener('click', fermerModalWifi);
|
||||
document.getElementById('wifiModalBackdrop').addEventListener('click', function(e) {
|
||||
if (e.target === this) fermerModalWifi();
|
||||
});
|
||||
document.getElementById('btnScan').addEventListener('click', demarrerScan);
|
||||
document.getElementById('btnWifiSave').addEventListener('click', envoyerWifiConnect);
|
||||
|
||||
// Bouton afficher/masquer mot de passe
|
||||
document.getElementById('btnTogglePass').addEventListener('click', function() {
|
||||
var inp = document.getElementById('wifiPassInput');
|
||||
if (inp.type === 'password') {
|
||||
inp.type = 'text';
|
||||
this.textContent = '🙈';
|
||||
} else {
|
||||
inp.type = 'password';
|
||||
this.textContent = '👁';
|
||||
}
|
||||
});
|
||||
|
||||
/* ──────────────────────────────────────────────────────────
|
||||
INITIALISATION
|
||||
────────────────────────────────────────────────────────── */
|
||||
|
||||
@@ -5,3 +5,9 @@ void network_init();
|
||||
|
||||
// À appeler à chaque loop() : gère OTA, reconnexion WiFi non-bloquante
|
||||
void network_update();
|
||||
|
||||
// Retourne le SSID actuellement utilisé (NVS ou config.h)
|
||||
const char* network_get_ssid();
|
||||
|
||||
// Sauvegarde les credentials WiFi en NVS (SSID + mot de passe)
|
||||
void network_save_credentials(const char* ssid, const char* password);
|
||||
|
||||
+36
-2
@@ -3,16 +3,34 @@
|
||||
#include <WiFi.h>
|
||||
#include <ESPmDNS.h>
|
||||
#include <ArduinoOTA.h>
|
||||
#include <Preferences.h>
|
||||
|
||||
static uint32_t _dernierRetryMs = 0;
|
||||
static uint32_t _debutConnexionMs = 0;
|
||||
static bool _connexionEnCours = false;
|
||||
|
||||
// Credentials actifs (NVS ou config.h)
|
||||
static char _ssidActif[64] = {};
|
||||
static char _passActif[128] = {};
|
||||
static Preferences _prefs;
|
||||
|
||||
// Retourne le SSID actuellement utilisé
|
||||
const char* network_get_ssid() { return _ssidActif; }
|
||||
|
||||
// Sauvegarde les credentials WiFi en NVS
|
||||
void network_save_credentials(const char* ssid, const char* password) {
|
||||
_prefs.begin("wifi", false);
|
||||
_prefs.putString("ssid", ssid);
|
||||
_prefs.putString("pass", password);
|
||||
_prefs.end();
|
||||
Serial.printf("[WIFI] Credentials sauvegardés: %s\n", ssid);
|
||||
}
|
||||
|
||||
// Démarre la tentative de connexion STA (non-bloquant)
|
||||
static void _demarrerSTA() {
|
||||
Serial.printf("[WIFI] Connexion STA → SSID: %s\n", WIFI_SSID);
|
||||
Serial.printf("[WIFI] Connexion STA → SSID: %s\n", _ssidActif);
|
||||
WiFi.mode(WIFI_STA);
|
||||
WiFi.begin(WIFI_SSID, WIFI_PASS);
|
||||
WiFi.begin(_ssidActif, _passActif);
|
||||
_debutConnexionMs = millis();
|
||||
_connexionEnCours = true;
|
||||
netStatus.modeAP = false; // reset dès la tentative STA
|
||||
@@ -52,6 +70,22 @@ static void _configurerOTA() {
|
||||
}
|
||||
|
||||
void network_init() {
|
||||
// Chargement des credentials depuis NVS
|
||||
_prefs.begin("wifi", true); // true = lecture seule
|
||||
String savedSsid = _prefs.getString("ssid", "");
|
||||
String savedPass = _prefs.getString("pass", "");
|
||||
_prefs.end();
|
||||
|
||||
if (savedSsid.length() > 0) {
|
||||
savedSsid.toCharArray(_ssidActif, sizeof(_ssidActif));
|
||||
savedPass.toCharArray(_passActif, sizeof(_passActif));
|
||||
Serial.printf("[WIFI] Credentials NVS: %s\n", _ssidActif);
|
||||
} else {
|
||||
strncpy(_ssidActif, WIFI_SSID, sizeof(_ssidActif) - 1);
|
||||
strncpy(_passActif, WIFI_PASS, sizeof(_passActif) - 1);
|
||||
Serial.printf("[WIFI] Credentials config.h: %s\n", _ssidActif);
|
||||
}
|
||||
|
||||
_demarrerSTA();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
#include "web_server.h"
|
||||
#include "network.h"
|
||||
#include "config.h"
|
||||
#include <LittleFS.h>
|
||||
#include <ESPAsyncWebServer.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include <WiFi.h>
|
||||
|
||||
static AsyncWebServer _server(80);
|
||||
static AsyncWebSocket _ws("/ws");
|
||||
@@ -133,6 +135,79 @@ void web_server_init() {
|
||||
}
|
||||
);
|
||||
|
||||
// GET /api/wifi/current — infos réseau actuel
|
||||
_server.on("/api/wifi/current", HTTP_GET, [](AsyncWebServerRequest* req) {
|
||||
JsonDocument doc;
|
||||
if (netStatus.wifiConnecte && !netStatus.modeAP) {
|
||||
doc["ssid"] = network_get_ssid();
|
||||
doc["ip"] = WiFi.localIP().toString();
|
||||
doc["rssi"] = WiFi.RSSI();
|
||||
doc["modeAP"] = false;
|
||||
} else {
|
||||
doc["ssid"] = netStatus.modeAP ? AP_SSID : network_get_ssid();
|
||||
doc["ip"] = netStatus.modeAP ? WiFi.softAPIP().toString() : "—";
|
||||
doc["rssi"] = 0;
|
||||
doc["modeAP"] = netStatus.modeAP;
|
||||
}
|
||||
String out;
|
||||
serializeJson(doc, out);
|
||||
req->send(200, "application/json", out);
|
||||
});
|
||||
|
||||
// GET /api/wifi/networks — scan WiFi asynchrone
|
||||
static bool _scanDemande = false;
|
||||
_server.on("/api/wifi/networks", HTTP_GET, [](AsyncWebServerRequest* req) {
|
||||
int n = WiFi.scanComplete();
|
||||
if (n == WIFI_SCAN_RUNNING) {
|
||||
req->send(200, "application/json", "{\"etat\":\"scan_en_cours\"}");
|
||||
return;
|
||||
}
|
||||
if (n == WIFI_SCAN_FAILED || !_scanDemande) {
|
||||
WiFi.scanNetworks(true); // scan asynchrone non-bloquant
|
||||
_scanDemande = true;
|
||||
req->send(200, "application/json", "{\"etat\":\"scan_en_cours\"}");
|
||||
return;
|
||||
}
|
||||
// Scan terminé — construction de la réponse
|
||||
JsonDocument doc;
|
||||
doc["etat"] = "ok";
|
||||
JsonArray arr = doc["reseaux"].to<JsonArray>();
|
||||
for (int i = 0; i < n; i++) {
|
||||
JsonObject r = arr.add<JsonObject>();
|
||||
r["ssid"] = WiFi.SSID(i);
|
||||
r["rssi"] = WiFi.RSSI(i);
|
||||
r["secure"] = (WiFi.encryptionType(i) != WIFI_AUTH_OPEN);
|
||||
}
|
||||
WiFi.scanDelete();
|
||||
_scanDemande = false;
|
||||
String out;
|
||||
serializeJson(doc, out);
|
||||
req->send(200, "application/json", out);
|
||||
});
|
||||
|
||||
// POST /api/wifi/connect — sauvegarde credentials NVS et redémarre
|
||||
_server.on("/api/wifi/connect", HTTP_POST,
|
||||
[](AsyncWebServerRequest* req) {},
|
||||
nullptr,
|
||||
[](AsyncWebServerRequest* req, uint8_t* data, size_t len, size_t, size_t) {
|
||||
JsonDocument doc;
|
||||
if (deserializeJson(doc, data, len)) {
|
||||
req->send(400, "application/json", "{\"erreur\":\"JSON invalide\"}");
|
||||
return;
|
||||
}
|
||||
const char* ssid = doc["ssid"] | "";
|
||||
const char* pass = doc["password"] | "";
|
||||
if (strlen(ssid) == 0) {
|
||||
req->send(400, "application/json", "{\"erreur\":\"SSID vide\"}");
|
||||
return;
|
||||
}
|
||||
network_save_credentials(ssid, pass);
|
||||
req->send(200, "application/json", "{\"ok\":true}");
|
||||
delay(500); // exception autorisée : redémarrage immédiat
|
||||
ESP.restart();
|
||||
}
|
||||
);
|
||||
|
||||
if (fsOk) {
|
||||
_server.serveStatic("/", LittleFS, "/").setDefaultFile("index.html");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user