Klock - a Minimal Diy Desk Clock

by The Uncertified Engineer in Circuits > Clocks

269 Views, 1 Favorites, 0 Comments

Klock - a Minimal Diy Desk Clock

Klock.png

I got fed up with desk clocks.

Not the old kind — the kind that just told you the time. I mean the new ones. The ones with companion apps, RGB underglow, Bluetooth sync, weather widgets, Spotify integration, a USB hub built into the base, and a screen bright enough to read by. I had one of those for six months. Every time I glanced at it to check the time I ended up reading a notification, adjusting an animation, or just staring at it because it kept changing. It was a distraction engine wearing a clock's face.

So I built my own. Twelve LEDs, one button, a buzzer the size of a pea, and a piece of cardboard. It tells the time. It tracks the sun. It runs a timer. It does those three things extremely well and then it shuts up and sits there looking good on my desk.

The irony is it's actually smarter than the clock it replaced — it syncs to NTP time servers over Wi-Fi, calculates local sunrise and sunset using real solar math, and automatically shifts its entire color palette from warm ambers to cool blues when the sun goes down. It just doesn't show off about it.

I've been using this on my desk for a few months now. It's the first clock project I've ever built that I didn't disassemble. Here's how to make one.

Supplies

20260504_170057.jpg
ESP32.png

Electronics:

- ESP32 DevKit (any 30 or 38-pin WROOM-32 variant - I used the most generic one I could find)

