# Fluister v2 — groepsgesprek · fluisterketting · geluidsinstructies · ESP32-devices

Een kring van stemmen. De regie schakelt live tussen:

| modus | wat hoor je |
|---|---|
| **Groepsgesprek** | iedereen hoort iedereen (behalve zichzelf) |
| **Fluisterketting** | je hoort alleen je buur in de kring; de kring wordt elke *n* seconden geschud |
| **Stilte** | niemand hoort elkaar, alleen de instructies |

Daarbovenop kan de regie **geluidsinstructies** in de oren leggen (de stemmen worden dan zachter gezet):

- **opgenomen instructies**: audiobestand uploaden (mp3, wav, m4a, ogg…) of rechtstreeks in de browser opnemen. Afspelen naar *iedereen*, naar *het begin van de ketting* (dan fluistert de kring het verder) of naar *één persoon*.
- **live instructie**: knop ingedrukt houden en spreken (of aan/uit-modus).
- **tekstinstructie**: verschijnt groot op telefoons en laptops.

Deelnemers kunnen een telefoon/laptop zijn, of een **zelfgebouwd Fluister-device** (ESP32 + microfoon + koptelefoon of luidsprekertje).

---

## 1. Waarom de audio nu via de server loopt

De vorige versie verbond telefoons rechtstreeks met WebRTC. Een ESP32 kan geen WebRTC, en de server moet instructies in ieders oor kunnen mixen. Daarom:

- elke deelnemer (browser of device) stuurt **16 kHz mono 16-bit PCM** in blokjes van 20 ms over een WebSocket (`/ws`);
- de server mixt per luisteraar wat die moet horen (groep / buur / stilte + instructies) en stuurt dat terug;
- **geen TURN/STUN meer nodig**: alles gaat over gewone HTTPS (poort 443), dus het werkt achter elke wifi of 4G;
- vertraging ≈ 150–250 ms (netwerk + 100 ms buffer), prima voor gesprek en fluisteren;
- bandbreedte ≈ 256 kbit/s per deelnemer in elke richting (20 deelnemers ≈ 5 Mbit/s op de server).

---

## 2. Server installeren

Vereist: Node.js 18+ (of Docker).

```bash
npm install
HOST_PIN=kies-een-pincode npm start      # http://localhost:3000  — regie op /host
npm test                                  # simulatietest van alle modi
```

Omgevingsvariabelen: `PORT` (3000), `HOST_PIN` (pincode regie), `SHUFFLE_SEC` (30, standaard schudinterval), `DATA_DIR` (./data — hier komen de instructies).

### Op experiments.samenschool.org/fluister/ (pm2)

De app werkt ook onder een submap. Code in `/var/www/experiments/fluister-v2`, nginx stuurt `/fluister/` door naar poort 3020 (zie `nginx-experiments.conf`). De instructies worden buiten de webmap bewaard (`/var/lib/fluister-v2`). Devices: server `experiments.samenschool.org`, pad `/fluister/ws`.

### Op de Hostinger-server met Docker Compose

```bash
cd /home/jo/fluister-v2
echo "HOST_PIN=kies-een-pincode" > .env
docker compose up -d --build
```

De container luistert op `127.0.0.1:3020`. Voeg in nginx de WebSocket-upgrade toe — zie `nginx-fluister.conf` (belangrijk: `location /ws` met `Upgrade`/`Connection` en `client_max_body_size 50m` voor uploads). Daarna `sudo nginx -t && sudo systemctl reload nginx`.

> Tip: draai v2 eerst naast de huidige versie op een tweede subdomein (bv. `fluister2.samenschool.org`). Werkt alles, zet dan `fluister.samenschool.org` naar poort 3020 en stop de oude pm2-app.

Met Cloudflare ervoor: WebSockets staan standaard aan; niets extra nodig.

### Of met pm2

```bash
npm install --omit=dev
HOST_PIN=kies-een-pincode PORT=3020 pm2 start server.js --name fluister-v2
```

---

## 3. Gebruik

