The Smartest Clock You'll Ever Need

A True Embedded Systems Experience

Don't We Have Enough Smart Things?

For the past couple of years, one of the more prominent pieces of technology on my kitchen counter has been a pint-sized information hub. Marketed as a device for communication and connection, it did far more than tell the time: it displayed favorite photos, helped managed grocery lists, enabled video calls with friends and family, and perhaps most impressively sold me products I didn't know I needed. It even came equipped with a voice assistant, allowing conversations from across the room, whether intentional or not.

I found the device particularly intriguing because of the moment in which it was released. It arrived too late to fully participate in the Internet of Things boom that swept through major tech companies, yet too early to benefit from the rise of generative AI and agentic systems. Hamstrung by its reliance on rigid, natively programmed applications, it refused to play nicely with other smart assistants or calendars and ultimately became little more than a dust-collecting artifact on my kitchen counter.

The catalyst for this project came from a recent initiative by the device's manufacturer: a push for all customers to upgrade to a premium subscription, offered free for a limited time. The campaign was deliberately disruptive. Every interaction - screensavers, menus, even video calls home to relatives - came with a persistent nudge to buy into the "latest and greatest." The advertisting was so relentlessly effective that it motivated me to finally remove the device from its dusty perch and replace it with something of my own design - one where everything could be truly custom.

This project evolved into a multi-month exploration of product design, embedded systems, and the realities of taking an idea from concept to completion. In this article, I'll walk through the highlights of that journey and conclude with a reflection of the final result.

Building the Core: ESP32 and HUB75-D

The two primary components of this project are the display and the processor. For the processor, I used a Hiletgo ESP32, which you may recognize from my Secure Boot article. This board offers a high degree of flexibility along with onboard Wi-Fi connectivity, making it well suited for handling both the display driving and application logic of my smart clock.

For the display, I selected an Adafruit 64x32 RGB LED Matrix (hereafter referred to as the Matrix) with a 5mm pitch.

Picture of the Adafruit 64x32 RGB LED Matrix with 5mm pitch

Picture of the Adafruit 64x32 RGB LED Matrix with 5mm pitch

Picture of the Hiletgo ESP32 with Wifi and Bluetooth

Picture of the Hiletgo ESP32 with Wifi and Bluetooth

The first step in this project was connecting the ESP32 to the Matrix. Fortunately, there are many online guides that document this process in detail. Below are some key takeaways from my own wiring experience.

First, it's important to understand that this Matrix is actually composed of two internally wired 16x32 sections. Because of this, there are two separate inputs for each color channel (red, green, and blue), as well as five address lines used to select which rows are currently being refreshed. Both sections share a common clock and latch signal, which proved helpful later in development.

The matrix uses a HUB75 interface, exposed via a 16-pin ribbon cable on the back of the panel. These pins include:

Not shown in the images is a separate 5V power input and ground, which I used to power the Matrix throughout the development process.

Most of the Matrix signals are connected directly to GPIO pins on the ESP32. Of the three available ground pins on the HUB75 connector, only two are tied to the ESP32's ground; the third is left unconnected, as it is primarily intended for daisy-chaining multiple panels. For the clock, latch, and E address signals, I added 220Ω resistors to improve signal stability and help with voltage regulation when interfacing with the ESP32.

With the hardware wired, the next challenge was rendering text on the display. To simplify the process, I used the MatrixPanel_I2S_DMA library, which abstracts away much of the low-level timing and refresh logic required by HUB75 panels. The basic drawing workflow looks like this:

  1. Define colors in memory. The MatrixPanel_I2S_DMA library uses the color565 format, where standard 8-bit RGB values (0-255) are converted into a 16-but color representation compatible with the Matrix.
  2. Clear or initialize the background using fillRect. Early in development, I filled the entire display using the Matrix's full width and height. Later, this approach was refined to better match the panel's optimal refresh behavior.
  3. Position the cursor using setCursor before rendering any text.
  4. Render text by setting the desired text color (setTextColor), text size (setTextSize), and outputting characters using print.

This library made it significantly easier to focus on layout and functionality rather than the underlying display protocol, allowing rapid iteration as the project evolved.

Picture of the code block used to write text to the Matrix

The code block used to write text to the Matrix

Wiring diagram for the Matrix and ESP32

Wiring diagram for the Matrix and ESP32

Click here to see the ESP32 to Matrix wiring strategy.

ESP32 to Matrix Wiring Strategy

