mirror of
https://github.com/deunix-educ/voice-assistant.git
synced 2026-10-13 00:11:57 +02:00
249 lines
9.4 KiB
C++
249 lines
9.4 KiB
C++
#include "WifiConfig.h"
|
|
#include "config.h"
|
|
|
|
#include <Preferences.h>
|
|
#include <WebServer.h>
|
|
#include <WiFi.h>
|
|
|
|
// ------------------------------------------------------------------ NVS keys
|
|
namespace {
|
|
|
|
constexpr const char* NVS_NS = "voice-cfg";
|
|
constexpr const char* KEY_SSID = "ssid";
|
|
constexpr const char* KEY_PASS = "pass";
|
|
constexpr const char* KEY_HOST = "mqttHost";
|
|
constexpr const char* KEY_PORT = "mqttPort";
|
|
constexpr const char* KEY_USER = "mqttUser";
|
|
constexpr const char* KEY_MPWD = "mqttPass";
|
|
|
|
// ------------------------------------------------------------------ HTML
|
|
// Encodage UTF-8 inline ; servi avec charset=utf-8.
|
|
const char HTML_FORM[] =
|
|
"<!DOCTYPE html><html lang='fr'><head>"
|
|
"<meta charset='utf-8'>"
|
|
"<meta name='viewport' content='width=device-width,initial-scale=1'>"
|
|
"<title>Assistant vocal — configuration</title>"
|
|
"<style>"
|
|
"body{font-family:sans-serif;max-width:420px;margin:32px auto;padding:0 16px;color:#222}"
|
|
"h2{color:#0078d4;margin-bottom:4px}"
|
|
"p.s{color:#666;font-size:.9em;margin:0 0 20px}"
|
|
"fieldset{border:1px solid #ccc;border-radius:6px;padding:12px;margin-bottom:16px}"
|
|
"legend{font-weight:bold;padding:0 6px}"
|
|
"label{display:block;margin-top:10px;font-size:.9em;color:#444}"
|
|
"input{width:100%;padding:7px 8px;box-sizing:border-box;border:1px solid #bbb;"
|
|
"border-radius:4px;margin-top:3px;font-size:1em}"
|
|
"button{width:100%;padding:12px;background:#0078d4;color:#fff;border:none;"
|
|
"border-radius:6px;font-size:1em;cursor:pointer}"
|
|
"button:hover{background:#005fa3}"
|
|
"</style></head><body>"
|
|
"<h2>Assistant vocal</h2>"
|
|
"<p class='s'>Renseignez le réseau Wi-Fi et l’adresse du broker MQTT,"
|
|
" puis cliquez sur <em>Enregistrer</em>.</p>"
|
|
"<form method='POST' action='/save'>"
|
|
"<fieldset><legend>Wi-Fi</legend>"
|
|
"<label>Réseau (SSID)</label>"
|
|
"<input type='text' name='ssid' value='%SSID%' maxlength='63' required>"
|
|
"<label>Mot de passe</label>"
|
|
"<input type='password' name='pass' placeholder='(vide si réseau ouvert)' maxlength='63'>"
|
|
"</fieldset>"
|
|
"<fieldset><legend>Broker MQTT</legend>"
|
|
"<label>Adresse IP</label>"
|
|
"<input type='text' name='mqttHost' value='%HOST%' placeholder='192.168.1.x' maxlength='63' required>"
|
|
"<label>Port</label>"
|
|
"<input type='number' name='mqttPort' value='%PORT%' min='1' max='65535' required>"
|
|
"<label>Utilisateur (vide = connexion anonyme)</label>"
|
|
"<input type='text' name='mqttUser' value='%USER%' maxlength='31'>"
|
|
"<label>Mot de passe MQTT</label>"
|
|
"<input type='password' name='mqttPass' placeholder='(vide si anonyme)' maxlength='63'>"
|
|
"</fieldset>"
|
|
"<button type='submit'>Enregistrer et redémarrer</button>"
|
|
"</form></body></html>";
|
|
|
|
const char HTML_OK[] =
|
|
"<!DOCTYPE html><html lang='fr'><head><meta charset='utf-8'>"
|
|
"<title>Sauvegardé</title></head>"
|
|
"<body style='font-family:sans-serif;text-align:center;padding:60px;color:#222'>"
|
|
"<p style='font-size:3em'>✓</p>"
|
|
"<h2 style='color:#107c10'>Configuration sauvegardée</h2>"
|
|
"<p>L’assistant redémarre dans quelques secondes…</p>"
|
|
"</body></html>";
|
|
|
|
} // namespace
|
|
|
|
WifiConfig* WifiConfig::_instance = nullptr;
|
|
|
|
WifiConfig::WifiConfig() {
|
|
_instance = this;
|
|
// Par défaut, l'utilisateur MQTT est DEVICE_ID : les règles ACL du broker
|
|
// (pattern voice/%u/...) l'exigent. Ce repli garantit que MqttLink ne
|
|
// stocke pas un pointeur vide avant que begin() soit appelé.
|
|
strncpy(_mqttUser, DEVICE_ID, sizeof(_mqttUser) - 1);
|
|
}
|
|
|
|
WifiConfig::~WifiConfig() {
|
|
delete _server;
|
|
}
|
|
|
|
// ------------------------------------------------------------------ NVS I/O
|
|
|
|
bool WifiConfig::loadFromNvs() {
|
|
Preferences prefs;
|
|
if (!prefs.begin(NVS_NS, true)) {
|
|
return false;
|
|
}
|
|
if (!prefs.isKey(KEY_SSID)) {
|
|
prefs.end();
|
|
return false;
|
|
}
|
|
prefs.getString(KEY_SSID, _ssid, sizeof(_ssid));
|
|
prefs.getString(KEY_PASS, _password, sizeof(_password));
|
|
prefs.getString(KEY_HOST, _mqttHost, sizeof(_mqttHost));
|
|
_mqttPort = prefs.getUShort(KEY_PORT, 1883);
|
|
prefs.getString(KEY_USER, _mqttUser, sizeof(_mqttUser));
|
|
prefs.getString(KEY_MPWD, _mqttPassword, sizeof(_mqttPassword));
|
|
prefs.end();
|
|
|
|
// Utilisateur vide → repli sur DEVICE_ID (requis par le broker ACL).
|
|
if (_mqttUser[0] == '\0') {
|
|
strncpy(_mqttUser, DEVICE_ID, sizeof(_mqttUser) - 1);
|
|
}
|
|
// SSID et hôte non vides : configuration valide.
|
|
return _ssid[0] != '\0' && _mqttHost[0] != '\0';
|
|
}
|
|
|
|
void WifiConfig::saveToNvs(const char* ssid, const char* password,
|
|
const char* mqttHost, uint16_t mqttPort,
|
|
const char* mqttUser, const char* mqttPassword) {
|
|
Preferences prefs;
|
|
if (!prefs.begin(NVS_NS, false)) {
|
|
Serial.println("# config : erreur d'ecriture NVS");
|
|
return;
|
|
}
|
|
prefs.putString(KEY_SSID, ssid);
|
|
prefs.putString(KEY_PASS, password);
|
|
prefs.putString(KEY_HOST, mqttHost);
|
|
prefs.putUShort(KEY_PORT, mqttPort);
|
|
prefs.putString(KEY_USER, mqttUser);
|
|
prefs.putString(KEY_MPWD, mqttPassword);
|
|
prefs.end();
|
|
Serial.println("# config : identifiants sauvegardes en NVS");
|
|
}
|
|
|
|
// ------------------------------------------------------------------ begin
|
|
|
|
void WifiConfig::begin(bool forcePortal,
|
|
const char* fbSsid, const char* fbPassword,
|
|
const char* fbMqttHost, uint16_t fbMqttPort,
|
|
const char* fbMqttUser, const char* fbMqttPass) {
|
|
if (!forcePortal && loadFromNvs()) {
|
|
Serial.printf("# config : Wi-Fi \"%s\", broker %s:%u (NVS)\n",
|
|
_ssid, _mqttHost, _mqttPort);
|
|
return;
|
|
}
|
|
|
|
// Repli depuis secrets.h : utilisé si les deux champs essentiels sont renseignés,
|
|
// sans être copié en NVS (l'utilisateur reste maître de la migration).
|
|
if (!forcePortal && fbSsid[0] != '\0' && fbMqttHost[0] != '\0') {
|
|
strncpy(_ssid, fbSsid, sizeof(_ssid) - 1);
|
|
strncpy(_password, fbPassword, sizeof(_password) - 1);
|
|
strncpy(_mqttHost, fbMqttHost, sizeof(_mqttHost) - 1);
|
|
_mqttPort = fbMqttPort;
|
|
const char* user = (fbMqttUser[0] != '\0') ? fbMqttUser : DEVICE_ID;
|
|
strncpy(_mqttUser, user, sizeof(_mqttUser) - 1);
|
|
strncpy(_mqttPassword, fbMqttPass, sizeof(_mqttPassword) - 1);
|
|
Serial.printf("# config : Wi-Fi \"%s\", broker %s:%u (secrets.h)\n",
|
|
_ssid, _mqttHost, _mqttPort);
|
|
return;
|
|
}
|
|
|
|
Serial.println(forcePortal
|
|
? "# config : portail force (bouton PTT tenu au demarrage)"
|
|
: "# config : aucun identifiant disponible, lancement du portail");
|
|
runPortal(); // ne retourne pas
|
|
}
|
|
|
|
// ------------------------------------------------------------------ Portail
|
|
|
|
void WifiConfig::onRoot() {
|
|
String page = HTML_FORM;
|
|
page.replace("%SSID%", _instance->_ssid);
|
|
page.replace("%HOST%", _instance->_mqttHost);
|
|
char portStr[8];
|
|
snprintf(portStr, sizeof(portStr), "%u", _instance->_mqttPort);
|
|
page.replace("%PORT%", portStr);
|
|
page.replace("%USER%", _instance->_mqttUser);
|
|
_instance->_server->send(200, "text/html; charset=utf-8", page);
|
|
}
|
|
|
|
void WifiConfig::onSave() {
|
|
WebServer* srv = _instance->_server;
|
|
|
|
const String ssid = srv->arg("ssid");
|
|
const String mqttHost = srv->arg("mqttHost");
|
|
|
|
if (ssid.isEmpty()) {
|
|
srv->send(400, "text/plain", "SSID manquant");
|
|
return;
|
|
}
|
|
if (mqttHost.isEmpty()) {
|
|
srv->send(400, "text/plain", "Adresse broker manquante");
|
|
return;
|
|
}
|
|
|
|
const String pass = srv->arg("pass");
|
|
const uint16_t port = static_cast<uint16_t>(srv->arg("mqttPort").toInt());
|
|
const String mqttUser = srv->arg("mqttUser");
|
|
const String mqttPass = srv->arg("mqttPass");
|
|
|
|
_instance->saveToNvs(
|
|
ssid.c_str(), pass.c_str(),
|
|
mqttHost.c_str(), port ? port : uint16_t(1883),
|
|
mqttUser.c_str(), mqttPass.c_str());
|
|
|
|
srv->send(200, "text/html; charset=utf-8", HTML_OK);
|
|
delay(1500);
|
|
ESP.restart();
|
|
}
|
|
|
|
void WifiConfig::onNotFound() {
|
|
_instance->_server->sendHeader("Location", "/", true);
|
|
_instance->_server->send(302, "text/plain", "");
|
|
}
|
|
|
|
void WifiConfig::runPortal() {
|
|
WiFi.disconnect(true);
|
|
WiFi.mode(WIFI_AP);
|
|
if (!WiFi.softAP(WIFI_CONFIG_AP_SSID)) {
|
|
Serial.println("# portail : echec softAP, redemarrage dans 5 s");
|
|
delay(5000);
|
|
ESP.restart();
|
|
}
|
|
|
|
Serial.printf(
|
|
"# portail : AP \"%s\" actif "
|
|
"-- connectez-vous au Wi-Fi puis allez sur http://192.168.4.1\n",
|
|
WIFI_CONFIG_AP_SSID);
|
|
|
|
// LED : clignotement rapide (100 ms) pendant toute la durée du portail.
|
|
// La broche est déjà configurée en OUTPUT par led.begin() avant begin().
|
|
pinMode(PIN_STATUS_LED, OUTPUT);
|
|
|
|
_server = new WebServer(80);
|
|
_server->on("/", HTTP_GET, onRoot);
|
|
_server->on("/save", HTTP_POST, onSave);
|
|
_server->onNotFound(onNotFound);
|
|
_server->begin();
|
|
|
|
Serial.println("# portail : en attente de configuration...");
|
|
uint32_t lastToggle = 0;
|
|
bool ledOn = false;
|
|
for (;;) {
|
|
_server->handleClient();
|
|
if (millis() - lastToggle >= 100) {
|
|
lastToggle = millis();
|
|
ledOn = !ledOn;
|
|
digitalWrite(PIN_STATUS_LED, ledOn ? HIGH : LOW);
|
|
}
|
|
}
|
|
}
|