Assistant vocal local ESP32 : étapes 1 à 15, et 15b écrite

État du projet au premier commit :
- firmware ESP32 : micro I2S, lecture anti-gigue, MQTT (état retenu, testament),
  mains libres, domotique de démonstration, chien de garde, présence du serveur ;
- serveur Python : sessions, VAD, faster-whisper, pyannote, identification,
  droits par profil, domotique, mot de réveil, Piper ;
- broker Mosquitto : configuration, comptes et ACL (non activés en développement) ;
- étape 15b écrite, en attente d'essai : agent MQTT pour machines Linux.

Étapes 1 à 15 validées. 203 tests, pyright sans erreur.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Denis Defolie
2026-09-30 17:40:55 +02:00
commit d20976fea2
108 changed files with 15022 additions and 0 deletions
+385
View File
@@ -0,0 +1,385 @@
# Brochage et câblage — ESP32-WROOM-32 + INMP441 + MAX98357A
Document de référence du câblage. Schéma visuel : [wiring.svg](wiring.svg)
(clic droit → « Open Preview » dans VSCodium, ou ouverture dans un navigateur).
> **Avant de câbler** : ESP32 débranché de l'USB. On ne modifie jamais un câblage
> sous tension : une inversion VDD/GND détruit l'INMP441 en quelques secondes.
---
## 1. Tableau de câblage complet
Carte de référence : **DOIT ESP32 DEVKIT V1, 30 broches** (la plus répandue en TP).
Sur une carte 38 broches, les numéros de GPIO sont identiques, seule leur position
physique change : **fiez-vous toujours aux numéros sérigraphiés**, jamais au rang.
### 1.1 Micro INMP441 (I2S0, réception)
| INMP441 | ESP32 | Fil conseillé | Rôle |
|---|---|---|---|
| `VDD` | **3V3** | rouge | alimentation **3,3 V uniquement** |
| `GND` | **GND** | noir | masse |
| `SCK` | **GPIO 25** | bleu | horloge bit (BCLK), générée par l'ESP32 |
| `WS` | **GPIO 26** | vert | sélection de mot (word select / LRCL) |
| `SD` | **GPIO 33** | violet | donnée série, micro → ESP32 |
| `L/R` | **GND** | noir | choix du canal : GND = **gauche** |
`L/R` à GND est la référence de ce projet : gardez ce câblage.
Attention à un piège propre à l'ESP32 : l'INMP441 n'émet que pendant **une moitié
de trame** et sort des **zéros** pendant l'autre. Or la correspondance entre la
broche `L/R` et les constantes `I2S_STD_SLOT_LEFT` / `..._RIGHT` du pilote n'est
pas celle qu'on attend sur l'ESP32 d'origine. Écouter la mauvaise moitié donne un
flux **parfaitement nul**, alors que le câblage est correct — le symptôme le plus
déroutant de l'étape 1.
Le firmware tranche donc tout seul : au démarrage, la classe `MicSelfTest`
réveille le micro **une seule fois**, lit les deux moitiés de trame en cadrage
MSB et décide sur la **structure** des mots : la moitié utile est celle qui n'est
ni nulle ni constante, et elle doit porter exactement **24 bits utiles**. Si elle
en porte 25, le micro suit le cadrage Philips et le firmware bascule. Le niveau
au repos est affiché pour information, sans intervenir dans la décision.
### 1.2 Amplificateur MAX98357A (I2S1, émission) — étape 2
| MAX98357A | ESP32 | Fil | Rôle |
|---|---|---|---|
| `Vin` | **VIN (5 V)** | orange | alimentation 5 V (3,3 V possible mais 4× moins de puissance) |
| `GND` | **GND** | noir | masse |
| `BCLK` | **GPIO 27** | bleu | horloge bit |
| `LRC` | **GPIO 14** | vert | sélection de mot |
| `DIN` | **GPIO 22** | violet | donnée série, ESP32 → ampli |
| `GAIN` | *non connecté* | — | gain 9 dB par défaut (voir §4) |
| `SD` | *non connecté* | — | déjà tiré au + par 100 kΩ sur la carte → ampli actif |
| `+` / `−` | haut-parleur | — | sortie pontée : **jamais de masse sur `−`** |
### 1.3 Interface utilisateur
| Élément | ESP32 | Câblage |
|---|---|---|
| Bouton PTT | **GPIO 4** | une borne sur GPIO 4, l'autre sur **GND** — pull-up interne activé par le firmware |
| LED d'état | **GPIO 2** | GPIO 2 → résistance **330 Ω** → anode LED ; cathode → GND |
La carte DevKit V1 possède déjà une LED bleue sur GPIO 2 : la LED externe est
**optionnelle**, l'étape 1 est validable sans elle.
### 1.4 Haut-parleur
| Caractéristique | Valeur conseillée |
|---|---|
| Impédance | **4 Ω** (3 W) ou 8 Ω (2 W) |
| Diamètre | 40 à 66 mm |
| Câblage | directement sur `+` et `−` de l'ampli |
La sortie du MAX98357A est **pontée** (les deux fils sont actifs, en opposition
de phase). Relier `−` à la masse court-circuite l'étage de sortie.
---
## 2. Schéma ASCII
```
ESP32-WROOM-32 (DevKit V1, 30 broches)
+----------------------------------------+
INMP441 | |
+---------+ | EN GPIO23 |
| VDD o--|--rouge------|--------------------------------> 3V3 | (broche 3V3)
| GND o--|--noir-------|---------------------------------> GND | (broche GND)
| SD o--|--violet---->| GPIO33 GPIO22 |<--violet--+
| SCK o--|--bleu------>| GPIO25 GPIO4 |<--+ |
| WS o--|--vert------>| GPIO26 GPIO2 |---|--+ |
| L/R o--|--noir--> GND| GPIO27 -->--bleu----------+ .. | | | |
+---------+ | GPIO14 -->--vert--------+ | .. | | | |
3,3 V SEULEMENT | | | VIN(5V)| | | |
+-------------------------|-|------------+ | | |
| | | | |
+-------------+ | | | |
| +-----------+ | | |
v v | | |
MAX98357A | | | | |
+----------|---|-----+ | | |
| LRC o<--+ | | | | |
| BCLK o<------+ | HP 4 ohms / 3 W | | |
| DIN o<------------|----------------------|--|----+
| GAIN o (non conn. = 9 dB) | |
| SD o (non conn. = actif) + o---+ | |
| GND o---noir--> GND - o---|--+ | |
| Vin o---orange--> VIN (5 V) | | | |
+------------------------------+ | | | |
v v | |
+---------+ | |
| ( ) HP| | |
+---------+ | |
| |
bouton PTT : GPIO4 ----[ / ]---- GND <-------------+ |
LED etat : GPIO2 ----[330]---->|---- GND <-----------+
```
---
## 3. Pourquoi ce choix de broches
| Contrainte | Conséquence |
|---|---|
| **Broches de strapping** GPIO 0, 2, 5, 12, 15 | leur niveau au reset choisit le mode de démarrage → aucun signal audio dessus. GPIO 2 ne porte qu'une LED, en sortie seule. |
| **GPIO 34-39 sont en entrée seule** | inutilisables pour SCK/WS (sorties d'horloge). |
| **GPIO 6-11** | reliées à la mémoire flash interne → jamais utilisables. |
| **Deux périphériques I2S séparés** | I2S0 pour le micro, I2S1 pour l'ampli. On ne partage **pas** les horloges : le micro tourne en 32 bits/slot et l'ampli en 16 bits, et surtout l'entrée et la sortie doivent pouvoir démarrer et s'arrêter indépendamment. Cinq fils au lieu de trois : c'est le prix de la simplicité logicielle. |
| **GPIO 1 / 3 (TX0/RX0)** | utilisées par l'USB-série → interdites. |
---
## 4. Réglages matériels du MAX98357A
### Broche `GAIN`
| Câblage de `GAIN` | Gain |
|---|---|
| direct à GND | 15 dB |
| à GND via 100 kΩ | 12 dB |
| **non connectée** | **9 dB** ← notre choix |
| à Vin via 100 kΩ | 6 dB |
| direct à Vin | 3 dB |
### Broche `SD` (shutdown + choix de canal)
La tension sur `SD` sélectionne le mode :
| Tension sur `SD` | Mode |
|---|---|
| < 0,16 V | ampli éteint |
| 0,16 – 0,77 V | canal droit seul |
| 0,77 – 1,4 V | canal gauche seul |
| **> 1,4 V** | **moyenne (gauche + droite) / 2** ← par défaut |
Les cartes de type Adafruit tirent `SD` au + par 100 kΩ : laissée libre, la broche
est donc au-dessus de 1,4 V et l'ampli fonctionne en mode moyenne.
Le firmware émet en **stéréo, le même échantillon sur les deux voies**
(`AudioOutput`). La moyenne (gauche + droite) / 2 redonne donc exactement le
signal : niveau correct, et une éventuelle inversion gauche/droite est sans effet.
On n'a pas misé sur le comportement implicite d'un mode « mono » : c'est la leçon
de l'étape 1, où une moitié de trame muette nous a longtemps égarés.
Pour couper le son par logiciel, on reliera plus tard `SD` à un GPIO
(niveau bas = silence). Inutile avant l'étape 15.
---
## 5. Bilan de consommation
| Consommateur | Courant moyen | Crête |
|---|---|---|
| ESP32 (Wi-Fi actif) | ~120 mA | ~500 mA pendant l'émission radio |
| INMP441 | 1,4 mA | — |
| MAX98357A au repos | 2,4 mA | — |
| MAX98357A à 3 W sur 4 Ω | ~670 mA | ~1,2 A sur les transitoires |
| **Total au volume maximal** | **~0,8 A** | **> 1,5 A** |
Conséquences pratiques :
1. Une alimentation USB 5 V / 1 A **ne suffit pas** au volume maximal : l'ESP32
redémarre (brown-out). Prévoir **5 V / 2 A**.
2. Ajouter un condensateur **électrolytique 470 µF / 16 V** entre `Vin` et `GND`
de l'ampli, au plus près de la carte : il fournit les crêtes que les fils de
breadboard ne peuvent pas suivre. Types et valeurs limites : **§6**.
3. Alternative propre : alimenter l'ampli par une source 5 V **externe**, en
reliant sa masse à celle de l'ESP32 (**masse commune obligatoire**).
---
## 6. Condensateurs : types et valeurs
### 6.1 Les trois condensateurs du montage
| Rep. | Où | Rôle | Type exact |
|---|---|---|---|
| **C1** | `Vin` / `GND` du MAX98357A | réservoir d'énergie pour les crêtes du haut-parleur | électrolytique aluminium radial, **faible ESR**, 105 °C |
| **C2** | `Vin` / `GND` du MAX98357A, collé à C1 | découplage haute fréquence (la classe D découpe à ~300 kHz) | céramique multicouche **X7R** ou X5R |
| **C3** | GPIO 4 / `GND` (**optionnel**) | anti-rebond matériel du bouton | céramique multicouche **X7R** |
### 6.2 Valeurs minimale, typique et maximale
| Rep. | Mini | **Typique** | Maxi | Tension de service |
|---|---|---|---|---|
| **C1** | 100 µF<br>`1 × 10⁸ pF` | **470 µF**<br>`4,7 × 10⁸ pF` | 1 000 µF<br>`1 × 10⁹ pF` | ≥ 10 V, **16 V conseillé** |
| **C2** | 10 nF<br>`10 000 pF` (code 103) | **100 nF**<br>`100 000 pF` (code **104**) | 1 µF<br>`1 000 000 pF` (code 105) | ≥ 25 V |
| **C3** | 10 nF<br>`10 000 pF` (code 103) | **100 nF**<br>`100 000 pF` (code **104**) | 220 nF<br>`220 000 pF` (code 224) | ≥ 16 V |
Les bornes ne sont pas arbitraires :
- **C1 mini 100 µF** — la chute de tension pendant une crête vaut `ΔV = I × Δt / C`.
Pour 1 A pendant 100 µs : 470 µF → 0,21 V (sans effet) ; 100 µF → 1,0 V (limite,
l'ESP32 décroche vers 4,0 V en entrée de son régulateur) ; 47 µF → 2 V, redémarrage garanti.
- **C1 maxi 1 000 µF** — au branchement, le condensateur vide est un court-circuit :
`I = C × dV/dt`. Charger 1 000 µF à 5 V en 1 ms demande **5 A**, ce qui déclenche la
protection du port USB ou du hub. Au-delà, il faut un circuit de pré-charge : hors sujet ici.
- **C2** — au-dessus de ~1 MHz, un électrolytique se comporte comme une self : il ne
découple plus rien. Sous 10 nF, la céramique ne stocke plus assez pour les fronts de
commutation. Au-dessus de 1 µF, aucun gain et le boîtier devient inutilement gros.
- **C3** — la constante de temps vaut `τ = R_pull-up × C`, avec le pull-up interne de
l'ESP32 à ≈ 45 kΩ : 100 nF → τ = 4,5 ms, soit ~6 ms pour franchir le seuil logique.
C'est bien en dessous des `BUTTON_DEBOUNCE_MS = 30 ms` du firmware. Avec 220 nF on
atteint ~13 ms : encore acceptable. Au-delà, le filtre matériel et le filtre logiciel
se cumulent et le bouton devient mou.
**C3 est facultatif** : l'anti-rebond est déjà fait en logiciel dans
[PushButton.cpp](../firmware/src/PushButton.cpp). Ne l'ajoutez que si le bouton est au
bout d'un fil long et capte du bruit. Dans ce cas, insérez aussi **100 Ω en série**
entre le GPIO et le bouton : cette résistance limite le courant de décharge du
condensateur dans le contact à chaque appui, ce qui préserve le bouton.
### 6.3 Lire le marquage — il est justement en picofarads
Les céramiques portent un **code à trois chiffres en pF** : deux chiffres significatifs,
puis le nombre de zéros à ajouter.
| Code imprimé | picofarads | nanofarads | microfarads |
|---|---|---|---|
| `102` | 1 000 pF | 1 nF | 0,001 µF |
| `103` | 10 000 pF | 10 nF | 0,01 µF |
| **`104`** | **100 000 pF** | **100 nF** | **0,1 µF** |
| `224` | 220 000 pF | 220 nF | 0,22 µF |
| `105` | 1 000 000 pF | 1 000 nF | 1 µF |
| `106` | 10 000 000 pF | 10 000 nF | 10 µF |
Une lettre suivante donne la tolérance : `J` = ±5 %, `K` = ±10 %, `M` = ±20 %.
Sur un découplage, la tolérance n'a aucune importance : `K` ou `M` conviennent.
Les électrolytiques, eux, portent la valeur en clair (`470µF 16V`) et une **bande
verticale marquant la borne négative**, en face de la patte la plus courte.
### 6.4 Pièges sur les condensateurs
1. **Polarité** — un électrolytique monté à l'envers chauffe, gonfle puis éclate en
quelques secondes. La bande imprimée est le **−**, la patte longue est le **+**.
Les céramiques, elles, ne sont pas polarisées.
2. **Diélectrique** — refusez les `Y5V` et `Z5U` : ils perdent jusqu'à 80 % de leur
capacité avec la température et la tension appliquée. **X7R** (ou X5R) pour le
découplage, `C0G`/`NP0` seulement pour les petites valeurs.
3. **Effet de la tension continue** — une céramique 10 µF / 6,3 V en boîtier 0603
ne fait plus que 4 µF sous 5 V. C'est pourquoi C2 est spécifiée en **25 V minimum**.
4. **ESR** — un 470 µF « general purpose » peut présenter 1 Ω de résistance série :
inutile pour des crêtes de 1 A. Cherchez la mention **low ESR** ou **105 °C**
(typiquement < 0,3 Ω).
5. **Distance** — C1 et C2 à moins de 3 cm des broches `Vin` / `GND` de l'ampli.
Au-delà, l'inductance des fils annule leur effet.
6. **C1 et C2 ne se remplacent pas** : le gros stocke l'énergie, le petit évacue le
haut du spectre. On met toujours les deux, en parallèle.
Les modules INMP441 et MAX98357A du commerce embarquent déjà leur découplage local
(100 nF, et 10 µF côté ampli). C1 et C2 compensent **les fils de la platine d'essai**,
pas un manque des modules.
---
## 7. Précautions de câblage
1. **Longueur des fils I2S** : 15 cm maximum. Au-delà, l'horloge se déforme et le
son devient bruité.
2. **Séparer** les fils du micro de ceux du haut-parleur : le courant du HP
(jusqu'à 1 A commuté à 300 kHz) rayonne dans les fils du micro.
3. **Masse commune** entre tous les modules, sans exception.
4. Ne pas alimenter l'INMP441 en 5 V : il est prévu pour 1,8 à 3,3 V.
5. Le micro est sensible aux vibrations : le fixer, ne pas le laisser pendre
contre le boîtier du haut-parleur (effet Larsen mécanique).
6. Sur breadboard, vérifier que les deux moitiés des rails d'alimentation sont
bien reliées (beaucoup de breadboards coupent les rails au milieu).
---
## 8. Vérifications au multimètre, avant le premier démarrage
ESP32 **débranché**, en mode continuité puis en mode tension :
| # | Mesure | Attendu |
|---|---|---|
| 1 | continuité `INMP441 VDD` ↔ `ESP32 3V3` | passant |
| 2 | continuité `INMP441 GND` ↔ `ESP32 GND` | passant |
| 3 | continuité `INMP441 L/R` ↔ `GND` | passant |
| 4 | continuité `INMP441 VDD` ↔ `GND` | **coupé** (sinon court-circuit) |
| 5 | continuité `MAX98357A Vin` ↔ `ESP32 VIN` | passant |
| 6 | continuité `MAX98357A −` ↔ `GND` | **coupé** |
| 7 | continuité `GPIO 4` ↔ `GND`, bouton relâché | **coupé** |
| 8 | continuité `GPIO 4` ↔ `GND`, bouton appuyé | passant |
### Continuité de la ligne de données
Le défaut le plus tenace de l'étape 1 est une ligne `SD` qui n'arrive pas. Testez
en continuité, **carte hors tension**, et **sur la pastille du module**, jamais au
bout du fil : une soudure sèche laisse passer le fil mais pas le signal.
| # | Mesure | Attendu |
|---|---|---|
| 8b | `GPIO 33` ↔ pastille `SD` du module | passant |
| 8c | pastille `L/R` du module ↔ `GND` | passant |
| 8d | `GPIO 33` ↔ `GND` | **coupé** |
Puis USB branché :
| # | Mesure | Attendu |
|---|---|---|
| 9 | tension `3V3` / `GND` | 3,2 à 3,4 V |
| 10 | tension `VIN` / `GND` | 4,6 à 5,1 V |
| 11 | tension `GPIO 4` / `GND`, bouton relâché | ≈ 3,3 V (pull-up interne) |
| 12 | tension `GPIO 4` / `GND`, bouton appuyé | ≈ 0 V |
Le test 11 exige que le firmware soit déjà téléversé : le pull-up est activé par
`pinMode(INPUT_PULLUP)`.
---
## 9. Diagnostic rapide
| Symptôme | Cause la plus fréquente | Vérification |
|---|---|---|
| Auto-test : les deux moitiés « silence numerique » | le micro n'est pas cadencé ou n'émet rien | tensions sur les **pastilles du module** : `VDD` 3,3 V, `SCK` et `WS` ≈ 1,6 V |
| Auto-test : « un bit de retard (Philips) » | le micro suit le cadrage Philips | aucune : le firmware bascule et revérifie |
| Auto-test : « DECALE : bit de signe perdu » (23 bits) | cadrage faux, le premier bit déborde dans le mot précédent | aucune en MSB ; si ça persiste, envoyer l'analyse bit à bit |
| Auto-test : « queue du mot non nulle » | bits parasites après les 24 bits utiles | vérifier `MIC_SD_PULLDOWN = true` |
| Auto-test : « les DEUX moities portent des donnees » | `L/R` flottant, ou deux micros sur la ligne | continuité `L/R` ↔ GND sur la pastille |
| Auto-test : « bruit de fond eleve » au repos | pièce bruyante, ou micro pas encore stabilisé | sans effet sur le cadrage ; juger sur l'enregistrement |
| Ligne SD : « FLOTTE », horloges correctes | rien ne pilote GPIO 33 | continuité `SD` ↔ GPIO 33 sur la pastille |
| Auto-test série : amplitude mesurable mais crête faible | simple question de gain | appliquer la valeur de `MIC_GAIN` conseillée par l'auto-test |
| Crête à 0 %, silence total | `L/R` non relié à GND | test 3 |
| Crête à 0 %, silence total | `SD` du micro sur la mauvaise broche | test de continuité vers GPIO 33 |
| Bruit blanc continu | micro alimenté en 5 V | test 9 sur la broche `VDD` du micro |
| Son très faible même en criant | `SCK` et `WS` inversés | inverser GPIO 25 et GPIO 26 |
| Signal haché, craquements réguliers | fils I2S trop longs | raccourcir à 10 cm |
| L'ESP32 redémarre quand le son sort | alimentation insuffisante | §5, points 1 et 2 |
| Téléversement impossible | LED sur GPIO 2 qui perturbe le boot | retirer la LED externe, utiliser celle de la carte |
| Le HP souffle ou vibre en permanence dès la mise sous tension | broches I2S de sortie flottantes : l'ampli amplifie du bruit | firmware ≥ étape 1 : `silenceAmplifier()` les force à 0. Si le bruit persiste pendant le boot, relier `SD` de l'ampli à `GND`, ou ne pas l'alimenter avant l'étape 2 |
| Sifflement aigu permanent | masse commune absente entre ampli et ESP32 | §7, point 3 |
---
## 10. Récapitulatif des GPIO réservés
| GPIO | Usage | Étape |
|---|---|---|
| 2 | LED d'état | 1 |
| 4 | bouton PTT | 1 |
| 14 | MAX98357A `LRC` | 2 |
| 22 | MAX98357A `DIN` | 2 |
| 25 | INMP441 `SCK` | 1 |
| 26 | INMP441 `WS` | 1 |
| 27 | MAX98357A `BCLK` | 2 |
| 33 | INMP441 `SD` | 1 |
| 21 | LED de démonstration domotique (« la lumière » de la pièce) | 11 |
### LED de démonstration (étape 11, facultative)
```
GPIO21 ──[ 330 Ω ]──►|── GND LED rouge ou verte : patte longue (anode) côté résistance
```
Courant : (3,3 V − 2,0 V) / 330 Ω ≈ 4 mA, bien sous les 12 mA conseillés par broche.
GPIO 21 est libre et sans rôle au démarrage (pas une broche de *strapping*). Elle
figure « la lumière » de la pièce de la carte ; volets et porte sont simulés
(état affiché sur la liaison série). Un module relais se brancherait au même endroit.
Cette LED n'est pas dessinée sur `wiring.svg`.
Les valeurs sont définies une seule fois, dans
[firmware/include/config.h](../firmware/include/config.h). Toute modification de
câblage se fait dans ce fichier, jamais dans le code des classes.
+158
View File
@@ -0,0 +1,158 @@
# Broker Mosquitto — installation et vérification
Le broker (serveur MQTT) relaie tous les messages entre l'ESP32 et le serveur
Python. Configuration unique : [mosquitto/voice.conf](../mosquitto/voice.conf).
---
## 1. Deux façons de le faire tourner
### Service Debian (Raspberry Pi, PC)
```bash
make install-system # installe mosquitto et mosquitto-clients
make mosquitto-config # copie voice.conf dans /etc/mosquitto/conf.d/ et redémarre
```
### Conteneur Docker
Monter `voice.conf` **à la place** de `mosquitto.conf` du conteneur, et publier le
port 1883 :
```yaml
services:
mosquitto:
image: eclipse-mosquitto:2.0
restart: unless-stopped
ports:
- 1883:1883
volumes:
- ./mosquitto/config:/mosquitto/config # contient voice.conf renommé mosquitto.conf
```
Si le conteneur existe déjà :
```bash
make mosquitto-docker MOSQ_DIR=~/srv/mosquitto MOSQ_COMPOSE=~/srv/docker-compose.mosquitto.yml
```
La cible sauvegarde l'ancienne configuration (`mosquitto.conf.bak`), installe
`voice.conf` avec le propriétaire du conteneur (uid 1883), relance le service et
affiche les dernières lignes du journal. Le `sudo` est nécessaire : les fichiers
appartiennent à l'utilisateur `mosquitto` du conteneur.
---
## 2. Comptes et droits (étape 15)
Sans authentification, n'importe quel appareil du réseau local peut s'abonner à
`voice/+/audio/stream` et **écouter le micro** en continu, ou publier une
commande. Depuis l'étape 15, le broker exige un compte, et un fichier de droits
(ACL, *Access Control List* : liste de qui peut lire ou écrire quel topic)
limite chaque compte :
| Compte | Qui | Droits ([mosquitto/acl](../mosquitto/acl)) |
|---|---|---|
| `voice-server` | le serveur, et les cibles `make mqtt-*`, `tone-mqtt`, `play-mqtt` | lire et écrire `voice/#` et `home/#` |
| `esp32-01` (= `DEVICE_ID`) | une carte | écrire **ses** `state`, `event`, `audio/in`, `audio/stream` ; lire **ses** `control`, `audio/out`, et `voice/server/status` |
| `pc-bureau` (= `id` de l'agent) | un agent Linux (étape 15b) | écrire **ses** `agent/<id>/state` et `result` ; lire **ses** `agent/<id>/command` |
Une carte ne peut donc ni écouter une autre carte, ni publier sous son nom, ni
envoyer de commande domotique. Vérifié sur un broker de test : connexion
anonyme et mauvais mot de passe refusés, usurpation d'une autre carte jetée.
```bash
make mqtt-user NAME=voice-server # mot de passe demandé deux fois, stocké haché
make mqtt-user NAME=esp32-01 # un compte par carte, du nom de son DEVICE_ID
make mosquitto-docker # installe voice.conf + acl + passwd, relance le broker
```
Les mots de passe en clair vont dans `server/.env` (`MQTT_USERNAME`,
`MQTT_PASSWORD`) et dans `firmware/include/secrets.h` (`MQTT_PASSWORD` ;
`MQTT_USER` vide = `DEVICE_ID`). `mosquitto/passwd` ne contient que des
empreintes (hachage : transformation à sens unique), et n'est pas versionné.
Le mot de passe circule encore **en clair** sur le réseau local : seul TLS
(chiffrement, port 8883) l'empêcherait. C'est l'option de l'étape 15, non
activée : sur un réseau Wi-Fi WPA2 domestique, l'écoute du trafic suppose déjà
d'avoir la clé du Wi-Fi.
---
## 3. Pièges rencontrés
| Symptôme | Cause | Correctif |
|---|---|---|
| L'ESP32 ne se connecte pas, `mosquitto_sub` sur le PC fonctionne | Mosquitto 2.x sans `listener` explicite n'écoute que sur `localhost` | `listener 1883 0.0.0.0` |
| Conteneur en état `restarting`, des centaines de redémarrages | deux lignes `listener 1883` : le second ne peut pas ouvrir le port (`Error: Address in use`) | **un seul** `listener` par port |
| Le broker plante sans rien dire | `log_dest syslog` : dans un conteneur, il n'y a pas de syslog | `log_dest stdout`, puis `docker logs <conteneur>` |
| `Note: It is recommended to replace message_size_limit` | option obsolète depuis Mosquitto 2.0 | `max_packet_size 8192` |
| L'ESP32 affiche `mqtt : echec (code -2)` | connexion TCP refusée : broker arrêté, mauvaise IP, pare-feu | `make mqtt-watch` depuis le PC ; vérifier `MQTT_HOST` dans `secrets.h` |
| Deux cartes se déconnectent l'une l'autre en boucle | même `DEVICE_ID`, donc même identifiant client MQTT | un `DEVICE_ID` différent par carte |
| `connexion refusee par le broker : Not authorized` (serveur), `code 5` (ESP32) | compte absent de `passwd`, mot de passe différent, ou `MQTT_PASSWORD` vide | `make mqtt-user NAME=...`, puis `make mosquitto-docker` ; recopier le même mot de passe |
| Tout est connecté, mais rien ne passe | droits : Mosquitto **jette en silence** une publication interdite et n'envoie rien à un abonnement interdit | nom d'utilisateur de la carte = son `DEVICE_ID` (la carte l'affiche au démarrage sinon) |
| Un autre programme de la maison ne se connecte plus au broker | `allow_anonymous false` vaut pour **tous** les clients | lui créer un compte, et ses lignes dans `mosquitto/acl` |
| `ACL pattern 'voice/server/status' does not contain '%c' or '%u'` au démarrage | avertissement : ce topic est commun à toutes les cartes, c'est voulu | rien à faire |
---
## 4. Vérifier le broker, sans ESP32
```bash
make mqtt-watch # terminal 1 : tout ce qui passe sur voice/#
make mqtt-control MSG='{"cmd":"ping"}' # terminal 2 : un message sur control
```
Le terminal 1 doit afficher `voice/esp32-01/control {"cmd":"ping"}`.
Sans le paquet `mosquitto-clients`, ces cibles utilisent automatiquement les
clients inclus dans l'image Docker `eclipse-mosquitto:2.0`.
---
## 5. État retenu et testament
| Topic | Qui publie | Quand | Retenu |
|---|---|---|---|
| `voice/esp32-01/state` `{"status":"online",...}` | l'ESP32 | à chaque connexion, puis toutes les 30 s | oui |
| `voice/esp32-01/state` `{"status":"offline"}` | **le broker**, à la place de l'ESP32 | quand l'ESP32 disparaît sans prévenir | oui |
| `voice/esp32-01/event` `{"event":"boot","reason":...}` | l'ESP32 | une fois par démarrage | non |
Messages publiés par le serveur (étape 10) :
| Topic | Qui publie | Quand | Retenu |
|---|---|---|---|
| `voice/esp32-01/transcript` `{"session":...,"utterances":[{"speaker":"Denis","text":...}]}` | le serveur (QoS 1) | après chaque question transcrite | non |
| `home/<pièce>/<appareil>/set` `{"action":"close","by":"Denis","board":"esp32-01",...}` | le serveur (QoS 1), étape 11 | à chaque commande domotique | **non** : rejouée à un abonné tardif, elle ferait bouger un volet |
| `voice/esp32-01/control` `{"cmd":"device","device":"light","action":"on"}` | le serveur, étape 11 | commande pour la pièce de la carte | non |
| `voice/esp32-01/event` `{"event":"device",...,"ok":true,"state":"on"}` | l'ESP32, étape 11 | confirmation d'exécution | non |
| `voice/esp32-01/control` `{"cmd":"listen","enabled":true}` | le serveur, étape 13 | connexion ou redémarrage de la carte ; `false` à l'arrêt du serveur | non |
| `voice/esp32-01/audio/stream` (PCM binaire, QoS 0) | l'ESP32, étape 13 | en continu tant que l'écoute est active, sauf pendant une réponse ou un appui | non |
| `voice/esp32-01/control` `{"cmd":"capture","active":true}` | le serveur, étape 13 | mot de réveil entendu (LED allumée) ; `false` à la fin de la commande | non |
| `voice/server/status` `{"status":"online"}` | le serveur, étape 15 | à chaque connexion au broker | **oui** |
| `voice/server/status` `{"status":"offline"}` | le serveur à l'arrêt, ou **le broker** (testament du serveur) s'il meurt | arrêt ou disparition du serveur : les cartes coupent leur micro | **oui** |
| `agent/<machine>/state` `{"status":"online","actions":[...]}` | l'agent Linux, étape 15b ; `offline` par son testament | à sa connexion | **oui** |
| `agent/<machine>/command` `{"action":"shutdown","id":...,"by":"Denis"}` | le serveur (QoS 1) | commande autorisée (et confirmée) | **non** : l'agent se connecte en session propre, rien n'est rejoué |
| `agent/<machine>/result` `{"action":...,"ok":true,"code":0,"output":...}` | l'agent | après exécution | non |
- **Retenu** : le broker garde le dernier message du topic et le donne tout de
suite à chaque nouvel abonné. Un serveur qui démarre après l'ESP32 connaît donc
aussitôt son état.
- **Testament** (*Last Will*) : confié au broker à la connexion. Si l'ESP32
disparaît sans se déconnecter proprement, c'est le broker qui publie `offline`.
Délai du testament :
| Disparition | Le broker s'en aperçoit | `offline` publié après |
|---|---|---|
| ESP32 : coupure de courant, reset, plantage, perte du Wi-Fi | quand l'ESP32 cesse de répondre : la puce ne prévient jamais | 1,5 × `MQTT_KEEPALIVE_S` = **15 s** au plus |
| client sur PC tué (`kill`, Ctrl-C) | immédiatement : le système d'exploitation ferme la connexion | < 1 s |
Après un simple reset, l'ESP32 se reconnecte en quelques secondes, souvent avant
l'expiration du keepalive. Le broker remplace alors l'ancienne session par la
nouvelle, et l'on peut voir passer un `offline` immédiatement suivi de `online`.
Pour observer le testament seul, il faut **couper l'alimentation** et attendre.
La cause du redémarrage (`reason`) vaut `poweron`, `external` (broche EN),
`software`, `panic` (plantage), `watchdog` ou **`brownout`** : chute de tension,
typiquement l'ampli qui tire trop sur une alimentation USB faible.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 331 KiB

+283
View File
@@ -0,0 +1,283 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1400 1010" width="1400" height="1010" font-family="DejaVu Sans, Arial, Helvetica, sans-serif">
<!-- Schéma de câblage : ESP32-WROOM-32 + INMP441 + MAX98357A + HP + bouton + LED -->
<!-- Fond blanc explicite : le schéma reste lisible en thème sombre. -->
<rect x="0" y="0" width="1400" height="1010" fill="#ffffff"/>
<style>
.box { fill:#f7f7f4; stroke:#333333; stroke-width:2; }
.title { font-size:17px; font-weight:bold; fill:#1a1a1a; }
.sub { font-size:12px; fill:#555555; }
.pin { font-size:12px; fill:#1a1a1a; }
.pinused { font-size:12px; font-weight:bold; fill:#1a1a1a; }
.lbl { font-size:12px; fill:#333333; }
.tick { stroke:#999999; stroke-width:1.5; }
.w { fill:none; stroke-width:2.4; stroke-linejoin:round; }
.gnd { stroke:#333333; }
.v33 { stroke:#cc2222; }
.v5 { stroke:#e08214; }
.clk { stroke:#1f6fd0; }
.ws { stroke:#12855d; }
.data { stroke:#8a3fc0; }
.warn { font-size:12px; font-weight:bold; fill:#b30000; }
</style>
<text x="30" y="40" class="title" font-size="22">Assistant vocal local — câblage complet</text>
<text x="30" y="62" class="sub">ESP32-WROOM-32 · INMP441 (micro I2S) · MAX98357A (ampli I2S) · haut-parleur · bouton PTT · LED d'état</text>
<!-- =============================== ESP32 =============================== -->
<text x="610" y="100" class="title" text-anchor="middle">ESP32-WROOM-32 (DevKit V1)</text>
<rect x="480" y="110" width="260" height="590" rx="6" class="box"/>
<text x="610" y="688" class="sub" text-anchor="middle">vue logique — repérez les numéros sérigraphiés</text>
<!-- colonne gauche -->
<g class="pin">
<text x="492" y="140">EN</text>
<text x="492" y="178">GPIO 36</text>
<text x="492" y="216">GPIO 39</text>
<text x="492" y="254">GPIO 34</text>
<text x="492" y="292">GPIO 35</text>
<text x="492" y="330">GPIO 32</text>
<text x="492" y="558">GPIO 12</text>
<text x="492" y="596">GPIO 13</text>
<text x="492" y="634">GND</text>
</g>
<g class="pinused">
<text x="492" y="368">GPIO 33</text>
<text x="492" y="406">GPIO 25</text>
<text x="492" y="444">GPIO 26</text>
<text x="492" y="482">GPIO 27</text>
<text x="492" y="520">GPIO 14</text>
<text x="492" y="672">VIN (5 V)</text>
</g>
<!-- colonne droite -->
<g class="pin" text-anchor="end">
<text x="728" y="140">GPIO 23</text>
<text x="728" y="216">TX0</text>
<text x="728" y="254">RX0</text>
<text x="728" y="292">GPIO 21</text>
<text x="728" y="330">GPIO 19</text>
<text x="728" y="368">GPIO 18</text>
<text x="728" y="406">GPIO 5</text>
<text x="728" y="444">GPIO 17</text>
<text x="728" y="482">GPIO 16</text>
<text x="728" y="596">GPIO 15</text>
</g>
<g class="pinused" text-anchor="end">
<text x="728" y="178">GPIO 22</text>
<text x="728" y="520">GPIO 4</text>
<text x="728" y="558">GPIO 2</text>
<text x="728" y="634">GND</text>
<text x="728" y="672">3V3</text>
</g>
<!-- ergots de broches -->
<g class="tick">
<line x1="470" y1="135" x2="480" y2="135"/><line x1="470" y1="173" x2="480" y2="173"/>
<line x1="470" y1="211" x2="480" y2="211"/><line x1="470" y1="249" x2="480" y2="249"/>
<line x1="470" y1="287" x2="480" y2="287"/><line x1="470" y1="325" x2="480" y2="325"/>
<line x1="470" y1="363" x2="480" y2="363"/><line x1="470" y1="401" x2="480" y2="401"/>
<line x1="470" y1="439" x2="480" y2="439"/><line x1="470" y1="477" x2="480" y2="477"/>
<line x1="470" y1="515" x2="480" y2="515"/><line x1="470" y1="553" x2="480" y2="553"/>
<line x1="470" y1="591" x2="480" y2="591"/><line x1="470" y1="629" x2="480" y2="629"/>
<line x1="470" y1="667" x2="480" y2="667"/>
<line x1="740" y1="135" x2="750" y2="135"/><line x1="740" y1="173" x2="750" y2="173"/>
<line x1="740" y1="211" x2="750" y2="211"/><line x1="740" y1="249" x2="750" y2="249"/>
<line x1="740" y1="287" x2="750" y2="287"/><line x1="740" y1="325" x2="750" y2="325"/>
<line x1="740" y1="363" x2="750" y2="363"/><line x1="740" y1="401" x2="750" y2="401"/>
<line x1="740" y1="439" x2="750" y2="439"/><line x1="740" y1="477" x2="750" y2="477"/>
<line x1="740" y1="515" x2="750" y2="515"/><line x1="740" y1="553" x2="750" y2="553"/>
<line x1="740" y1="591" x2="750" y2="591"/><line x1="740" y1="629" x2="750" y2="629"/>
<line x1="740" y1="667" x2="750" y2="667"/>
</g>
<!-- =============================== INMP441 =============================== -->
<rect x="150" y="150" width="240" height="200" rx="6" class="box"/>
<text x="270" y="178" class="title" text-anchor="middle" font-size="15">INMP441</text>
<text x="270" y="196" class="sub" text-anchor="middle">micro I2S — entrée audio</text>
<text x="270" y="214" class="warn" text-anchor="middle">3,3 V uniquement</text>
<g class="pinused" text-anchor="end">
<text x="378" y="240">SD</text>
<text x="378" y="268">SCK</text>
<text x="378" y="296">WS</text>
</g>
<g class="pinused">
<text x="162" y="344">L/R</text>
<text x="196" y="344">GND</text>
<text x="236" y="344">VDD</text>
</g>
<g class="tick">
<line x1="390" y1="236" x2="400" y2="236"/>
<line x1="390" y1="264" x2="400" y2="264"/>
<line x1="390" y1="292" x2="400" y2="292"/>
<line x1="170" y1="350" x2="170" y2="360"/>
<line x1="200" y1="350" x2="200" y2="360"/>
<line x1="240" y1="350" x2="240" y2="360"/>
</g>
<!-- fils du micro -->
<polyline class="w data" points="390,236 455,236 455,363 470,363"/>
<polyline class="w clk" points="390,264 440,264 440,401 470,401"/>
<polyline class="w ws" points="390,292 425,292 425,439 470,439"/>
<polyline class="w gnd" points="170,350 170,790"/>
<polyline class="w gnd" points="200,350 200,790"/>
<polyline class="w v33" points="240,350 240,750"/>
<text x="120" y="378" class="lbl" transform="rotate(-90 120 378)">L/R → GND (canal gauche)</text>
<!-- =============================== MAX98357A =============================== -->
<rect x="880" y="380" width="260" height="240" rx="6" class="box"/>
<text x="1010" y="408" class="title" text-anchor="middle" font-size="15">MAX98357A</text>
<text x="1010" y="426" class="sub" text-anchor="middle">ampli classe D mono — sortie audio</text>
<text x="1010" y="444" class="sub" text-anchor="middle">alimenté en 5 V</text>
<g class="pinused">
<text x="892" y="475">LRC</text>
<text x="892" y="503">BCLK</text>
<text x="892" y="531">DIN</text>
</g>
<g class="pin">
<text x="898" y="600">GAIN</text>
<text x="950" y="600">SD</text>
<text x="996" y="600">GND</text>
<text x="1050" y="600">Vin</text>
</g>
<g class="pinused" text-anchor="end">
<text x="1128" y="475">+</text>
<text x="1128" y="535">−</text>
</g>
<g class="tick">
<line x1="870" y1="470" x2="880" y2="470"/>
<line x1="870" y1="500" x2="880" y2="500"/>
<line x1="870" y1="530" x2="880" y2="530"/>
<line x1="920" y1="620" x2="920" y2="630"/>
<line x1="970" y1="620" x2="970" y2="630"/>
<line x1="1020" y1="620" x2="1020" y2="630"/>
<line x1="1070" y1="620" x2="1070" y2="630"/>
<line x1="1140" y1="470" x2="1150" y2="470"/>
<line x1="1140" y1="530" x2="1150" y2="530"/>
</g>
<!-- fils de l'ampli : LRC et BCLK contournent la carte par le bas -->
<polyline class="w ws" points="470,515 430,515 430,715 860,715 860,470 880,470"/>
<polyline class="w clk" points="470,477 415,477 415,730 845,730 845,500 880,500"/>
<polyline class="w data" points="750,173 830,173 830,530 880,530"/>
<text x="262" y="680" class="lbl" font-size="11">GPIO 14 → LRC (vert)</text>
<text x="262" y="696" class="lbl" font-size="11">GPIO 27 → BCLK (bleu)</text>
<text x="262" y="716" class="lbl" font-size="11">ces deux fils contournent la carte</text>
<text x="760" y="166" class="lbl">GPIO 22 → DIN</text>
<!-- GAIN et SD laissés libres -->
<text x="880" y="650" class="lbl" font-size="11">GAIN : NC → 9 dB</text>
<text x="880" y="666" class="lbl" font-size="11">SD : NC → ampli actif</text>
<polyline class="w gnd" points="1020,620 1020,790"/>
<polyline class="w v5" points="1070,620 1070,830"/>
<!-- découplage conseillé : C1 (réservoir) + C2 (haute fréquence) -->
<g>
<polyline class="w v5" points="1070,655 1290,655"/>
<circle cx="1070" cy="655" r="4" fill="#333"/>
<!-- C1 : électrolytique polarisé -->
<line x1="1180" y1="655" x2="1180" y2="660" stroke="#333" stroke-width="2"/>
<line x1="1164" y1="660" x2="1196" y2="660" stroke="#333" stroke-width="3"/>
<line x1="1164" y1="672" x2="1196" y2="672" stroke="#333" stroke-width="3"/>
<polyline class="w gnd" points="1180,672 1180,790"/>
<text x="1150" y="666" class="lbl" font-size="13">+</text>
<text x="1202" y="650" class="pinused">C1</text>
<!-- C2 : céramique -->
<line x1="1260" y1="655" x2="1260" y2="660" stroke="#333" stroke-width="2"/>
<line x1="1244" y1="660" x2="1276" y2="660" stroke="#333" stroke-width="3"/>
<line x1="1244" y1="672" x2="1276" y2="672" stroke="#333" stroke-width="3"/>
<polyline class="w gnd" points="1260,672 1260,790"/>
<text x="1282" y="650" class="pinused">C2</text>
<text x="1000" y="876" class="title" font-size="13">Découplage conseillé</text>
<text x="1000" y="900" class="lbl">C1 = 470 µF / 16 V électrolytique, faible ESR</text>
<text x="1000" y="920" class="lbl"> polarisé : la bande imprimée est le « − »</text>
<text x="1000" y="940" class="lbl">C2 = 100 nF X7R céramique (code 104)</text>
<text x="1000" y="960" class="lbl">les deux à moins de 3 cm des broches de l'ampli</text>
</g>
<!-- =============================== haut-parleur =============================== -->
<rect x="1210" y="440" width="130" height="120" rx="6" class="box"/>
<text x="1275" y="478" class="title" text-anchor="middle" font-size="15">HP</text>
<text x="1275" y="500" class="sub" text-anchor="middle">4 Ω / 3 W</text>
<text x="1275" y="518" class="sub" text-anchor="middle">(ou 8 Ω / 2 W)</text>
<line class="w gnd" x1="1150" y1="470" x2="1210" y2="470"/>
<line class="w gnd" x1="1150" y1="530" x2="1210" y2="530"/>
<text x="1152" y="462" class="lbl">+</text>
<text x="1150" y="586" class="lbl" font-size="11">« − » jamais à la masse</text>
<!-- =============================== bouton PTT =============================== -->
<rect x="900" y="150" width="160" height="56" rx="6" class="box"/>
<text x="980" y="172" class="title" text-anchor="middle" font-size="14">Bouton PTT</text>
<text x="980" y="192" class="sub" text-anchor="middle">pull-up interne</text>
<polyline class="w gnd" points="750,515 800,515 800,177 900,177"/>
<polyline class="w gnd" points="1060,177 1120,177 1120,225"/>
<text x="756" y="508" class="lbl" font-size="11">→ bouton</text>
<g stroke="#333333" stroke-width="2.4" fill="none">
<line x1="1108" y1="225" x2="1132" y2="225"/>
<line x1="1113" y1="231" x2="1127" y2="231"/>
<line x1="1118" y1="237" x2="1122" y2="237"/>
</g>
<text x="1140" y="231" class="lbl">GND</text>
<!-- =============================== LED d'état =============================== -->
<rect x="900" y="250" width="160" height="56" rx="6" class="box"/>
<text x="980" y="272" class="title" text-anchor="middle" font-size="14">LED + 330 Ω</text>
<text x="980" y="292" class="sub" text-anchor="middle">optionnelle (LED intégrée)</text>
<polyline class="w gnd" points="750,553 815,553 815,277 900,277"/>
<polyline class="w gnd" points="1060,277 1120,277 1120,325"/>
<text x="756" y="546" class="lbl" font-size="11">→ LED</text>
<g stroke="#333333" stroke-width="2.4" fill="none">
<line x1="1108" y1="325" x2="1132" y2="325"/>
<line x1="1113" y1="331" x2="1127" y2="331"/>
<line x1="1118" y1="337" x2="1122" y2="337"/>
</g>
<text x="1140" y="331" class="lbl">GND</text>
<!-- =============================== rails =============================== -->
<line class="w v33" x1="140" y1="750" x2="1340" y2="750"/>
<line class="w gnd" x1="140" y1="790" x2="1340" y2="790"/>
<line class="w v5" x1="140" y1="830" x2="1340" y2="830"/>
<text x="140" y="744" class="lbl" fill="#cc2222">3,3 V</text>
<text x="140" y="784" class="lbl">GND (masse commune)</text>
<text x="140" y="824" class="lbl" fill="#e08214">5 V</text>
<!-- alimentations prélevées sur l'ESP32 -->
<polyline class="w v33" points="750,667 800,667 800,750"/>
<polyline class="w gnd" points="750,629 775,629 775,790"/>
<polyline class="w v5" points="470,667 455,667 455,830"/>
<!-- points de connexion aux rails -->
<g fill="#cc2222"><circle cx="800" cy="750" r="4"/><circle cx="240" cy="750" r="4"/></g>
<g fill="#333333">
<circle cx="775" cy="790" r="4"/><circle cx="170" cy="790" r="4"/>
<circle cx="200" cy="790" r="4"/><circle cx="1020" cy="790" r="4"/>
<circle cx="1180" cy="790" r="4"/><circle cx="1260" cy="790" r="4"/>
</g>
<g fill="#e08214"><circle cx="455" cy="830" r="4"/><circle cx="1070" cy="830" r="4"/></g>
<!-- =============================== légende =============================== -->
<rect x="30" y="852" width="520" height="140" rx="6" fill="#ffffff" stroke="#bbbbbb" stroke-width="1.5"/>
<text x="44" y="876" class="title" font-size="14">Légende</text>
<g class="lbl">
<line class="w v5" x1="44" y1="896" x2="80" y2="896"/><text x="88" y="900">5 V</text>
<line class="w v33" x1="44" y1="918" x2="80" y2="918"/><text x="88" y="922">3,3 V</text>
<line class="w gnd" x1="44" y1="940" x2="80" y2="940"/><text x="88" y="944">masse / logique</text>
<line class="w clk" x1="230" y1="896" x2="266" y2="896"/><text x="274" y="900">horloge bit (SCK / BCLK)</text>
<line class="w ws" x1="230" y1="918" x2="266" y2="918"/><text x="274" y="922">mot (WS / LRC)</text>
<line class="w data" x1="230" y1="940" x2="266" y2="940"/><text x="274" y="944">données audio (SD / DIN)</text>
</g>
<circle cx="52" cy="964" r="4" fill="#333"/>
<text x="88" y="968" class="lbl">connexion électrique — deux fils qui se croisent sans point ne sont pas reliés.</text>
<!-- =============================== avertissements =============================== -->
<text x="580" y="876" class="warn">Masse commune obligatoire entre tous les modules.</text>
<text x="580" y="900" class="lbl">Câblez hors tension. INMP441 en 3,3 V : le 5 V le détruit.</text>
<text x="580" y="920" class="lbl">Fils I2S de 15 cm maximum, éloignés des fils du haut-parleur.</text>
<text x="580" y="940" class="lbl">Alimentation 5 V / 2 A : l'ampli demande jusqu'à 1,2 A sur les crêtes.</text>
<text x="580" y="960" class="lbl">Détails et vérifications : docs/BROCHAGE.md</text>
</svg>

After

Width:  |  Height:  |  Size: 15 KiB