From 99beaff4ca86db60fd1cc0125ed102aa59c6e441 Mon Sep 17 00:00:00 2001 From: denis defolie Date: Tue, 6 Oct 2026 14:12:13 +0200 Subject: [PATCH] firmware-readme-update --- README-EN.md | 136 ++++++++++++++++++-- README.md | 138 ++++++++++++++++++-- firmware/include/config.h | 13 ++ firmware/src/MqttLink.h | 4 + firmware/src/WifiConfig.cpp | 248 ++++++++++++++++++++++++++++++++++++ firmware/src/WifiConfig.h | 75 +++++++++++ firmware/src/main.cpp | 47 ++++++- 7 files changed, 628 insertions(+), 33 deletions(-) create mode 100644 firmware/src/WifiConfig.cpp create mode 100644 firmware/src/WifiConfig.h diff --git a/README-EN.md b/README-EN.md index f478823..18acec0 100644 --- a/README-EN.md +++ b/README-EN.md @@ -103,33 +103,143 @@ No Base64: MQTT payloads carry raw PCM bytes. ## 4. Installation -```bash -# 1. System packages (mosquitto, ffmpeg, venv) + serial port access -make install-system -# Log out and back in to activate the "dialout" group. +### 4.1 ESP32 firmware -# 2. Python environment and dev tooling (pyright, pytest, platformio) +**Prerequisites:** a USB cable with data lines (not charge-only), port identified +(`ls /dev/ttyUSB*` or `/dev/ttyACM*` after plugging in). + +```bash +make pio-venv # PlatformIO in its own venv — once only +make fw-build # compile without flashing +make fw-upload SERIAL_PORT=/dev/ttyUSB0 +``` + +**First boot — WiFi configuration portal** + +If no credentials have been configured yet, the board automatically opens an +access point: + +1. The LED blinks rapidly (100 ms on / 100 ms off). +2. Connect a phone or PC to the **VoiceAssist-Config** Wi-Fi network. +3. Open `http://192.168.4.1` in a browser. +4. Enter the Wi-Fi network **(2.4 GHz required)** and the MQTT broker IP address. +5. Click **Save and restart** — credentials are saved to persistent memory (NVS) + and the board reboots. + +> **Reconfiguration:** hold the PTT button for 3 s at power-on to reopen the +> portal, even if credentials are already stored. + +**Alternative — `secrets.h`** (for repeated development builds) + +```bash +cp firmware/include/secrets.h.example firmware/include/secrets.h +# Edit: WIFI_SSID, WIFI_PASSWORD, MQTT_HOST +make fw-upload SERIAL_PORT=/dev/ttyUSB0 +``` + +NVS takes priority over `secrets.h`. To start from scratch, force the portal +with the PTT button (or erase the NVS with `esptool.py erase_flash`). + +--- + +### 4.2 Python server + +**Prerequisites:** Python 3.10+, Docker (for the broker) or native Mosquitto. + +```bash +# 1. System packages (mosquitto, ffmpeg, venv) + serial port access (dialout group) +make install-system +# → log out and back in to activate the "dialout" group + +# 2. MQTT broker (choose one) +make mosquitto-docker # Docker container (recommended) +make mosquitto-config # native Debian Mosquitto service (installed via make install-system) + +# 3. Python environment + dev tooling (pytest, pyright, PlatformIO) make install-dev # PlatformIO gets its own venv (.pio-venv): its dependencies clash with the # voice server's. -# 3. Speech recognition and synthesis (step 6): faster-whisper, Piper, ~550 MB of models +# 4. Speech recognition + synthesis (~550 MB) make install-speech make models -# 4. Diarization (step 8): pyannote + CPU torch (~1.3 GB), Hugging Face token required +# 5. Diarization + speaker identification (~1.3 GB) — optional, heavy on RPi 4 +# Prerequisite: Hugging Face "Read" token in server/.env (HF_TOKEN=hf_…) +# Accept the conditions at https://huggingface.co/pyannote/speaker-diarization-community-1 +cp server/.env.example server/.env # then HF_TOKEN=hf_… make install-diarization -cp server/.env.example server/.env # then HF_TOKEN=... (see step 8) make models -# 5. Secrets (used from step 3 onwards) -cp firmware/include/secrets.h.example firmware/include/secrets.h -cp server/.env.example server/.env +# 6. Wake word openWakeWord (~5 MB) — optional +make install-wakeword +make models -# 6. Help -make help +# 7. Run the assistant +make run ``` +**Files to configure:** + +| File | Contents | +|---|---| +| `server/.env` | `MQTT_USERNAME`, `MQTT_PASSWORD`, `HF_TOKEN` | +| `server/config.yaml` | rooms, voices, access rights, machine list | + +For help on all targets: `make help`. + +--- + +### 4.3 Linux agent (machines controlled by voice) + +Each Linux machine to control (desktop PC, NAS…) runs a lightweight service that +executes **only** the actions listed in its own configuration file — no shell +exposed. + +**On the target machine:** + +```bash +# 1. Copy the agent/ directory from the Raspberry Pi +scp -r /home/rpi5/voice-assistant/agent/ user@desktop-pc:~/voice-agent-install/ + +# 2. Install the service (creates /opt/voice-agent/, /etc/voice-agent/, systemd unit) +cd ~/voice-agent-install +sudo make install + +# 3. Configure +sudo nano /etc/voice-agent/agent.yaml +# id: pc-bureau ← name used in MQTT topics +# mqtt.host: 192.168.1.104 ← broker IP address +# dry_run: false ← true to test without executing anything + +# 4. Enable and start +sudo systemctl enable --now voice-agent +sudo systemctl status voice-agent +``` + +**On the server,** add the machine to `server/config.yaml`: + +```yaml +machines: + list: + - id: pc-bureau + name: "PC du bureau" # spoken as: "le PC du bureau" + mac: "AA:BB:CC:DD:EE:FF" # MAC address for Wake-on-LAN + ip: 192.168.1.50 +``` + +**Try it without shutting anything down** (`dry_run: true` in `agent.yaml`): + +```bash +make run # server (terminal 1) +make agent-run # agent in test mode on this machine (terminal 2) +# Say: "Hey Mycroft, éteins le PC du bureau" +# → the agent prints: essai : systemctl poweroff (non exécuté) +``` + +Default available actions: `shutdown`, `reboot`, `lock`; `wake` +(power on over the network) is handled by the server via Wake-on-LAN. + --- ## 5. Usage diff --git a/README.md b/README.md index 4b0c2e5..c9be78d 100644 --- a/README.md +++ b/README.md @@ -102,33 +102,145 @@ Pas de Base64 : les payloads MQTT contiennent des octets PCM bruts. ## 4. Installation +### 4.1 Firmware ESP32 + +**Prérequis :** câble USB avec les données (pas charge seule), port identifié +(`ls /dev/ttyUSB*` ou `/dev/ttyACM*` après branchement). + ```bash -# 1. Paquets système (mosquitto, ffmpeg, venv) + accès au port série +make pio-venv # PlatformIO dans son propre venv — une seule fois +make fw-build # compile sans flasher +make fw-upload SERIAL_PORT=/dev/ttyUSB0 +``` + +**Premier démarrage — portail de configuration WiFi** + +Si aucun identifiant n'a encore été configuré, la carte ouvre automatiquement +un point d'accès : + +1. La LED clignote rapidement (100 ms on / 100 ms off). +2. Connectez un téléphone ou un PC au réseau **VoiceAssist-Config**. +3. Ouvrez `http://192.168.4.1` dans un navigateur. +4. Renseignez le Wi-Fi **(réseau 2,4 GHz obligatoire)** et l'adresse IP du + broker MQTT. +5. Cliquez **Enregistrer et redémarrer** — les identifiants sont sauvegardés + dans la mémoire permanente (NVS) et la carte redémarre. + +> **Reconfiguration :** tenez le bouton PTT appuyé 3 s à la mise sous tension +> pour rouvrir le portail, même si des identifiants sont déjà enregistrés. + +**Alternative — `secrets.h`** (pour les compilations répétées en développement) + +```bash +cp firmware/include/secrets.h.example firmware/include/secrets.h +# Éditer : WIFI_SSID, WIFI_PASSWORD, MQTT_HOST +make fw-upload SERIAL_PORT=/dev/ttyUSB0 +``` + +La NVS reste prioritaire sur `secrets.h`. Pour repartir de zéro, forcez le +portail avec le bouton PTT (ou effacez la NVS avec `esptool.py erase_flash`). + +--- + +### 4.2 Serveur Python + +**Prérequis :** Python 3.10+, Docker (pour le broker) ou Mosquitto natif. + +```bash +# 1. Paquets système (mosquitto, ffmpeg, venv) + groupe dialout (port série) make install-system -# Se déconnecter/reconnecter pour activer le groupe « dialout ». +# → se déconnecter/reconnecter pour activer le groupe « dialout » -# 2. Environnement Python et outils de développement (pyright, pytest, platformio) +# 2. Broker MQTT (choisir l'une ou l'autre) +make mosquitto-docker # conteneur Docker (recommandé) +make mosquitto-config # service Mosquitto Debian natif (déjà installé via make install-system) + +# 3. Environnement Python + outils de développement (pytest, pyright, PlatformIO) make install-dev -# PlatformIO est installé dans son propre venv (.pio-venv) : ses dépendances -# entrent en conflit avec celles du serveur vocal. +# PlatformIO est dans son propre venv (.pio-venv) : ses dépendances entrent +# en conflit avec celles du serveur vocal. -# 3. Transcription et synthèse vocale (étape 6) : faster-whisper, Piper, ~550 Mo de modèles +# 4. Transcription + synthèse vocale (~550 Mo) make install-speech make models -# 4. Diarisation (étape 8) : pyannote + torch CPU (~1,3 Go), jeton Hugging Face requis +# 5. Diarisation + identification vocale (~1,3 Go) — optionnel, lourd sur RPi 4 +# Prérequis : compte Hugging Face + accepter les conditions sur +# https://huggingface.co/pyannote/speaker-diarization-community-1 +# Créer un jeton « Read » sur https://huggingface.co/settings/tokens +cp server/.env.example server/.env # puis HF_TOKEN=hf_… make install-diarization -cp server/.env.example server/.env # puis HF_TOKEN=... (voir étape 8) make models -# 5. Secrets (utilisés à partir de l'étape 3) -cp firmware/include/secrets.h.example firmware/include/secrets.h -cp server/.env.example server/.env +# 6. Mot de réveil openWakeWord (~5 Mo) — optionnel +make install-wakeword +make models -# 6. Aide -make help +# 7. Lancer l'assistant +make run ``` +**Fichiers à configurer :** + +| Fichier | Contenu | +|---|---| +| `server/.env` | `MQTT_USERNAME`, `MQTT_PASSWORD`, `HF_TOKEN` | +| `server/config.yaml` | pièces, voix, droits d'accès, liste des machines | + +Pour l'aide sur toutes les cibles : `make help`. + +--- + +### 4.3 Agent Linux (machines commandées par la voix) + +Chaque machine Linux à commander (PC du bureau, NAS…) fait tourner un service +léger qui n'exécute **que** les actions listées dans son fichier de +configuration — aucun shell exposé. + +**Sur la machine cible :** + +```bash +# 1. Copier le répertoire agent/ (depuis le Raspberry Pi) +scp -r /home/rpi5/voice-assistant/agent/ user@pc-bureau:~/voice-agent/ + +# 2. Installer le service (crée /opt/voice-agent/, /etc/voice-agent/, unité systemd) +cd ~/voice-agent +sudo make install + +# 3. Configurer +sudo nano /etc/voice-agent/agent.yaml +# id: pc-bureau ← nom dans les topics MQTT +# mqtt.host: 192.168.1.104 ← adresse IP du broker +# dry_run: false ← true pour tester sans rien exécuter + +# 4. Activer et démarrer +sudo systemctl enable --now voice-agent +sudo systemctl status voice-agent +``` + +**Sur le serveur,** ajouter la machine dans `server/config.yaml` : + +```yaml +machines: + list: + - id: pc-bureau + name: "PC du bureau" # dit à voix haute : « le PC du bureau » + mac: "AA:BB:CC:DD:EE:FF" # adresse MAC pour Wake-on-LAN + ip: 192.168.1.50 +``` + +**Essai sans rien éteindre** (`dry_run: true` dans `agent.yaml`) : + +```bash +make run # serveur (terminal 1) +make agent-run # agent en mode essai sur cette machine (terminal 2) +# Dire : « Hey Mycroft, éteins le PC du bureau » +# → l'agent affiche : essai : systemctl poweroff (non exécuté) +``` + +Actions disponibles par défaut : `shutdown`, `reboot`, `lock` ; `wake` +(allumer depuis le réseau) est géré par le serveur via Wake-on-LAN. + --- ## 5. Utilisation diff --git a/firmware/include/config.h b/firmware/include/config.h index 3a7de10..8b17eaa 100644 --- a/firmware/include/config.h +++ b/firmware/include/config.h @@ -176,6 +176,19 @@ static constexpr uint32_t CAPTURE_TASK_STACK_BYTES = 4096; /// Débit série : 921 600 baud = ~92 kB/s, largement au-dessus des 32 kB/s d'audio. static constexpr uint32_t SERIAL_BAUD = 921600; +// -------------------------------------------------------- Configuration Wi-Fi (portail AP) + +/** + * SSID du point d'accès ouvert en mode configuration (premier démarrage ou + * bouton PTT tenu pendant WIFI_CONFIG_HOLD_MS au démarrage). La carte sert + * un formulaire HTTP sur http://192.168.4.1 tant que l'utilisateur n'a pas + * validé les identifiants Wi-Fi + MQTT. + */ +static constexpr const char* WIFI_CONFIG_AP_SSID = "VoiceAssist-Config"; + +/// Durée de maintien du bouton PTT au démarrage pour forcer le portail (ms). +static constexpr uint32_t WIFI_CONFIG_HOLD_MS = 3000; + // ------------------------------------------------------------ Wi-Fi et MQTT (étape 3) /// Identifiant du nœud, utilisé dans les topics MQTT : voice//... diff --git a/firmware/src/MqttLink.h b/firmware/src/MqttLink.h index b8147ea..ab1ade1 100644 --- a/firmware/src/MqttLink.h +++ b/firmware/src/MqttLink.h @@ -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; diff --git a/firmware/src/WifiConfig.cpp b/firmware/src/WifiConfig.cpp new file mode 100644 index 0000000..003f770 --- /dev/null +++ b/firmware/src/WifiConfig.cpp @@ -0,0 +1,248 @@ +#include "WifiConfig.h" +#include "config.h" + +#include +#include +#include + +// ------------------------------------------------------------------ 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[] = + "" + "" + "" + "Assistant vocal — configuration" + "" + "

Assistant vocal

" + "

Renseignez le réseau Wi-Fi et l’adresse du broker MQTT," + " puis cliquez sur Enregistrer.

" + "
" + "
Wi-Fi" + "" + "" + "" + "" + "
" + "
Broker MQTT" + "" + "" + "" + "" + "" + "" + "" + "" + "
" + "" + "
"; + +const char HTML_OK[] = + "" + "Sauvegardé" + "" + "

✓

" + "

Configuration sauvegardée

" + "

L’assistant redémarre dans quelques secondes…

" + ""; + +} // 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(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); + } + } +} diff --git a/firmware/src/WifiConfig.h b/firmware/src/WifiConfig.h new file mode 100644 index 0000000..0d98265 --- /dev/null +++ b/firmware/src/WifiConfig.h @@ -0,0 +1,75 @@ +#pragma once + +#include + +/** + * @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(); +}; diff --git a/firmware/src/main.cpp b/firmware/src/main.cpp index 22f086b..adc46c9 100644 --- a/firmware/src/main.cpp +++ b/firmware/src/main.cpp @@ -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(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"); }