An Esp32-s3 Voice Reminder Device That Keeps Working Offline

Photo of chanchaldada

Made by chanchaldada / IoT

About the project

Most reminder products assume a live internet connection.

Project info

Difficulty: Expert

Platforms: Espressif

Estimated time: 5 hours

License: MIT license (MIT)

Items used in this project

Hardware components

Seeed Studio Xiao Esp32s3 Sense - 2.4ghz Wi-fi, Ble 5.0, Ov2640 Camera Sensor, Embedded Ml Seeed Studio Xiao Esp32s3 Sense - 2.4ghz Wi-fi, Ble 5.0, Ov2640 Camera Sensor, Embedded Ml x 1
The Sparkfun I2s Audio Breakout Board Uses The Max98357a Digital To Analog Converter (dac)  Which Converts I2s (not Be Confused With I2c) Audio To An Analog Signal To Drive Speakers. The I2s Audio Breakout Converts The Digital Audio Signals Using The The Sparkfun I2s Audio Breakout Board Uses The Max98357a Digital To Analog Converter (dac) Which Converts I2s (not Be Confused With I2c) Audio To An Analog Signal To Drive Speakers. The I2s Audio Breakout Converts The Digital Audio Signals Using The x 1
Max98357 I2s Amplifier Module Max98357 I2s Amplifier Module x 1
Rtc Module Rtc Module x 1

Story

A few months back, a client came to us with a problem that sounded simple enough. Their users wanted a small desk device that could play spoken reminders — medication times, meeting alerts, household chores — without relying on a cloud service or a phone that had to stay connected. The catch? It had to keep working when the Wi-Fi dropped or the phone walked out of the room.


We built it on the ESP32-S3. Here's how it went.


The Constraint That Shaped Everything


Most reminder products assume a live internet connection. The phone pushes a notification, the cloud schedules it, and the device is basically a speaker with a screen. That model breaks the moment connectivity does.


Our client wanted the opposite. The device had to hold its own schedule, store its own audio, and fire reminders on time whether or not anything else was reachable. The phone app was for setup and management, not for runtime. That single decision pushed us toward local storage, a real-time clock with battery backup, and a BLE-first architecture for the mobile side.


It also meant the BLE mobile app development work became a core part of the project, not an afterthought. The app had to configure schedules, upload voice clips, and sync time — all reliably, and all without a cloud round trip.


System Block Diagram


```

┌─────────────────────────────────────────────────────────────┐

│                     ESP32-S3 (dual-core, 240 MHz)           │

│                                                             │

│  ┌──────────────┐   ┌──────────────┐   ┌────────────────┐   │

│  │  BLE GATT    │   │  Scheduler   │   │  I2S Audio Out │   │

│  │  Server      │   │  (Zephyr     │   │  → MAX98357A   │   │

│  │  (NimBLE)    │   │   RTOS)      │   │  → Speaker     │   │

│  └──────┬───────┘   └──────┬───────┘   └────────────────┘   │

│         │                  │                                │

│  ┌──────┴──────────────────┴───────┐   ┌────────────────┐   │

│  │  Flash (16 MB) — audio + config │   │  RTC (PCF8563) │   │

│  └─────────────────────────────────┘   └────────────────┘   │

└──────────────┬──────────────────────────────────────────────┘

               │ BLE

      ┌────────┴────────┐

      │  Mobile App     │

      │  (iOS/Android)  │

      │  BLE GATT Client│

      └─────────────────┘


Optional: Wi-Fi STA mode for OTA firmware updates only

```


Hardware Choices


The ESP32-S3 was the right call for a few reasons. It has enough RAM to decode compressed audio on the fly, native USB for flashing and debugging, and a mature BLE stack. We paired it with 16 MB of external flash — enough to hold roughly 40 minutes of 16 kHz mono speech at a reasonable bitrate, which covered the client's use case with room to spare.


