We built a desk clock that talks. It sits on your table, shows the time on a round 360×360 display, and at 8 pm says "take your tablet" in your own voice. No cloud service, no app subscription, no account. Just a small board, a speaker, and firmware that keeps working when the WiFi doesn't.
This post walks through what the device does, how it's put together, and the parts we'd change before shipping it as a product. The full source is on GitHub, and the technical README covers every screen and WiFi state if you want to build one yourself.
What it does• Your voice as the alarm. Upload an mp3, wav, m4a or ogg file from the browser. The browser converts it to mono 16-bit WAV before upload, so the ESP32 never has to decode MP3. That keeps the firmware small and playback reliable.
• WiFi setup over Bluetooth. No credentials compiled into the firmware. The device advertises as DMV-Audio-XXXX over BLE, you send a small JSON payload with your SSID and password, and it saves them to NVS only after a successful connection.
• Web interface at http://dmvaudio.local. Dashboard, Reminders, Audio, Settings and Wi-Fi pages are embedded in the firmware itself. Works on phone and desktop.
• Up to 32 reminders. By weekday or one-time on a date, with 1–10 repeats.
• Eight icons (bell, water, pill, heart, cup, mail, moon, star), picked automatically from the reminder title.
• Animated display. Analog hands plus digital time, next reminder, WiFi status dot, an audio-reactive ring while playing, and a touch STOP button. Runs at about 30 fps.
• Plain HTTP API for scripting reminders and playback.
HardwareThe build is deliberately simple. One board, one speaker, one cable.
• 1 × Waveshare ESP32-S3-Touch-LCD-1.85, speaker version (360×360 round QSPI display, CST816 touch, I2S amp, 16 MB flash, 8 MB PSRAM)
• 1 × Speaker connected to the board's speaker connector
• 1 × USB-C cable for flashing and power
• 1 × 5 V USB power supply
No soldering. The 16 MB flash gives us roughly 10 MB of LittleFS for audio and reminder data, and the 8 MB PSRAM holds the display framebuffer without trouble. The I2S amplifier drives the speaker directly.
Once the firmware is on the board, setup takes a couple of minutes and needs only a phone and a browser.
On first boot the screen shows BLE Setup. Open nRF Connect on your phone, connect to DMV-Audio-XXXX, and write this JSON to the provisioning characteristic as UTF-8 text:{"ssid":"MyWiFi", "pass":"MyPassword"}The device joins the network, saves the credentials, switches Bluetooth off and shows the clock. Then open http://dmvaudio.local, set your time zone under Settings (India is IST-5:30), upload a clip, and add a reminder. The Test button plays it immediately so you can check volume and timing.
Debugging tipThe serial log at 115200 baud prints WiFi attempts, BLE events, NTP sync, reminder firing and touch coordinates, which usually narrows a problem down fast.
How it handles WiFi lossThis is the part we spent the most time on. A reminder clock that stops reminding because the router rebooted is a broken product.
• 0 s: the status dot blinks red; the clock and reminders keep running; WiFi retries continuously.
• 2 min: BLE setup opens in the background; the screen stays on the clock.
• Every 30 s after that: the saved network is retried.
• WiFi back: Bluetooth off, mDNS re-registered, time re-synced.
At boot the device tries a fast-connect using the remembered channel and router address, then a normal scan, up to 3 attempts. A typical restart reaches a synced clock in about 5 seconds. That fast-connect path matters more than it sounds: a full scan adds several seconds to every power cycle, and users notice.
The HTTP APIEverything the web UI does goes through a plain HTTP API, so you can script it from a Raspberry Pi, a Home Assistant automation, or a cron job. No authentication, so keep it on a trusted network.
• GET /api/state: time, WiFi, settings, files, reminders, free space
• GET /api/audio/status: playback state, position, duration
• GET /api/wifi/scan: visible networks
• POST /api/reminders: replace the reminder list
• POST /api/settings: {ntp, tz, vol}
• POST /upload: upload a WAV (multipart, field "file")
• POST /api/play?name=&title=&icon=&repeat=: play a clip
• POST /api/stop, /api/audio/pause, /api/audio/resume: playback control
• POST /api/audio/seek?to=<ms>: seek
A reminder looks like this:{"title":"Drink Water", "time":"08:00", "days":127, "date":"", "file":"drink_water.wav", "repeat":1, "icon":1, "enabled":true}The days field is a bitmask, so 127 means every day of the week.
We kept the stack boring on purpose. Arduino framework on [email protected], C++17, and three libraries, plus LittleFS and NVS from the core:
• GFX Library for Arduino for the QSPI display
• NimBLE-Arduino for BLE provisioning, lighter than the default Bluedroid stack and leaves more RAM for audio
• ArduinoJson 7 for the API and config
• LittleFS for audio and reminders (~10 MB), NVS for WiFi, NTP, time zone and volume
The firmware is split into main.cpp (WiFi, BLE, web server, scheduler), audio.cpp (WAV playback, seek, volume), display.cpp (clock, animations, touch STOP), and web_ui.h (the HTML/CSS/JS embedded in flash). Splitting display and audio into their own translation units kept the main loop readable.
Where it falls shortWe're honest about the gaps, because they're the same gaps any reference design has before it becomes a product:
• Now: after a full power-off, WiFi + NTP are needed once before reminders fire. Planned: use the on-board PCF85063 RTC as a time source.
• Now: no authentication on the web UI or API. Planned: add a password / token.
• Now: one stored WiFi network. Planned: multiple saved networks.
• Now: only one touch action (STOP). Planned: snooze; tap or pick-up gestures using the on-board IMU.
• Now: WAV only on the device (~44 KB/s at 22 kHz). Planned: compressed audio playback.
The RTC one is the biggest. Right now, pull the plug and leave it unplugged overnight, and the device needs a network sync before it can fire anything. The PCF85063 is already on the board; we just haven't wired it into the time path yet.
Taking it furtherThis design is a solid starting point. Getting from a working prototype to a shippable product means power-loss handling, secure provisioning, a custom enclosure, and EMC and certification work. That's where embedded development services earn their keep, not just a board that boots.
If you're building something in this space, such as a medication reminder, a desk companion or a home automation panel on a round display, the patterns here (BLE provisioning, local scheduling, offline-first behaviour) carry over directly.
ContributingIssues and pull requests are welcome. If something misbehaves, include the serial log at 115200 baud. It prints WiFi attempts, BLE events, NTP sync, reminder firing and touch coordinates, which usually narrows the problem down fast.






Comments