WhisperBridge
WiFi/MQTT-to-BLE bridge that lets Home Assistant control a Vent-Axia Svara fan via a one-shot boost command — no cloud subscription required.
// Overview
WhisperBridge is an ESP32 firmware that acts as a protocol bridge between a home automation network and a Vent-Axia Svara bathroom fan. The fan exposes a proprietary BLE GATT interface — it has no WiFi or MQTT support. WhisperBridge fills that gap: it connects to the home network, subscribes to an MQTT topic, and translates each ON command into the correct BLE authentication and boost command sequence targeted at the fan by MAC address.
On first boot with no stored WiFi credentials, the device starts a WPA2 captive-portal access point (WhisperBridge-Setup) at 10.0.0.1. The user connects, scans nearby networks, picks one, and saves credentials — the device then restarts in station mode. In station mode it registers as whisperbridge.local via mDNS, starts ArduinoOTA, and publishes Home Assistant MQTT auto-discovery so the fan appears automatically as a switch entity with the mdi:fan icon.
The BLE authentication and boost sequence blocks for hundreds of milliseconds. Rather than stalling the main loop, this work runs in a dedicated FreeRTOS task (ble_boost, 8 KB stack). The main loop posts a task notification to trigger a run; the BLE task atomically updates _running and _lastSuccess flags that the loop reads safely without locks. Once the task completes, the loop detects the falling edge on _running and publishes OFF to the state topic to keep Home Assistant in sync.
// Key decisions
Why NimBLE over the default BLE stack?
NimBLE uses significantly less flash and RAM than the default ESP32 BLE stack, leaving more headroom for application logic. It also has a cleaner API for GATT client operations.
Why a dedicated FreeRTOS task for the BLE sequence?
The entire BLE sequence — connect, authenticate, command, disconnect — blocks for several seconds. Running it on a dedicated FreeRTOS task (ble_boost, priority 5, 8 KB stack) keeps the Arduino loop task free to service the MQTT client, OTA updates, and REST requests without any stall. The task wakes on xTaskNotifyGive and blocks on ulTaskNotifyTake between commands.
Why Emulated Hue for Alexa?
Avoids any cloud dependency or Alexa skill development. Emulated Hue presents devices as Philips Hue lights on the local network, which Alexa discovers natively. No subscription, no account linking.
Why not use the fan's cloud API?
The whole point is local-only control with no cloud subscription. BLE gives direct device access, and MQTT keeps everything on the local network.
Why defer ESP.restart() to loop()?
Calling ESP.restart() directly inside an AsyncWebServer callback would execute on the lwIP network task, which crashes the device. The s_pendingRestart flag lets loop() call it safely from the Arduino task on the next iteration.