How I Made a Totally Practical, WiFi-Calibrated Mini Wall Clock For $20
w/MicroPython and ESP8266; not perfect but still works fantastic, beautifully and accurate
I’ve made and programmed quite a few DIY WiFi clocks . The core code is always the same, but you can slap different displays on it. I have made clocks using very small 0.96" OLED display, with shiny HD44780 LCDs, with matrix LED display, even one with a motorized flip clock. I have one in my office using a RGB LCD which change colors like rainbow.
However, I wanted to make something very practical for myself. The problem with my other clocks was that the Maker-oriented display modules are usually too small to see beyond a couple of meters. They are fine on your desk, but not easy to see across the room.
So this is what I’ve came up with:
The Modules
- Mcrocontroller: a WeMos D1 mini (ESP8266)
- a TM1637 4-digit 7-segment LED display
- a NeoPixel (WS2812) ring with 24 RGB LEDs
- a BH1750 ambient digital light sensor (GY-30 and GY-302 are functionally the same)
- a DS3231 real time clock (RTS) module
- a push button
- a mini breadboard and jumper wires
The clock does the following things:
- Look up time with NTP protocol once an hour and save the time in the RTS module. The RTS has a battery so the time will be persistent even if the system is turned off.
- Display time on the 7-segment LED display and the NeoPixel ring with the RTS time. Without updates the RTS can stay accurate for some time (it is said DS3231 would be off several minutes per year).
- The NeoPixel ring use three dots as the hour, one dot as the minute and a blinking dot as the second.
- If you push the button, the 7-segment LED display will display year, month and date for a few seconds.
- The light sensor adjusts the brightness of the 7-segment LED display and the NeoPixel ring based on room light.
- The onboard LED indicates if it is (still) connected to WiFi.
It’s pretty simple, but this is what really matters: I can see both LED display from another side of the room under any light conditions. It stays deadly accurate as long as WiFi is on (it can even function without WiFi). NeoPixels are notorious for their power consumption but I only need five LEDs at any time and only use 1/5 power at maximum. And the whole thing costed about $20 USD (not include the wood frame in the photo).
Yes — one problem with ESP8266 is that it would reportedly freeze every few days. I’ve noticed that my clock would reboot itself once in a while, but actually not very often.
I suppose you can try to use a ESP32 instead, but the smallness of D1 mini allowed me to assemble everything under the NeoPixel ring.
The Wiring
- D1 (GPIO 5): SCL of BH1750/DS3231
- D2 (GPIO 4): SDA of BH1750/DS3231
- D4 (GPIO 2): onboard LED
- D5 (GPIO 14): CLK of TM1637
- D6 (GPIO 12): DIO of TM1637
- D7 (GPIO 13): DIN of NeoPixels
- D8 (GPIO 15): button (with internal pulled-up)
TM1637, BH1750 and DS3231 are powered by 3.3V. NeoPixels are powered by 5V, which is directly connected to the USB port. I power the whole system with a 5V 1A charger.
The Code
The code is written with MicroPython. I update the firmware whenever there are new version possible. You can find instructions here:
Remember not to select “Erase flash before installing” if you just want to update firmware.
Drivers are also need to be uploaded:
Upload bh1750fvi.py, tm1637.py and DS3231.py to your board.
Finally, upload the following code (set your own WiFi name and password) to your board as main.py:
SSID = 'your_wifi_name'
PW = 'your_wifi_password'HOUR_OFFSET = 0 # time zone hour offset
MAX_LUX_LEVEL = 400 # room lux is usually 300-600
MAX_NEO_LEVEL = 50 # max 255
MIN_NEO_LEVEL = 5
MAX_DIGIT_LEVEL = 7 # max 7
HOUR_COLOR = (0, 91, 187)
MINUTE_COLOR = (255, 213, 0)
SECOND_COLOR = (255, 255, 255)import network, urequests, ntptime, utime, gc
from machine import Pin, I2C, Timer
from neopixel import NeoPixel# https://github.com/catdog2/mpy_bh1750fvi_esp8266
import bh1750fvi
# https://github.com/mcauser/micropython-tm1637
from tm1637 import TM1637
# https://github.com/micropython-Chinese-Community/mpy-lib/tree/master/misc/DS3231
from DS3231 import DS3231gc.enable()i2c = I2C(scl=Pin(5), sda=Pin(4))
ds = DS3231(i2c)btn = Pin(15, Pin.IN, Pin.PULL_UP)
led = Pin(2, Pin.OUT, value=1)
tm = TM1637(clk=Pin(14), dio=Pin(12))
np = NeoPixel(Pin(13, Pin.OUT), 24)tm.numbers(0, 0)
np.fill((1, 1, 1))
np.write()lux = 0
s_prev = 0
colon_flag = Falsedef translate(value, l1, l2, r1, r2):
leftSpan = l2 - l1
rightSpan = r2 - r1
valueScaled = float(value - l1) / float(leftSpan)
return r1 + (valueScaled * rightSpan)def color(color):
return tuple([round(MAX_NEO_LEVEL * (c / 255) * (max(lux, 10) / MAX_LUX_LEVEL))
for c in color])def rtcUpdate(timer):
while wifi.isconnected():
try:
ntptime.settime()
dt = utime.localtime(utime.time() + HOUR_OFFSET * 3600)
ds.DateTime([dt[0], dt[1], dt[2], dt[6], dt[3], dt[4], dt[5]])
gc.collect()
break
except:
utime.sleep(30)def lightLevelUpdate(timer):
global lux
lux = min(bh1750fvi.sample(i2c), MAX_LUX_LEVEL)
tm_level = round(translate(lux, 0, MAX_LUX_LEVEL, 1, MAX_DIGIT_LEVEL))
tm.brightness(tm_level)
led.value(not wifi.isconnected())def clockUpdate(timer):
global s_prev, colon_flag
now = ds.DateTime()
if btn.value():
np.fill((0, 0, 0))
np.write()
for d in now[:3]:
tm.number(d)
utime.sleep_ms(400)
tm.write([0, 0, 0, 0])
utime.sleep_ms(100)
return
h, mi, s = now[4], now[5], now[6]
np.fill((0, 0, 0))
np_h = (h % 12) * 2 + (1 if mi >= 30 else 0)
np.mi = round(mi / 2.5) % 24
np[np_h] = color(HOUR_COLOR)
np[(np_h - 1) if (np_h - 1) >= 0 else 23] = color(HOUR_COLOR)
np[(np_h + 1) if (np_h + 1) < 24 else 0] = color(HOUR_COLOR)
np[np.mi] = color(MINUTE_COLOR)
if s_prev != s:
colon_flag = not colon_flag
np[round(s / 2.5) % 24] = color(SECOND_COLOR)
tm.numbers(h, mi, colon=colon_flag)
np.write()
s_prev = swifi = network.WLAN(network.STA_IF)
wifi.active(True)
wifi.connect(SSID, PW)t = utime.ticks_ms()
while wifi.status() == network.STAT_CONNECTING:
if utime.ticks_diff(utime.ticks_ms(), t) >= 30000:
breakrtcUpdate(None)timer_rtc, timer_lightlvl, timer_display = Timer(-1), Timer(-1), Timer(-1)
timer_rtc.init(mode=Timer.PERIODIC, period=3600000, callback=rtcUpdate)
timer_lightlvl.init(mode=Timer.PERIODIC, period=2000, callback=lightLevelUpdate)
timer_display.init(mode=Timer.PERIODIC, period=100, callback=clockUpdate)
The core of the code is consisted of three timers (asynchronous functions running on schedule): one for updating the time, one for displaying the time, and one for measuring the brightness level.
Since the NeoPixel ring has 24 LEDs, every LED represents 2 to 3 minutes/seconds of a normal clock. The hour uses 3 LEDs and shifts forward 1 LED every half an hour. This is close enough to represent clock hands.
The Result
Later I bought a wooden photo frame and glued the mini breadboard on the glass plate, hanged it on the wall. It runs smoothly for almost one and half years now (and have went through several version of firmware) when I wrote this article.
If you are interested making your own MicroPython clock, I have two basic script (one use NTP and one use a RESTful API) that can be the basis of something more complicated.