Matrix ESP32 Description
R1 25 Pin controlling the red LEDs for the top half of the Matrix.
G1 26 Pin controlling the green LEDs for the top half of the Matrix.
B1 27 Pin controlling the blue LEDs for the top half of the Matrix.
GND GND Pin connecting to board ground voltage.
R2 14 Pin controlling the red LEDs for the bottom half of the Matrix.
G2 12 Pin controlling the green LEDs for the bottom half of the Matrix.
B2 13 Pin controlling the blue LEDs for the bottom half of the Matrix.
GND Disconnected Pin connecting to ground voltage between chained Matrices. I left this pin intentionally disconnected since I am not using a chain.
A 23 Control signal A. Used in conjunction with the B/C/D/E control signals to identify which row to display in binary. This signal is high when rows on the bottom half of the Matrix are being displayed.
B 22 Control signal B. Used in conjunction with the A/C/D/E control signals to identify which row to display in binary. This signal is high when rows 8-15 or 25-32 are being displayed.
C 05 Control signal C. Used in conjunction with the A/B/D/E control signals to identify which row to display in binary. This signal is high when rows 5-8, 13-16, 21-24, or 29-32 are being displayed.
D 17 Control signal D. Used in conjunction with the A/B/C/E control signals to identify which row to display in binary. This signal is high when rows 3-4, 7-8, 11-12, 15-16, 19-20, 23-24, 27-28, or 31-32 are being displayed.
Clock 16 Clock signal. Used to determine when new data should be shifted to color signals. In the code, I have adjusted the ESP32 to send new data on the falling edge of CLK, this way there are no race conditions or strange artifacts on the display when shifting data.
Latch 04 Latch signal. This signal is used as a timing reference for color data to be displayed on the Matrix. When Latch is high, data is displayed.
OE 15 Control signal E. Used in conjunction with the A/B/C/D control signals to identify which row to display in binary. This signal is high when an odd-numbered row is being displayed.
Ground Ground Pin connecting to board ground voltage.

Programming the Essentials: Connections and Expressions

Wi-Fi

With the ESP32 successfully wired to the Matrix, the next step was programming the display to function as a fully fledged clock. One of my primary goals for this project was to implement a world clock synchronized using the Network Time Protocol (NTP). To accomplish this, the ESP32 needed to connect to the Internet, query an NTP server, and render the resuting local time on the Matrix.

There are numerous examples available that demonstrate how to connect an ESP32 to a local Wi-Fi network and perform a simple HTTP or NTP request. As a result, combining these examples with the text-rendering functions described in the previous section was straightforward, though still a critical milestone in the project.

From a computer security perspective, the need to hardcode Wi-Fi credentials directly into the firmware immediately raised concerns. Embedding sensitive network information in source code is both inflexible and insecure, so I needed a more robust solution. This led me to WiFiManager (wm), a library that temporarily turns the ESP32 into an access point (AP) to facilitate a secure network configuration. At a high level, WiFiManager works as follows:

Scrolling Text

As development of the smart clock continued, I wanted to add support for rendering long strings of text, such as daily quotes or world clock details, in a way that was both readable and visually appealing. I quickly discovered that attempting to display large amounts of static text on a small Matrix led to cluttered and unattractive results. To address this, I implemented a cubic easing-based scrolling function that smoothly scrolls text across the display while fading it in and out. Below is a high-level overview of how this system works.

A snippet of the scrolling code and an animation of the end result is shown below.

Picture of the code block used to write and scroll through the World Clock

The code block used to write and scroll through the World Clock

Animation for the World Clock scrolling program

Animation for the World Clock scrolling program

Diving Deeper: Peripheral Accessories

Picture of a small enclosed Piezo with Wires. Courtesy of adafruit.com

Now that the main programming has been fleshed out, the next step is to think of some additional functionality to include in the smart clock design that we can't get through programming alone. This led me to purchase some nifty sensors that added another level to the intricacy of this project: some buttons, a piezo for alarms, a battery backup for keeping time without power, and a dual temperature/elevation sensor to transform the smart clock into a functional weather station.

First, I grabbed some simple push buttons left over from my Arduino starter kit and created a quick toggle system for switching the Matrix between different screens. The buttons became a central part of the project as I tacked more sensors and functionality into the project; this way, the transition between different items seemed more natural when instantiated by a button press.

What is a clock without an alarm? I answered this question when I added a Piezo buzzer to the mix, also left over from my Arduino kit. This allowed me to install a quick alarm configuration section into the ESP32's programming, complete with setting alarm times through the WiFiManager captive portal and quick snooze using the push buttons.

Picture of a BMP280 sensor by SunFounder. Courtesy of digikey.com

Next, I wanted to take it a little further and add a new sensor that would give me the current temperature of the room. That's what led me to add a BMP280 to my project. The BMP280 is a digital sensor that measures temperature and atmospheric pressure. This allows me to not only measure the temperature of the clock, but I can do some quick math to use the atmospheric pressure to determine the clock's elevation. This sensor is a small, low-power device that operates in both SPI and I2C, transforming my clock into a miniature weather station, barometer, and altimiter all in one.

For this project, I wanted to use the BMP280 in I2C mode. This involved making a couple of conscious decisions when it came to wiring and programming:

Picture of a DS3231 sensor by Analog Devices. Courtesy of digikey.com