- **Deelnemers**: `https://fluister.samenschool.org/?room=naam-van-de-kring` → naam invullen → *Doe mee*. Koptelefoon of oortjes aangeraden (anders rondzingen; zonder koptelefoon wordt de micro automatisch gedempt terwijl er geluid uit de speaker komt).
- **Regie**: `https://fluister.samenschool.org/host?room=naam-van-de-kring` → pincode. Daar: modus wisselen, kring schudden, instructies beheren, live spreken, tekst sturen, meeluisteren, QR-code om mensen uit te nodigen.
- Meerdere kringen tegelijk kan: elke `room` is een aparte kring.

---

## 4. Het Fluister-device (ESP32-WROOM-32)

### Bouwvarianten (kies in `config.h` → `#define VARIANT`)

| variant | onderdelen naast de ESP32 | ongeveer |
|---|---|---|
| **2 · oortje** (standaard, aanrader) | INMP441 + PCM5102A + eigen koptelefoon/oortjes. Knop = BOOT-knop, led = blauwe led op het bordje | €10–14 |
| **3 · luidspreker** | INMP441 + MAX98357A + luidsprekertje 4–8 Ω | €10–14 |
| **1 · volledig** | alles hieronder: koptelefoon én luidspreker, schakelaar, potmeter, RGB-led | €20–30 |

Blauwe led (variant 2 en 3): snel knipperen = verbinden, traag knipperen = instructie, uit = micro uit, aan = in de kring.

### Componenten (volledige variant)

| # | onderdeel | waarom deze |
|---|---|---|
| 1 | **ESP32-WROOM-32 DevKit** (DevKitC, 30 of 38 pinnen) | heb je al |
| 1 | **INMP441** I2S MEMS-microfoon (breakout) | **de aanbevolen microfoon**: digitaal (I2S), dus geen ruis van de zwakke ESP32-ADC; omnidirectioneel, 24-bit, SNR 61 dB, goedkoop en goed ondersteund. Alternatief van hogere kwaliteit: ICS-43434. *Niet*: MAX9814/analoge micro's (ruisige ADC) of SPH0645 (timingproblemen op ESP32). |
| 1 | **PCM5102A** I2S-DAC-module **met 3,5 mm-jack** (de paarse “GY-PCM5102”) | zuiver geluid voor koptelefoon/oortjes; jack zit er al op |
| 1 | **MAX98357A** I2S-versterker (3 W, klasse D) | stuurt het ingebouwde luidsprekertje rechtstreeks digitaal aan |
| 1 | luidsprekertje **4–8 Ω, 2–3 W**, Ø 28–40 mm | |
| 1 | drukknop (6 of 12 mm) | micro aan/uit · 3 s = wifi instellen |
| 1 | schuif- of tuimelschakelaar (of 3,5 mm-jack met schakelcontact) | koptelefoon ↔ luidspreker |
| 1 | potentiometer **10 kΩ lineair** + knop | volume |
| 1 | WS2812B-LED (1 “NeoPixel”, optioneel) | toont modus, instructie, mute, verbinding |
| 1 | elco 470–1000 µF / 10 V | stabiele 5V voor de versterker |
| – | perfboard of breadboard, draadjes, USB-kabel, **powerbank 5 V ≥ 1 A**, behuizing met gaatje voor de micro | |

Beide audiomodules hangen parallel op dezelfde drie I2S-draden; de software zet de luidspreker uit (pin SD van de MAX98357A) als de schakelaar op *koptelefoon* staat. Zo kan de koptelefoon altijd gewoon in de jack van de PCM5102A.

### Bedrading

Zie **`firmware/docs/bedrading.svg`**. Samengevat:

| module | pin | ESP32 |
|---|---|---|
| INMP441 | VDD / GND / L/R | 3V3 / GND / GND |
| | SCK / WS / SD | GPIO 32 / 33 / 35 |
| PCM5102A | VIN / GND | 5V (VIN) / GND |
| | BCK / LCK / DIN | GPIO 26 / 25 / 22 |
| | SCK, FLT, DEMP, FMT | GND |
| | XSMT | 3V3 (= niet gedempt) |
| MAX98357A | VIN / GND | 5V / GND |
| | BCLK / LRC / DIN | GPIO 26 / 25 / 22 (zelfde als PCM5102A) |
| | SD | GPIO 21 |
| drukknop | – | GPIO 4 ↔ GND |
| koptelefoon-schakelaar | – | GPIO 13 ↔ GND (dicht = koptelefoon) |
| potmeter | loper | GPIO 34 (uiteinden 3V3 en GND) |
| WS2812B | DIN | GPIO 23 (voeding 3V3, GND) |

