last updated: 2025-09-26
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. The distance between the LEDs is 1cm, so the clock dimensions are 32 cmx8 cm.
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.
The housing was designed with FreeCAD. My simple box macro helped to speed uop the task: https://www.weigu.lu/other_projects/freecad/freecad_3dbox_macro/index.html.
I used an LDR with 5 mm in diameter.
 


FreeCAD files are on github.
Here is the Micropython program (also on github). Download Micropython from https://micropython.org/download/RPI_PICO/ (the uf2 file). Then connect the Pico to your PC while pressing the BOOTSEL button. The Pico shows as mass storage device and so you can copy the file to the Pico. Now your Pico is running Micropython.
The simplest way to copy the program to your Pico is to use the Python IDE Thonny. Install Thonny from https://thonny.org/.
If you the program main.py it will start at boot.
If you don't need the LDR, replace the line brightness = get_brightness() with e.g. brightness = 100.
""" 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)
WHITE = (1, 1, 1)
MYCOLOR = GREEN
BRIGHTNESS = [1,4,8]
# 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_adc = ldr.read_u16()
    lum = int(256-lum_adc*256/65536)
    if lum > 200:
        brightness = BRIGHTNESS[2]
    elif lum < 100:
        brightness = BRIGHTNESS[0]
    else:
        brightness = BRIGHTNESS[1]
    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] == 5 and now_time[6] == 0:
        try:
            ntptime.settime()   # Sync RTC with NTP (UTC)
            utc_time = utime.gmtime()
            set_rtc_with_timezone_and_dst(utc_time, TIMEZONE_OFFSET)
            sleep(1)
        except:
            pass
    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(0.1)