- 12-LED WS2812B NeoPixel Ring (get the ones with center through-holes, they're easier to mount)

- 1x Momentary tactile push button - NOT a latching switch

- 1x Passive buzzer (cylindrical, labeled PS1240 or similar - must be *passive*, not active)

- Male-to-female jumper wires

- Small piece of perfboard or a breadboard

- USB micro cable for programming


Enclosure (the fun part):

- Thick cardboard or mounting board

- Thin white printer paper or tracing paper (for the diffuser)

- Craft knife and cutting mat

- Ruler, pencil, a compass or round object to trace circles

- PVA glue or a hot glue gun

- Matte black spray paint or black acrylic (optional but it looks much better)


Software:

- Arduino IDE

- Adafruit NeoPixel library (Library Manager)

- OneButton library by Matthias Hertel (Library Manager)

The Philosophy of the Thing

Before we get to wiring, I want to explain one decision that shapes everything else about this build, because it'll help you understand why certain things are done the way they are.


The clock has exactly one button. Not because I ran out of GPIO pins. Because one button is enough.


Single-click adds a minute to the timer. Double-click changes the mode. Long-press starts the timer counting down. That covers everything. There's no menu to navigate, no setting to accidentally change, no "hold for three seconds to enter config mode." You interact with it when you mean to and ignore it the rest of the time.


The firmware is built the same way. The single most important rule in the code: zero `delay()` calls in the main loop. Every timing operation — animations, buzzer notes, button detection, NTP polling — runs as a non-blocking state machine against `millis()`. This matters because the ESP32 is juggling a lot simultaneously: polling the button fast enough to catch a double-click, fading LEDs at ~120 frames per second, playing multi-note melodies on the buzzer, and tracking time. A single `delay()` would cause missed clicks and stuttering animations. The non-blocking approach is what makes the whole thing feel smooth and responsive despite costing about the same as a coffee.


The pixel engine uses three layers: `tgt[]` (where you want to be), `fromBuf[]` (where you were), and `live[]` (what's on the hardware right now). Every renderer writes to `tgt[]` and triggers a 28-step cross-fade. So every clock tick, every mode change, every LED that turns off when the timer counts down — it all eases in instead of snapping. That softness is what makes it look intentional rather than hobbyist.


Alright. Let's actually build it.

Wiring

SmartSelect_20260505_105630_Chrome.jpg
20260504_175710.jpg

Five connections. That's the whole circuit.

NeoPixel Ring
VCC → ESP32 3.3V
GND → ESP32 GND
DIN → ESP32 GPIO 5

Push Button
One leg → ESP32 GPIO 15
Other leg → GND

Passive Buzzer
+ → ESP32 GPIO 18
- → ESP32 GND


At this stage it looks like a mess of jumper wires. That's fine. Get it working first, then we'll tidy it up inside the enclosure.

Configuring the Code

10.png
12.png
11.png

Open the .ino file. Change these five lines before you flash anything:

const char* WIFI_SSID = "YOUR_SSID";
const char* WIFI_PASSWORD = "YOUR_PASSWORD";
const long GMT_OFFSET_SEC = 19800; // your UTC offset × 3600
// e.g. UTC+5:30 → 5.5 × 3600 = 19800
// UTC-5 → -5 × 3600 = -18000
const float LOC_LATITUDE = xx.xxxxx;
const float LOC_LONGITUDE = xx.xxxxx;

Latitude and longitude are used for the solar clock and the automatic day/night theme. You don't need precision — your city's rough coordinates from Google Maps are plenty. The solar algorithm (Spencer 1971 declination + NOAA equation of time) is accurate to about 15 minutes, which is more than enough for an LED to represent the position of the sun.

If your wiring uses different pins than mine, change these too:

#define PIN_NEOPIXEL 5
#define PIN_BUTTON 15
#define PIN_BUZZER 18


Source code: [link]

Flash and Verify

20260504_185237.jpg

In Arduino IDE:


Board: ESP32 Dev Module

Upload Speed: 921600

Partition Scheme: Default


Upload and open Serial Monitor at 115200 baud. A good boot looks like this:

WiFi......... OK
09:42:17
Sunrise ≈ 06:14 Sunset ≈ 19:38
Theme: DAY

The ring will play a rainbow startup animation that fades out, then go straight into the clock mode. Three lit pixels gliding around a dark ring — that's it. Clean, readable, calm.

If Wi-Fi times out: check your SSID and password. If NTP fails: give it one more reset, NTP sync can be slow on the first connection. If the ring shows nothing: double-check the data line connection and the resistor.

Once it's running properly, set it aside and build the enclosure.

Building the Cardboard Enclosure

20260504_191352.jpg
2.png
3.png
20260504_202947.jpg
20260504_203101.jpg

This is the part that turns a breadboard prototype into something you actually want on your desk. The design is a simple two-layer sandwich: a back panel, a spacer ring that creates depth, and a front face with a circular window.

The goal is a small, flat, matte-black square with a glowing circle set slightly recessed behind a paper diffuser. It looks like something you'd pay money for. It's cardboard and paper.

Cutting the pieces:

The ring is about 50mm in diameter. I built my enclosure as an 60mm circle — compact enough to sit unobtrusively on a desk, big enough to handle without fumbling.

Cut a 60mm circular hole centered in the middle. A compass works perfectly.

Cut one piece of white printer paper or tracing paper to 60 mm diameter, this is the diffuser that goes directly in front of the LEDs and turns twelve individual points of light into soft glowing arcs.

Assembling:

Stack and glue in this order, back to front:


Solid back panel (60 mm no hole) — the ESP32, Buzzer and button are fixed to this panel.

Side covering ( 80mm strip of hight 20mm )

Tracing paper diffuser, glued flat across the outside front face to close it up


Use PVA glue and let each layer dry before adding the next. Hot glue works faster but the seams are harder to keep clean.

The button gets a small hole through the back panel of the clock along with a hole for the buzzer — 6–7mm is right for a standard tactile button cap. Push it through, hot-glue the body of the button to the back wall.

Route the wires from the ESP32's USB port out the side so you can power it without lifting the whole thing up.

The Base:

The puck needs something to lean against or it'll just lie flat on your desk. Cut a strip of cardboard 95mm long and about 20mm wide — this is the spine of the base. Score it at 80mm from one end and fold it to create an L-shape. Cut a second identical piece and glue both together for thickness. Then cut a curved notch into the top edge where the cylinder will sit — trace the bottom of the puck to get the curve right. Glue the base to the back panel of the clock so the whole thing leans forward at a natural reading angle. Paint it with the rest of the body. It takes five minutes and costs nothing, but without it the clock looks unfinished.

Finishing:

Two thin coats of matte black spray paint on the outside of the assembled body. Don't paint the diffuser window. Let it dry fully before handling.

The matte black removes the extra noice (light leaking) but I like to keep it in that raw cardboard-style.

That's it. The whole enclosure takes about an hour, costs almost nothing, and is fully repairable with a craft knife and some glue.

The Three Modes

4.png
5.png
6.png

Everything runs from the one button.

Double-click steps forward through the three modes. Each change plays a four-note rising arpeggio and sweeps a comet-tail animation across the ring before the new mode fades in. You always know which mode you're entering.


Mode 1: Minimalist Clock


Twelve LEDs, twelve hours. LED 0 is 12 o'clock. Three hands, gliding.

Hand Day Night
Hour Warm gold Electric blue
Minute Burnt orange Deep indigo
Second Cream white Icy pale blue

The second hand doesn't jump — it uses fractional sub-pixel blending to move continuously, bleeding onto the adjacent LED in proportion to its position between ticks. It looks like an analog sweep. When two hands share an LED their colors blend additively, so you can always read both.

Unlit LEDs are fully off. The contrast between the three bright hands and the dark ring is the whole aesthetic. It reads instantly from across a room.


Mode 2: Solar / Lunar Clock


The bottom six LEDs represent the horizon. A single pixel represents the sun during the day, the moon at night. It moves across the upper arc from east (right) to west (left) based on your real local sunrise and sunset times pulled from NTP.

At noon, the sun is at the top. An hour before sunset it's near the western edge. Just after midnight the moon is at the top and slowly drifting toward dawn. It's a genuinely useful piece of information displayed in the most minimal way possible — one pixel, moving.

This is the mode I leave it on most often. It does a better job of telling me what time of day it actually feels like than the clock face does.


Mode 3: Timer


Single-click adds one minute, lights one more LED. The ring breathes slowly while you set it — a sine-wave pulse so you know it's ready and waiting. Maximum 12 minutes, one per LED.

Long-press starts it. The color shifts from idle cyan/amber to a running color, and LEDs extinguish one by one as each minute passes. With three minutes left it shifts to red or violet to warn you.

At zero: three buzzer notes, six flashing pulses that dim and fade out. Calm but impossible to miss.

Automatic Day / Night Themes

7.png
8.png
9.png

There's nothing to configure here. Every 60 seconds the firmware checks the current NTP time against the calculated sunrise and sunset for your coordinates. When the sun goes below the horizon, the entire color palette switches — all three modes, simultaneously.

Daytime: warm golds, ambers, burnt oranges. It matches morning light. Nighttime: deep blues, indigos, icy pale blue. It matches a dark room.

You'll catch it changing once and think "oh, nice." Then you'll forget it's a feature. That's exactly what it's supposed to do.

Troubleshooting

LEDs flickering randomly — check your DIN data line connection. Adding a 300Ω resistor in series on the data wire can also help if the problem persists.

Double-click isn't registering — OneButton's default window is 400ms which is tight. Add btn.setDoubleClickTicks(600) in setup() after the button attachment lines.

Time is off by a whole number of hours — GMT_OFFSET_SEC is wrong. Make sure it's in seconds, not hours. UTC+5:30 is 19800, not 5.5.

Buzzer makes no sound at all — it's an active buzzer, not a passive one. Active buzzers only produce a single fixed tone regardless of the signal from tone(). Swap it. Passive buzzers are usually unmarked or labeled PS1240. Active buzzers often have a vent hole on top and a PCB inside.

Wi-Fi never connects — the ESP32 only supports 2.4GHz networks. If your router is 5GHz only, the connection will always fail.

Final Thoughts

lv_0_20260505170509.gif

Total cost: around $11 including the ESP32. Build time: an afternoon. The cardboard enclosure cost me nothing — it came from the back of a sketchpad I'd already emptied.

The desk clock it replaced had a companion app, a settings menu, and a power brick the size of a paperback book. This one has a USB cable and a button.

I check the time on this thing dozens of times a day and it has never once made me pick up my phone or open a tab or do anything other than know what time it is. That sounds like a low bar. It turns out it isn't.

Build it plain first, get it working, then make the enclosure. The enclosure is where it stops being a project and starts being a thing


Questions? Drop them in the comments below.