Microcontroller projects

Neo Clock 3: WS2812B 8x32 matrix with Pi Pico W using NTP

last updated: 2025-09-17

Quick links

Introduction

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!

neo clock 3

BOM

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.

Circuit

So the circuit is minimal:

neopixel clock circuit

3D printed housing

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.

neo clock 3 inside neo clock 3 LDR

neo clock 2 housing 1

FreeCAD files are on github.

Software

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)

Downloads