mirror of
https://github.com/deunix-educ/voice-assistant.git
synced 2026-10-13 08:21:58 +02:00
firmware-readme-update
This commit is contained in:
+123
-13
@@ -103,33 +103,143 @@ No Base64: MQTT payloads carry raw PCM bytes.
|
|||||||
|
|
||||||
## 4. Installation
|
## 4. Installation
|
||||||
|
|
||||||
```bash
|
### 4.1 ESP32 firmware
|
||||||
# 1. System packages (mosquitto, ffmpeg, venv) + serial port access
|
|
||||||
make install-system
|
|
||||||
# Log out and back in to activate the "dialout" group.
|
|
||||||
|
|
||||||
# 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
|
make install-dev
|
||||||
# PlatformIO gets its own venv (.pio-venv): its dependencies clash with the
|
# PlatformIO gets its own venv (.pio-venv): its dependencies clash with the
|
||||||
# voice server's.
|
# 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 install-speech
|
||||||
make models
|
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
|
make install-diarization
|
||||||
cp server/.env.example server/.env # then HF_TOKEN=... (see step 8)
|
|
||||||
make models
|
make models
|
||||||
|
|
||||||
# 5. Secrets (used from step 3 onwards)
|
# 6. Wake word openWakeWord (~5 MB) — optional
|
||||||
cp firmware/include/secrets.h.example firmware/include/secrets.h
|
make install-wakeword
|
||||||
cp server/.env.example server/.env
|
make models
|
||||||
|
|
||||||
# 6. Help
|
# 7. Run the assistant
|
||||||
make help
|
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
|
## 5. Usage
|
||||||
|
|||||||
@@ -102,33 +102,145 @@ Pas de Base64 : les payloads MQTT contiennent des octets PCM bruts.
|
|||||||
|
|
||||||
## 4. Installation
|
## 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
|
```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
|
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
|
make install-dev
|
||||||
# PlatformIO est installé dans son propre venv (.pio-venv) : ses dépendances
|
# PlatformIO est dans son propre venv (.pio-venv) : ses dépendances entrent
|
||||||
# entrent en conflit avec celles du serveur vocal.
|
# 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 install-speech
|
||||||
make models
|
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
|
make install-diarization
|
||||||
cp server/.env.example server/.env # puis HF_TOKEN=... (voir étape 8)
|
|
||||||
make models
|
make models
|
||||||
|
|
||||||
# 5. Secrets (utilisés à partir de l'étape 3)
|
# 6. Mot de réveil openWakeWord (~5 Mo) — optionnel
|
||||||
cp firmware/include/secrets.h.example firmware/include/secrets.h
|
make install-wakeword
|
||||||
cp server/.env.example server/.env
|
make models
|
||||||
|
|
||||||
# 6. Aide
|
# 7. Lancer l'assistant
|
||||||
make help
|
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
|
## 5. Utilisation
|
||||||
|
|||||||
@@ -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.
|
/// 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;
|
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)
|
// ------------------------------------------------------------ Wi-Fi et MQTT (étape 3)
|
||||||
|
|
||||||
/// Identifiant du nœud, utilisé dans les topics MQTT : voice/<DEVICE_ID>/...
|
/// Identifiant du nœud, utilisé dans les topics MQTT : voice/<DEVICE_ID>/...
|
||||||
|
|||||||
@@ -83,6 +83,10 @@ public:
|
|||||||
/// Nombre de connexions réussies depuis le démarrage.
|
/// Nombre de connexions réussies depuis le démarrage.
|
||||||
uint32_t connections() const;
|
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:
|
private:
|
||||||
PubSubClient _client;
|
PubSubClient _client;
|
||||||
const char* _host;
|
const char* _host;
|
||||||
|
|||||||
@@ -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 — 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
@@ -34,15 +34,21 @@
|
|||||||
#include "SerialFramer.h"
|
#include "SerialFramer.h"
|
||||||
#include "StatusLed.h"
|
#include "StatusLed.h"
|
||||||
#include "VoiceSession.h"
|
#include "VoiceSession.h"
|
||||||
|
#include "WifiConfig.h"
|
||||||
#include "WifiLink.h"
|
#include "WifiLink.h"
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
|
|
||||||
// Les secrets (Wi-Fi, broker) ne sont jamais versionnés : on les exige ici, avec
|
// secrets.h est optionnel : s'il est présent ses valeurs servent de repli si
|
||||||
// un message clair, plutôt qu'une erreur de compilation obscure plus loin.
|
// la NVS est vide. Sans lui, le portail AP/HTTP est lancé au premier démarrage.
|
||||||
#if __has_include("secrets.h")
|
#if __has_include("secrets.h")
|
||||||
#include "secrets.h"
|
#include "secrets.h"
|
||||||
#else
|
#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
|
#endif
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
@@ -63,8 +69,11 @@ DemoDevices devices(PIN_DEMO_LIGHT);
|
|||||||
SerialFramer framer(Serial);
|
SerialFramer framer(Serial);
|
||||||
SerialFrameReader reader(Serial);
|
SerialFrameReader reader(Serial);
|
||||||
WiFiClient network;
|
WiFiClient network;
|
||||||
WifiLink wifi(WIFI_SSID, WIFI_PASSWORD, DEVICE_ID);
|
// cfg DOIT être déclaré avant wifi et mqtt : leurs constructeurs stockent des
|
||||||
MqttLink mqtt(network, MQTT_HOST, MQTT_PORT, DEVICE_ID, MQTT_USER, MQTT_PASSWORD);
|
// 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.
|
/// Chunk en cours d'envoi réseau : 1600 octets, alloué une seule fois.
|
||||||
uint8_t outgoingChunk[AUDIO_CHUNK_BYTES];
|
uint8_t outgoingChunk[AUDIO_CHUNK_BYTES];
|
||||||
@@ -462,6 +471,29 @@ void setup() {
|
|||||||
devices.begin();
|
devices.begin();
|
||||||
button.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.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",
|
Serial.printf("# format : %lu Hz, 16 bits, mono, chunk %lu ms = %u octets\n",
|
||||||
static_cast<unsigned long>(AUDIO_SAMPLE_RATE),
|
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
|
// --- 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.
|
// 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",
|
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,
|
cfg.ssid(), cfg.mqttHost(), cfg.mqttPort(), MQTT_TOPIC_PREFIX, DEVICE_ID,
|
||||||
MQTT_PASSWORD[0] != '\0' ? "oui" : "NON (anonyme)");
|
cfg.mqttPassword()[0] != '\0' ? "oui" : "NON (anonyme)");
|
||||||
network.setConnectionTimeout(MQTT_CONNECT_TIMEOUT_MS);
|
network.setConnectionTimeout(MQTT_CONNECT_TIMEOUT_MS);
|
||||||
|
mqtt.setPort(cfg.mqttPort()); // synchronise _port avec la valeur chargée par cfg
|
||||||
if (!mqtt.begin(onMqttMessage, onMqttConnect)) {
|
if (!mqtt.begin(onMqttMessage, onMqttConnect)) {
|
||||||
Serial.println("# ERREUR : tampon MQTT impossible a allouer");
|
Serial.println("# ERREUR : tampon MQTT impossible a allouer");
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user