firmware-readme-update

This commit is contained in:
2026-10-06 14:12:13 +02:00
parent d20976fea2
commit 99beaff4ca
7 changed files with 628 additions and 33 deletions
+4
View File
@@ -83,6 +83,10 @@ public:
/// Nombre de connexions réussies depuis le démarrage.
uint32_t connections() const;
/// Met à jour le port avant le premier appel à begin() (utile si la
/// configuration vient de la NVS plutôt que d'une constante de secrets.h).
void setPort(uint16_t port) { _port = port; }
private:
PubSubClient _client;
const char* _host;
+248
View File
@@ -0,0 +1,248 @@
#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 &#8212; 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&eacute;seau Wi-Fi et l&rsquo;adresse du broker MQTT,"
" puis cliquez sur <em>Enregistrer</em>.</p>"
"<form method='POST' action='/save'>"
"<fieldset><legend>Wi-Fi</legend>"
"<label>R&eacute;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&eacute;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&eacute;marrer</button>"
"</form></body></html>";
const char HTML_OK[] =
"<!DOCTYPE html><html lang='fr'><head><meta charset='utf-8'>"
"<title>Sauvegard&eacute;</title></head>"
"<body style='font-family:sans-serif;text-align:center;padding:60px;color:#222'>"
"<p style='font-size:3em'>&#10003;</p>"
"<h2 style='color:#107c10'>Configuration sauvegard&eacute;e</h2>"
"<p>L&rsquo;assistant red&eacute;marre dans quelques secondes&hellip;</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);
}
}
}
+75
View File
@@ -0,0 +1,75 @@
#pragma once
#include <Arduino.h>
/**
* @brief Configuration Wi-Fi et MQTT persistée en NVS (Preferences).
*
* Au premier démarrage (NVS vide, pas de secrets.h), ou si le bouton PTT est
* tenu pendant WIFI_CONFIG_HOLD_MS au démarrage, la carte ouvre un point
* d'accès Wi-Fi (SSID défini par WIFI_CONFIG_AP_SSID dans config.h) et sert
* un formulaire de configuration sur http://192.168.4.1. Après validation,
* les identifiants sont sauvegardés en NVS et la carte redémarre.
*
* En fonctionnement normal, les identifiants viennent de la NVS. Si la NVS
* est vide et que secrets.h fournit des valeurs non vides, elles sont utilisées
* comme repli sans être copiées en NVS.
*
* Les accesseurs retournent des pointeurs vers des tampons internes valides
* pour toute la durée de vie de l'objet : ils peuvent être passés directement
* aux constructeurs de WifiLink et MqttLink (trick pointeur — les tampons sont
* remplis par begin() avant que begin() de ces classes ne soit appelé).
*/
class WifiConfig {
public:
WifiConfig();
~WifiConfig();
/**
* @brief Charge les identifiants depuis la NVS ou le repli (secrets.h).
*
* Si forcePortal vaut true, ou si aucun identifiant valide n'est disponible,
* lance le portail AP + HTTP et ne revient jamais (l'ESP32 redémarre après
* sauvegarde).
*
* @param forcePortal true pour forcer le portail (bouton tenu au démarrage).
* @param fbSsid Repli (secrets.h) : SSID — chaîne vide si absent.
* @param fbPassword Repli : mot de passe Wi-Fi.
* @param fbMqttHost Repli : adresse IP du broker.
* @param fbMqttPort Repli : port du broker.
* @param fbMqttUser Repli : utilisateur MQTT.
* @param fbMqttPass Repli : mot de passe MQTT.
*/
void begin(bool forcePortal,
const char* fbSsid, const char* fbPassword,
const char* fbMqttHost, uint16_t fbMqttPort,
const char* fbMqttUser, const char* fbMqttPass);
const char* ssid() const { return _ssid; }
const char* password() const { return _password; }
const char* mqttHost() const { return _mqttHost; }
uint16_t mqttPort() const { return _mqttPort; }
const char* mqttUser() const { return _mqttUser; }
const char* mqttPassword() const { return _mqttPassword; }
private:
char _ssid[64] = {};
char _password[64] = {};
char _mqttHost[64] = {};
uint16_t _mqttPort = 1883;
char _mqttUser[32] = {};
char _mqttPassword[64] = {};
class WebServer* _server = nullptr;
bool loadFromNvs();
void saveToNvs(const char* ssid, const char* password,
const char* mqttHost, uint16_t mqttPort,
const char* mqttUser, const char* mqttPassword);
void runPortal();
static WifiConfig* _instance;
static void onRoot();
static void onSave();
static void onNotFound();
};
+40 -7
View File
@@ -34,15 +34,21 @@
#include "SerialFramer.h"
#include "StatusLed.h"
#include "VoiceSession.h"
#include "WifiConfig.h"
#include "WifiLink.h"
#include "config.h"
// Les secrets (Wi-Fi, broker) ne sont jamais versionnés : on les exige ici, avec
// un message clair, plutôt qu'une erreur de compilation obscure plus loin.
// secrets.h est optionnel : s'il est présent ses valeurs servent de repli si
// la NVS est vide. Sans lui, le portail AP/HTTP est lancé au premier démarrage.
#if __has_include("secrets.h")
#include "secrets.h"
#else
#error "firmware/include/secrets.h absent : cp firmware/include/secrets.h.example firmware/include/secrets.h, puis renseignez le Wi-Fi et l'adresse du broker"
static constexpr const char* WIFI_SSID = "";
static constexpr const char* WIFI_PASSWORD = "";
static constexpr const char* MQTT_HOST = "";
static constexpr uint16_t MQTT_PORT = 1883;
static constexpr const char* MQTT_USER = "";
static constexpr const char* MQTT_PASSWORD = "";
#endif
namespace {
@@ -63,8 +69,11 @@ DemoDevices devices(PIN_DEMO_LIGHT);
SerialFramer framer(Serial);
SerialFrameReader reader(Serial);
WiFiClient network;
WifiLink wifi(WIFI_SSID, WIFI_PASSWORD, DEVICE_ID);
MqttLink mqtt(network, MQTT_HOST, MQTT_PORT, DEVICE_ID, MQTT_USER, MQTT_PASSWORD);
// cfg DOIT être déclaré avant wifi et mqtt : leurs constructeurs stockent des
// pointeurs vers ses tampons internes, remplis plus tard par cfg.begin().
WifiConfig cfg;
WifiLink wifi(cfg.ssid(), cfg.password(), DEVICE_ID);
MqttLink mqtt(network, cfg.mqttHost(), cfg.mqttPort(), DEVICE_ID, cfg.mqttUser(), cfg.mqttPassword());
/// Chunk en cours d'envoi réseau : 1600 octets, alloué une seule fois.
uint8_t outgoingChunk[AUDIO_CHUNK_BYTES];
@@ -462,6 +471,29 @@ void setup() {
devices.begin();
button.begin();
// Détection du mode configuration : bouton PTT tenu dès le démarrage.
// On attend WIFI_CONFIG_HOLD_MS ms ; si relâché avant, démarrage normal.
bool forceConfig = false;
if (digitalRead(PIN_BUTTON_PTT) == LOW) {
Serial.println("# bouton PTT detecte au demarrage, attente pour confirmer...");
const uint32_t holdStart = millis();
while (digitalRead(PIN_BUTTON_PTT) == LOW) {
if (millis() - holdStart >= WIFI_CONFIG_HOLD_MS) {
forceConfig = true;
break;
}
delay(10);
}
}
// Charge les identifiants Wi-Fi + MQTT depuis la NVS ou secrets.h ;
// lance le portail AP/HTTP si aucun n'est disponible ou si forcé.
// Ne retourne pas en cas de portail (reboot après sauvegarde).
cfg.begin(forceConfig,
WIFI_SSID, WIFI_PASSWORD,
MQTT_HOST, MQTT_PORT,
MQTT_USER, MQTT_PASSWORD);
Serial.println("\n# --- assistant vocal, etapes 1 a 3 : audio + Wi-Fi + MQTT ---");
Serial.printf("# format : %lu Hz, 16 bits, mono, chunk %lu ms = %u octets\n",
static_cast<unsigned long>(AUDIO_SAMPLE_RATE),
@@ -511,9 +543,10 @@ void setup() {
// --- Réseau, en dernier : l'auto-test du micro se fait sans les émissions
// radio du Wi-Fi (pointes de 300 mA), qui pourraient fausser la mesure.
Serial.printf("# reseau : Wi-Fi \"%s\", broker %s:%u, topics %s/%s/..., compte MQTT %s\n",
WIFI_SSID, MQTT_HOST, MQTT_PORT, MQTT_TOPIC_PREFIX, DEVICE_ID,
MQTT_PASSWORD[0] != '\0' ? "oui" : "NON (anonyme)");
cfg.ssid(), cfg.mqttHost(), cfg.mqttPort(), MQTT_TOPIC_PREFIX, DEVICE_ID,
cfg.mqttPassword()[0] != '\0' ? "oui" : "NON (anonyme)");
network.setConnectionTimeout(MQTT_CONNECT_TIMEOUT_MS);
mqtt.setPort(cfg.mqttPort()); // synchronise _port avec la valeur chargée par cfg
if (!mqtt.begin(onMqttMessage, onMqttConnect)) {
Serial.println("# ERREUR : tampon MQTT impossible a allouer");
}