**PCM5102A-jumpers**: op de achterkant van de paarse module zitten soldeerpads H1L–H4L. Standaard: FLT = L, DEMP = L, **XSMT = H**, FMT = L. Staat XSMT op L, dan blijft hij stil. Zorg ook dat de pin **SCK aan GND** hangt (sommige modules hebben daar een brugje voor).

Microfoon: monteer de INMP441 met het gaatje naar buiten, zo ver mogelijk van de luidspreker, liefst met een stukje schuim ervoor.

### Firmware flashen (Arduino IDE 2)

1. *Boards Manager* → installeer **esp32 by Espressif Systems, versie 3.x** (de code gebruikt `ESP_I2S.h` uit core 3).
2. *Library Manager* → installeer: **WebSockets** (Markus Sattler), **WiFiManager** (tzapu), **ArduinoJson** (Benoit Blanchon, v7), **Adafruit NeoPixel**.
3. Open `firmware/fluister-device/fluister-device.ino`. Pas zo nodig `config.h` aan (standaardserver, pinnen, versterking).
4. Board: **ESP32 Dev Module** · Partition Scheme: **Huge APP (3MB No OTA)** · Upload.
5. Serial Monitor op **115200** om te zien wat er gebeurt.

PlatformIO kan ook: `firmware/platformio.ini` (gebruikt het pioarduino-platform voor core 3).

### Eerste keer instellen

Het device maakt een wifi-netwerk **`Fluister-XXXX`**. Verbind met je telefoon → het portaal opent (of surf naar `192.168.4.1`) → kies je wifi en vul in: server (`fluister.samenschool.org`), poort (`443`), wss (`1`), kring en naam. Wordt bewaard.
Op een nieuwe plek: **knop 3 seconden ingedrukt** (of ingedrukt houden bij het aanzetten) → portaal opnieuw.

Lokaal testen zonder internet: server op je laptop (`npm start`), in het portaal: server = IP van je laptop, poort `3000`, wss `0`.

### LED

| kleur | betekenis |
|---|---|
| oranje knipperend | verbinden met wifi/server (vast oranje = wifi-portaal) |
| cyaan (helderder als je spreekt) | groepsgesprek |
| paars (knippert = nog geen buur) | fluisterketting |
| zwak wit | stilte |
| wit pulserend | er klinkt een instructie |
| rood | micro uit |

### Afstellen

- Micro te stil/te luid → `MIC_GAIN` in `config.h` (8–32).
- Luidspreker zingt rond → lager `SPEAKER_MAX`, of `ECHO_DUCK` kleiner. Met koptelefoon is er geen rondzingen.
- Hapert het geluid → kijk in de Serial Monitor naar `underruns` en `rssi` (wifi-signaal; beter dan −70 dBm).

---

## 5. Bestanden

```
server.js                  server: kringen, mixer, instructies, API
public/index.html          deelnemerspagina
public/host.html           regiepagina
public/fluister-client.js  gedeelde client (WebSocket + audio + kring-visualisatie)
public/fluister-worklet.js audio-thread: micro → 16 kHz, 16 kHz → speaker
test/sim.js                simulatietest (npm test)
test/browser.js            browsertest met Playwright
firmware/fluister-device/  ESP32-code (fluister-device.ino + config.h)
firmware/docs/bedrading.svg
Dockerfile, docker-compose.yml, nginx-fluister.conf
```

### Protocol (voor wie er zelf iets op wil aansluiten)

WebSocket `wss://host/ws`. Eerste bericht (JSON):
`{"t":"hello","room":"fluister","name":"Ada","kind":"web|device","role":"participant|host","pin":"…"}`
Daarna binaire berichten = 16 kHz mono Int16 LE PCM (veelvoud van 320 samples), in beide richtingen. De server stuurt elke 20 ms een frame van 640 bytes. Controle: `mute`, en voor de regie `mode`, `shuffle`, `shuffleEvery`, `duck`, `play`, `stop`, `live`, `listen`, `text`, `kick`. De server stuurt `welcome`, `state`, `levels` en `text`.