While EEPROM helps keep user preferences stored in non-volatile memory, what if we need more resilience in data protection? Enter the DS3231, a real-time clock (RTC) that features highly accurate date and time registers. The DS3231 features a cell battery backup, so it can maintain a very accurate measure of time over long periods of activity (or inactivity). For example, if we leave the smart clock unplugged for a couple of months and plug it back in later, we can expect the DS3231 to keep an accurate time down to fractions of a second. This is really important, considering the primary function of the smart clock is, well, keeping time.

Like the BMP280, I wanted to use the DS3231 in I2C mode. There were some modifications I made to the wiring and the code to make this happen:

Below are some programming snippets and a wiring diagram showing how these additional components factored into the final product.

Picture of the code snippets used for the Buttons, Piezo, BMP280, and DS3231.

Picture of the code snippets used for the Buttons, Piezo, BMP280, and DS3231.

Wiring diagram for the Buttons, Piezo, BMP280, and DS3231.

Wiring diagram for the Buttons, Piezo, BMP280, and DS3231.

Click here to see the accessory wiring strategy.

Accessory Wiring Strategy

Accessory ESP32 Description
Button 1 00 Pin listening for a falling edge (button press) from Button 1. When this button is pressed, the clock will alternate between displays (world clock, temperature/elevation, stock ticker, etc.)
Button 2 02 Pin listening for a falling edge (button press) from Button 2. When this button is pressed, the ESP32 will refresh its Wi-Fi credentials and reboot, opening the WiFiManager captive portal for 2 minutes. The clock will fall back to RTC time via DS3231 until an NTP server can be reached.
SCL 21 Pin controlling the Serial Clock (SCL) for both the BMP280 and DS3231. The Serial Clock pin helps the BMP280 poll for changes in temperature/elevation as well as the DS3231 for knowing when to update the current time.
SDA 19 Pin controlling the Serial Data (SDA) for both the BMP280 and DS3231. The Serial Data pin helps the BMP280 update the current changes in temperature/elevation as well as the DS3231 for writing the current time to memory.
Piezo 09 Pin controlling the output of the Piezo buzzer. When this pin transmits a signal, it is emitted by the Piezo buzzer as an audible square wave. This is how the smart clock broadcasts alarms.

Putting It All Together: What Happens Now?

I eventually reached a critical point in the development process where progress began to stall. Several challenged emerged simultaneously, and together they delayed the project's completion. Three primary issues, in particular, prevented me from releasing this project sooner:

  1. Power Management Challenges: During development, I relied on two separate power sources: one for the Matrix and another for the ESP32. Problems arose when it came time to consolidate these into a single, unified power solution. Doing so required more complex wiring and careful power regulation than I had initially anticipated. While this became a valuable learning experience in proper power management, it ultimately proved too cumbersome to integrate cleanly into the final design.
  2. Enclosure and Physical Design: I originally planned to include a custom-designed back cover for the clock, modeled in CAD and inspired by similar projects I had seen. However, the physical constraints of my 3D printer quickly became a limiting factor. Fitting all of the components - especially the tall battery backup - into a compact, functional enclosure was more challenging than expected. As a result, I pivoted to a simpler solution: a set of support legs extending from the rear corners, which allowed the clock to stand upright while preserving accessibility and airflow.
  3. Wiring Issues and Lost Momentum: The most significant factor in the project's delay was a loss of momentum near the finish line. The combination of power challenges, enclosure constraints, and the general demands of work and daily life pushed this project onto the back burner. Compounding these issues was a wiring failure that eventually prevented reliable communication with the ESP32 containing the firmware. At that point, I was left with a functional clock that I could no longer easily update or iterate on, further dampening motivation.

Despite these setbacks, the project ultimately delivered everything I wanted to replace the infotainment device on my kitchen counter. The clock features timekeeping, an audible buzzer, a scrolling world clock, motivational quotes, and even a stock ticker. This project fully leverages the ESP32's Wi-Fi and display capabilities.

Perhaps most importantly, the final stages of this project reinforced how critical time management and realistic planning are, especially for creative and tehcnically demanding builts. While the challenges were frustrating at times, they provided valuable lessons that will carry forward into future project. With clearer expectations and better planning, each new build becomes a little easier - and that, ultimately, is all part of the learning process.

Picture of the Smart Clock in Development

In this photo, I had just finished wiring together the Matrix. I mimicked the test layout from the developer to make sure all of my channels were working.

Picture of the Smart Clock in Development

In this photo, I had just finished installing the DS3231 and displayed readings on the top page of the clock. I was also experimenting with different font sizes here: the clock face is set to font size 2, and each number has a different color based on the time of day.

Sources

Adafruit.com: Pictures of matrix, Piezo

Digikey.com: Pictures of BMP280, DS3231

Hiletgo.com: ESP32 picture

wokwi.com: Wiring schematics and board layouts

Published December 2025