Audio output runs through a MAX98357A I2S amplifier into a small 3W speaker. No DAC needed; the amp takes I2S directly. We kept the analog path short and avoided ground loops that showed up in the first prototype as a faint buzz during playback.


For timekeeping, we added a PCF8563 RTC on I2C with a coin cell backup. The ESP32-S3's internal RTC drifts, and reminders that fire two minutes late are worse than useless for medication. The external chip keeps drift under a couple of minutes a year, and it survives full power loss.


The PCB is a four-layer board designed in KiCad. We kept the RF section on the top layer with a proper ground plane underneath, and the antenna keep-out area clear of copper. The first revision had the antenna too close to the flash chip and we saw BLE range drop to about 4 meters. Moving the flash and re-tuning the matching network got us back to a solid 15–20 meters indoors.


The enclosure was a separate 3D design effort — a compact wedge with a downward-firing speaker and a single status LED. We printed several iterations to get the speaker grille right before committing to tooling.


Firmware on Zephyr RTOS


We run Zephyr rather than the default ESP-IDF environment for one main reason: the scheduler and the BLE stack need to coexist cleanly, and Zephyr's threading model made that predictable.


The firmware has three main tasks. The BLE task runs NimBLE as a GATT server, exposing characteristics for schedule entries, audio clip uploads, and current time. The scheduler task wakes on RTC alarms and checks the next due reminder. The audio task pulls the right clip from flash, decodes it, and streams it out over I2S.


One detail worth mentioning: we store audio as 16 kHz mono Opus. Decoding on the ESP32-S3 takes about 8% of one core, which leaves plenty of headroom. Raw PCM would have eaten the flash budget four times over.


The scheduler keeps a rolling window of upcoming reminders in RAM and reloads from flash only when the window runs low. That keeps wake-ups cheap and lets the chip spend most of its time in light sleep, pulling around 1.2 mA average.


The BLE Mobile App


This is where a lot of the project's real complexity lived. The app connects over BLE, reads the current schedule, lets the user add or edit reminders, records or imports a voice clip, and writes it all back.


We built it as a native app on both platforms using a shared protocol layer. The GATT service exposes a small set of characteristics with a simple framing scheme: a header with command type, payload length, and a checksum, followed by the payload. Audio uploads are chunked into 244-byte writes with flow control, because BLE throughput on a busy 2.4 GHz band is not something you can assume.


A few things we learned during BLE mobile app development on this project:


- MTU negotiation matters. Bumping the MTU to 247 bytes cut upload time for a 10-second clip from about 40 seconds to under 8.

- Reconnection needs a plan. Phones drop BLE links constantly — screen locks, app backgrounding, OS power management. The app reconnects and resumes the sync state machine without losing progress.

- iOS and Android behave differently. Android's BLE stack is more forgiving of rapid write bursts; iOS needs explicit delays between certain operations. We built the protocol layer to pace writes conservatively so one codebase works on both.


The app also handles time sync. The phone sends its current time on connect, and the device sets the RTC from it. No network needed on either side.


What Shipped


We delivered a working prototype run of units with firmware, PCB, enclosure, and both mobile apps, plus the flashing and test fixtures for the client's manufacturing partner. The device boots, syncs over BLE in a few seconds, and plays reminders on schedule with the phone nowhere in sight.


The offline-first design turned out to be the right bet. Users treat a reminder device like an alarm clock — they expect it to just work. Keeping the schedule and the audio on the device, and using BLE only for setup, means it does.


If you're building something in this space and want to talk through the BLE app side or the firmware architecture, we're happy to compare notes.



Code

main.cpp

Credits

Photo of chanchaldada

chanchaldada

Chanchal Dada is an IoT developer at DigitalMonk, focused on building smart embedded systems using Arduino and ESP32. She specializes in IoT-based automation, smart monitoring solutions, and real-world hardware-software integration for practical applications.

   

Leave your feedback...