Directive 3: ESP32 MQTT auto-reconnect, heartbeat, MQTT-state LED, drop logging

- Reconnect and re-subscribe on every (re)connect; log the subscribe result.
- Heartbeat every 5 min on voice/audio_stream (valid silent PCM frame) so the
  Directive-1 watchdog can tell "device dead" from "nobody spoke". voice_bridge
  treats it as silence and ignores it.
- Retained LWT "offline" / "online" presence on voice/status for layer
  attribution in the watchdog.
- LED: green now means MQTT connected AND publishing. Wi-Fi down = red blink,
  Wi-Fi up but MQTT down = amber blink. Never green without MQTT.
- Log the reason on every drop: PubSubClient state code, or the Wi-Fi
  disconnect reason from the Wi-Fi event.
- Bound the I2S read (50 ms) so Wi-Fi/MQTT housekeeping cannot stall; reset a
  stale streaming state on disconnect (the stuck-green bug).
- Document the MQTT/LED contract in README; flag config.h and led_control.h as
  stale drafts so they cannot creep back as a second config.
- Credentials stay placeholders; nothing secret is committed.

Not flashed/tested: no ESP32 and no arduino-cli is reachable from the build
environment. Hardware-in-the-loop verification needs a human at the device.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
2026-10-14 10:59:19 +11:00
parent 3943b00dac
commit 611fdf812b
4 changed files with 373 additions and 89 deletions

View File

@@ -45,13 +45,35 @@ Location: `~/voice_bridge/` (On Server .13)
## 🚦 LED Status Indicators (ESP32)
The LED Ring provides real-time feedback on the device state.
**Green now means "MQTT connected and publishing", not merely "Wi-Fi joined".**
This is the Directive 3 fix: the old ring could sit on green (streaming) while the
socket was dead and nothing reached the broker.
| Color | State | Trigger |
| :--- | :--- | :--- |
| **Off** | Idle | Listening for sound (VAD). |
| **Green** | Recording | Sound detected above threshold. Streaming audio. |
| **Blue** | Processing | Silence detected. End of stream sent to server. |
| **Rainbow** | Acknowledged | "Wake Word" detected by server OR Command executed. |
| **Red** | Error | Wi-Fi or MQTT connection loss. |
| **Red blink** | Wi-Fi down | Not joined to the access point. Never green. |
| **Amber blink** | MQTT down | Wi-Fi up but the broker link is down. Never green. |
| **Green (steady)** | Idle + healthy | MQTT connected, subscribed, heartbeats publishing. |
| **Green (bright)** | Recording | Sound detected above threshold, streaming audio. |
| **Blue** | Processing | Silence detected, end of stream sent to server, waiting for reply. |
| **Yellow** | Acknowledged | Server replied `OK` (command executed). |
## 🔌 MQTT reliability (Hub Fixes — Directive 3)
- **Auto-reconnect + re-subscribe on every (re)connect.** PubSubClient reopens
the socket but does not restore subscriptions. `connectMqtt()` re-subscribes to
`voice/status` on every connect and logs the result. Without this the client can
hold a live socket with no subscription and go silently deaf.
- **Heartbeat every 5 minutes on `voice/audio_stream`.** The watchdog (Directive 1)
watches that topic, so topic silence means "the publisher is dead", not "nobody
spoke". The heartbeat is a valid silent PCM frame (256 bytes), so `voice_bridge`
treats it as silence and ignores it.
- **Presence.** The device sets a retained Last Will `offline` on `voice/status`
and publishes retained `online` on connect. This gives the watchdog a way to
attribute a failure to the ESP32 layer.
- **Drop logging.** On every connected→disconnected transition the serial console
prints the PubSubClient state code (for example `CONNECTION_LOST`) or the Wi-Fi
disconnect reason code.
## ⚙️ Configuration Details
@@ -59,10 +81,14 @@ The LED Ring provides real-time feedback on the device state.
* **Mic SCK:** GPIO 4
* **Mic WS:** GPIO 5
* **Mic SD:** GPIO 6
* **LED Pin:** GPIO 5
* **LED Pin:** GPIO 1
* **VAD Threshold:** `1000` (Adjust based on noise floor)
* **Soft Gain:** `x6` (Multiplies raw input to match Whisper requirements)
> `config.h` and `led_control.h` are stale early drafts and are **not** included
> by the sketch. `voice_assistant.ino` is the single source of truth. Do not add
> a second copy of the broker/topic/pin config.
### Docker Environment
* **Network:** Internal bridge network between Bridge and Whisper.
* **Models:**
@@ -72,10 +98,19 @@ The LED Ring provides real-time feedback on the device state.
## 🚀 Setup & Commands
**1. Flash ESP32**
Credentials in `voice_assistant.ino` are placeholders in this repo. Set the real
values locally before flashing, and never commit them. Flashing needs the ESP32
physically connected over USB to a machine with `arduino-cli` (this is the step
that cannot be done from the server):
```bash
arduino-cli upload -v -p /dev/ttyACM0 --fqbn esp32:esp32:esp32s3:CDCOnBoot=cdc,USBMode=hwcdc,PSRAM=disabled,FlashMode=dio,DebugLevel=info --input-dir ./build
```
Watch the serial console (`arduino-cli monitor -p /dev/ttyACM0 -c baudrate=115200`)
to confirm the `[mqtt] re-subscribe` and `[heartbeat]` lines.
**2. Start Backend**
```bash
cd ~/voice_bridge