last updated: 2025-09-17
I saw a flexible WS2812B Matrix LED Panel Module with 8x32 RGB Pixels at https://www.az-delivery.de and thought it would be a good catch for my music projects. There was a promotion for a set of three.
When I needed a bigger clock (vision is diminishing with age ;)) I remembered these displays.
So here is neo clock number three!
I wanted to familiarise with Micropython and I try to use more often the Raspberry Pi Pico for my projects. The latest political signals from China (pact with dictators Putin and Kim Jong-un) lead me to believe that it is time to boycott as far as possible Chinese microcontrollers.
I used a Pi Pico W and added only an LDR to adjust automatically the brightness of the display. The voltage of 3 V from the Pico outputs are enough to address the LEDs.
So the circuit is minimal:
I had still some transparent PLA filament (Premium from FelixPrinters) from my wastebin project, and so I used this to print a housing. You need a 300 mm² square bed to do so (45° rotation of the pieces). I used my Voron 2.4r2 for this and it worked quite well. The lid was some mm too big, and so I printed it in two halves and glued them together.
I used an LDR with 5 mm in diameter.
FreeCAD files are on github.
Here is the Micropython program (also on github).
""" Neo Clock 3
weigu.lu
AI helped with the functions for dst and timezone :)
"""
import machine, neopixel, network, socket, struct, ntptime, utime
from time import sleep, gmtime, localtime
# Constants for display and hardware
DISPL_PIXEL_NR = 8*32
COL_PIXEL_NR = 8
PIN_NEOPIXEL = machine.Pin(2)
PIN_ADC = machine.Pin(26)
# Network and time settings
NTP_HOST = "pool.ntp.org"
TIMEZONE_OFFSET = 1 # Luxembourg UTC + 1
SSID = "xxx"
WIFI_PW = "xxx?"
# Color definitions
OFF = (0,0,0)
RED = (1, 0, 0)
GREEN = (0, 1, 0)
BLUE = (0, 0, 1)
MYCOLOR = GREEN
# Digit patterns for display
DIGITS_5x8 = {0: [0, 1, 1, 1, 1, 1, 1, 0,
1, 0, 0, 0, 0, 0, 0, 1,
1, 0, 0, 0, 0, 0, 0, 1,
1, 0, 0, 0, 0, 0, 0, 1,
0, 1, 1, 1, 1, 1, 1, 0],
1: [0, 0, 0, 0, 0, 0, 0, 0,
1, 0, 0, 0, 0, 0, 0, 1,
1, 1, 1, 1, 1, 1, 1, 1,
0, 0, 0, 0, 0, 0, 0, 1,
0, 0, 0, 0, 0, 0, 0, 0],
2: [0, 1, 0, 0, 0, 0, 1, 1,
1, 0, 0, 0, 0, 1, 0, 1,
1, 0, 0, 0, 1, 0, 0, 1,
1, 0, 0, 1, 0, 0, 0, 1,
0, 1, 1, 0, 0, 0, 0, 1],
3: [0, 1, 0, 0, 0, 0, 1, 0,
1, 0, 0, 0, 0, 0, 0, 1,
1, 0, 0, 1, 0, 0, 0, 1,
1, 0, 0, 1, 0, 0, 0, 1,
0, 1, 1, 0, 1, 1, 1, 0],
4: [0, 0, 0, 1, 1, 0, 0, 0,
0, 0, 1, 0, 1, 0, 0, 0,
0, 1, 0, 0, 1, 0, 0, 0,
1, 1, 1, 1, 1, 1, 1, 1,
0, 0, 0, 0, 1, 0, 0, 0],
5: [1, 1, 1, 1, 0, 0, 1, 0,
1, 0, 0, 1, 0, 0, 0, 1,
1, 0, 0, 1, 0, 0, 0, 1,
1, 0, 0, 1, 0, 0, 0, 1,
1, 0, 0, 0, 1, 1, 1, 0],
6: [0, 1, 1, 1, 1, 1, 1, 0,
1, 0, 0, 1, 0, 0, 0, 1,
1, 0, 0, 1, 0, 0, 0, 1,
1, 0, 0, 1, 0, 0, 0, 1,
0, 1, 0, 0, 1, 1, 1, 0],
7: [1, 0, 0, 0, 0, 0, 1, 1,
1, 0, 0, 0, 0, 1, 0, 0,
1, 0, 0, 0, 1, 0, 0, 0,
1, 0, 0, 1, 0, 0, 0, 0,
1, 1, 1, 0, 0, 0, 0, 0],
8: [0, 1, 1, 0, 1, 1, 1, 0,
1, 0, 0, 1, 0, 0, 0, 1,
1, 0, 0, 1, 0, 0, 0, 1,
1, 0, 0, 1, 0, 0, 0, 1,
0, 1, 1, 0, 1, 1, 1, 0],
9: [0, 1, 1, 0, 0, 0, 1, 0,
1, 0, 0, 1, 0, 0, 0, 1,
1, 0, 0, 1, 0, 0, 0, 1,
1, 0, 0, 1, 0, 0, 0, 1,
0, 1, 1, 1, 1, 1, 1, 0],
10: [0, 0, 0, 0, 0, 0, 0, 0, # off
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0]}
DIGIT_1_3x8_0 = {1: [1, 0, 0, 0, 0, 0, 0, 1,
1, 1, 1, 1, 1, 1, 1, 1,
0, 0, 0, 0, 0, 0, 0, 1]}
DIGIT_1_3x8_1 = {1: [1, 0, 0, 0, 0, 0, 0, 1,
1, 1, 1, 1, 1, 1, 1, 1,
1, 0, 0, 0, 0, 0, 0, 0]}
DIGITS_3x5 = {0: [1, 1, 1, 1, 1, 0, 0, 0,
1, 0, 0, 0, 1, 0, 0, 0,
1, 1, 1, 1, 1, 0, 0, 0],
1: [0, 0, 0, 0, 1, 0, 0, 0,
1, 1, 1, 1, 1, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0],
2: [1, 0, 0, 1, 1, 0, 0, 0,
1, 1, 0, 0, 1, 0, 0, 0,
1, 0, 1, 1, 1, 0, 0, 0,],
3: [1, 0, 0, 0, 1, 0, 0, 0,
1, 0, 1, 0, 1, 0, 0, 0,
1, 1, 1, 1, 1, 0, 0, 0],
4: [0, 0, 1, 1, 1, 0, 0, 0,
0, 0, 1, 0, 0, 0, 0, 0,
1, 1, 1, 1, 1, 0, 0, 0],
5: [1, 0, 1, 1, 1, 0, 0, 0,
1, 0, 1, 0, 1, 0, 0, 0,
0, 1, 1, 0, 1, 0, 0, 0],
6: [1, 1, 1, 1, 1, 0, 0, 0,
1, 0, 1, 0, 1, 0, 0, 0,
1, 1, 1, 0, 1, 0, 0, 0],
7: [0, 0, 0, 0, 1, 0, 0, 0,
1, 1, 1, 0, 1, 0, 0, 0,
0, 0, 0, 1, 1, 0, 0, 0],
8: [1, 1, 1, 1, 1, 0, 0, 0,
1, 0, 1, 0, 1, 0, 0, 0,
1, 1, 1, 1, 1, 0, 0, 0],
9: [1, 0, 1, 1, 1, 0, 0, 0,
1, 0, 1, 0, 1, 0, 0, 0,
1, 1, 1, 1, 1, 0, 0, 0],
10: [0, 0, 0, 0, 0, 0, 0, 0, # off
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0]}
def is_dst_eu(year, month, day):
"""Returns True if DST is in effect in Europe for the given date."""
if month < 3 or month > 10:
return False
if month > 3 and month < 10:
return True
if month == 3: # March: last Sunday
last_day = 31 # Find the last Sunday in March
dow = utime.localtime(utime.mktime((year, 3, last_day, 0, 0, 0, 0, 0, 0)))[6]
last_sunday = last_day - dow
return day >= last_sunday
if month == 10: # October: last Sunday
last_day = 31
dow = utime.localtime(utime.mktime((year, 10, last_day, 0, 0, 0, 0, 0, 0)))[6]
last_sunday = last_day - dow
return day <= last_sunday
return False
def set_rtc_with_timezone_and_dst(utc_time, std_offset):
""" Sets RTC with DST-aware local time. """
year, month, day, *_ = utc_time
if is_dst_eu(year, month, day):
offset = std_offset + 1 # DST is UTC+1 more than standard
else:
offset = std_offset
utc_seconds = utime.mktime(utc_time)
local_seconds = utc_seconds + (offset * 3600)
local_time = utime.localtime(local_seconds)
rtc = machine.RTC()
rtc.datetime((local_time[0], local_time[1], local_time[2],
local_time[6] + 1, local_time[3],
local_time[4], local_time[5], 0))
print("RTC set to local time (DST-aware):", rtc.datetime())
def connect():
""" Connect to WiFi """
max_wait = 10 # in seconds
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
wlan.connect(SSID, WIFI_PW)
while max_wait > 0:
if wlan.status() < 0 or wlan.status() >= 3:
break
max_wait -= 1
print('waiting for connection...')
sleep(1)
if wlan.status() != 3:
raise RuntimeError('network connection failed')
else:
print('connected')
status = wlan.ifconfig()
print( 'ip = ' + status[0] )
def display_clear():
""" Clear all pixels. """
for i in range(0, DISPL_PIXEL_NR, 1):
np[i] = OFF
np.write()
def write_digit_5x8(nr, offset = 0, color = RED):
""" Draw 5x8 digit at offset. """
p_offset = offset*COL_PIXEL_NR
if offset%2: # starting with even column
for i in range(0,3):
for j in range(i*16 + 7 + p_offset, i*16 - 1 + p_offset, -1):
if DIGITS_5x8[nr][(7+i*32)-(j-p_offset)]:
np[j] = color
for i in range(0,2):
for j in range(i*16 + 8 + p_offset, i*16 + 16 +p_offset, 1):
if DIGITS_5x8[nr][j-p_offset]:
np[j] = color
else: # starting with odd column
for i in range(0,3):
for j in range(i*16 + p_offset, i*16 + 8 + p_offset, 1):
if DIGITS_5x8[nr][j-p_offset]:
np[j] = color
for i in range(0,2):
for j in range(i*16 + 15 + p_offset, i*16 + 7 + p_offset, -1):
if DIGITS_5x8[nr][(23+i*32)-(j-p_offset)]:
np[j] = color
np.write()
def write_colon_5x8(offset = 0, color = RED):
""" Draw colon symbol at offset. """
p_offset = offset*COL_PIXEL_NR
np[p_offset+2] = color
np[p_offset+5] = color
np.write()
def write_1_first_pos(offset = 0, color = RED):
""" Draw digit '1' in first or second column. """
if offset == 0:
for i in range(0,24):
if DIGIT_1_3x8_0[1][i]:
np[i] = color
else:
for i in range(8,32):
if DIGIT_1_3x8_1[1][i-8]:
np[i] = color
np.write()
def write_digit_3x5(nr, offset = 0, color = RED):
""" Draw 3x5 digit at offset. """
p_offset = offset*COL_PIXEL_NR
if offset%2: # starting with even column
for i in range(0,2):
for j in range(i*16 + 4 + p_offset, i*16 - 1 + p_offset, -1):
if DIGITS_3x5[nr][j-p_offset]:
np[j] = color
for j in range(11 + p_offset, 16 +p_offset, 1):
if DIGITS_3x5[nr][p_offset + 23 -j]:
np[j] = color
else: # starting with odd column
for i in range(0,2):
for j in range(i*16 + 3 + p_offset, i*16 + 8 + p_offset, 1):
if DIGITS_3x5[nr][i*32+7+p_offset-j]:
np[j] = color
for j in range(12 + p_offset, 7 + p_offset, -1):
if DIGITS_3x5[nr][j-p_offset]:
np[j] = color
np.write()
def get_brightness():
""" Read LDR and calculate brightness. """
lum = ldr.read_u16()
brightness = int(256-lum*256/65536)
#print(brightness)
if brightness > 200:
brightness = 20
elif brightness < 100:
brightness = 3
else:
brightness = 7
return brightness
def draw_clock():
""" Update only changed digits """
if hour//10 != 1:
write_1_first_pos(1, OFF)
else:
write_1_first_pos(1, mycolor)
if hour%10 != time_prev[1]:
write_digit_5x8(time_prev[1], 5, OFF)
write_digit_5x8(hour%10, 5, mycolor)
write_colon_5x8(11, mycolor)
if min//10 != time_prev[2]:
write_digit_5x8(time_prev[2], 13, OFF)
write_digit_5x8(min//10, 13, mycolor)
if min%10 != time_prev[3]:
write_digit_5x8(time_prev[3], 19, OFF)
write_digit_5x8(min%10, 19, mycolor)
if sec//10 != time_prev[4]:
write_digit_3x5(time_prev[4], 25, OFF)
write_digit_3x5(sec//10, 25, mycolor)
if sec%10 != time_prev[5]:
write_digit_3x5(time_prev[5], 29, OFF)
write_digit_3x5(sec%10, 29, mycolor)
def draw_whole_clock():
""" Redraw all digits """
if hour//10 != 1:
write_1_first_pos(1, OFF)
else:
write_1_first_pos(1, mycolor)
write_digit_5x8(time_prev[1], 5, OFF)
write_digit_5x8(hour%10, 5, mycolor)
write_colon_5x8(11, mycolor)
write_digit_5x8(time_prev[2], 13, OFF)
write_digit_5x8(min//10, 13, mycolor)
write_digit_5x8(time_prev[3], 19, OFF)
write_digit_5x8(min%10, 19, mycolor)
write_digit_3x5(time_prev[4], 25, OFF)
write_digit_3x5(sec//10, 25, mycolor)
write_digit_3x5(time_prev[5], 29, OFF)
write_digit_3x5(sec%10, 29, mycolor)
# Setup
np = neopixel.NeoPixel(PIN_NEOPIXEL, DISPL_PIXEL_NR)
ldr = machine.ADC(PIN_ADC)
connect()
ntptime.settime()
rtc = machine.RTC()
utc_time = utime.gmtime()
set_rtc_with_timezone_and_dst(utc_time, TIMEZONE_OFFSET)
display_clear()
time_prev = [10,10,10,10,10,10]
brightness_prev = 10
# Main loop: update display every second
while True:
now_time = rtc.datetime()
month = now_time[1]
day = now_time[2]
hour = now_time[4]
min = now_time[5]
sec = now_time[6]
# check ntp once a day
if now_time[4] == 3 and now_time[5] == 0 and now_time[6] == 0:
ntptime.settime() # Sync RTC with NTP (UTC)
utc_time = utime.gmtime()
hour = hour%12
brightness = get_brightness()
mycolor = tuple([brightness*x for x in MYCOLOR])
if brightness_prev != brightness:
draw_whole_clock()
else:
draw_clock()
brightness_prev = brightness
time_prev = [hour//10, hour%10, min//10, min%10, sec//10, sec%10]
sleep(1)