I was looking for a clock that would tell the time in words rather than just hands (analog) or numbers (digital). I found a few for sale on places like Amazon and eBay, but none of those particularly stood out, so I decided to create a customizable one myself.
I designed and built two variants. Both use the same RGB matrix panel. Both use CircuitPython on the ESP32-S3, but one uses the Matrix Portal S3, and the other uses a standalone Feather ESP32-S3 with an Adafruit RGB Matrix Featherwing Kit. The Matrix Portal version requires no soldering; the Feather version requires some simple soldering. I will show both here.
The text is left justified, vertically centered on the display and each line is in a random color which changes every time the minute changes. Since you control the CircuitPython code, you can change this formatting any way you choose, e.g. same color lines, individually horizontally centered lines, etc.
- Any of the 64x64 RGB LED matrix panels will work. The only differences in the result will be the panel dimensions and appearance. I chose the 2mm pitch version because I liked the smaller panel size and less space between LEDs. The 2.5 mm pitch also looks good. Just choose one that meets your requirements.
- Optional - magnetic feet for matrix panel
- Adafruit Matrix Portal S3
- Any 5V, 2A or higher USB power supply and cable with a USB type C connector. For example:
- Assembly
- Prep the Matrix Portal. See: https://learn.adafruit.com/adafruit-matrixportal-s3/prep-the-matrixportal
- Using a USB cable, install CircuitPython on the Matrix Portal. See: https://learn.adafruit.com/adafruit-matrixportal-s3/install-circuitpython
Be sure to update the settings.toml file with your WiFi SSID, WiFi password, and timezone information.
CIRCUITPY_WIFI_SSID = "xxxxxx" CIRCUITPY_WIFI_PASSWORD = "xxxxxx" CIRCUITPY_WEB_API_PASSWORD = "yyyyyy" CIRCUITPY_WEB_API_PORT=80 # UTC_OFFSET, if present, will override TZ, no worldtimeapi.org API query will be done, and no DST adjustment #UTC_OFFSET=-25200 TZ="America/Phoenix"
- Update your code.py to the following:
# SPDX-FileCopyrightText: 2023 Frederick M Meyer
#
# SPDX-License-Identifier: MIT
import ipaddress
import ssl
import wifi
import socketpool
import adafruit_requests
import random
import os
import time
import rtc
import adafruit_ntp
import adafruit_datetime
import displayio
import framebufferio
import rgbmatrix
import board
import digitalio
import terminalio
import adafruit_display_text as adt
import adafruit_display_text.label as adtl
UTC_OFFSET = os.getenv('UTC_OFFSET')
TZ = os.getenv('TZ')
QUARTER = "Quarter"
HALF = "Half"
PAST = "Past"
UNTIL = "Until"
OCLOCK = "O'clock"
NOON = "Noon"
MIDNIGHT = "Midnight"
MINUTE = "Minute"
SINGLE_DIGITS = ["One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine"]
TEN_PLUS = ["Ten", "Eleven", "Twelve", "Thirteen", "Fourteen", "Fifteen", "Sixteen", "Seventeen", "Eighteen", "Nineteen"]
TENS = ["Twenty", "Thirty", "Fourty", "Fifty"]
#time.sleep(15)
led = digitalio.DigitalInOut(board.LED)
led.direction = digitalio.Direction.OUTPUT
led.value = False
displayio.release_displays()
# Wifi details are in settings.toml file
print("My MAC addr:", [hex(i) for i in wifi.radio.mac_address])
print("Connecting to %s"%os.getenv("CIRCUITPY_WIFI_SSID"))
wifi.radio.connect(os.getenv("CIRCUITPY_WIFI_SSID"), os.getenv("CIRCUITPY_WIFI_PASSWORD"))
print("Connected to %s!"%os.getenv("CIRCUITPY_WIFI_SSID"))
print("My IP address is", wifi.radio.ipv4_address)
pool = socketpool.SocketPool(wifi.radio)
if UTC_OFFSET is None:
requests = adafruit_requests.Session(pool, ssl.create_default_context())
response = requests.get("http://worldtimeapi.org/api/timezone/" + TZ)
response_as_json = response.json()
UTC_OFFSET = response_as_json["raw_offset"] + response_as_json["dst_offset"]
ntp = adafruit_ntp.NTP(pool, server="us.pool.ntp.org", tz_offset=UTC_OFFSET // 3600)
rtc.RTC().datetime = ntp.datetime
last_minute_displayed = -1
def format_hour(hour_num):
hour = hour_num % 24
if hour == 0:
o_hour = MIDNIGHT
elif hour == 12:
o_hour = NOON
else:
if hour > 12:
hour -= 12
elif hour == 0:
hour = 12
if hour < 10:
o_hour = SINGLE_DIGITS[hour - 1]
else:
o_hour = TEN_PLUS[hour - 10]
return hour, o_hour
COLOR_VALUES = [0, 128, 255] # Brighter - Possible color values used for random 0..2 selection
#COLOR_VALUES = [0, 80, 160] # Dimmer - Possible color values used for random 0..2 selection
#COLOR_VALUES = [0, 64, 128] # Dimmer - Possible color values used for random 0..2 selection
def pick_random_color():
# Pick a random color for each line and add it to the display
r = COLOR_VALUES[random.randint(0, 2)]
g = COLOR_VALUES[random.randint(0, 2)]
b = COLOR_VALUES[random.randint(0, 2)]
if not (r | g | b): r = g = b = COLOR_VALUES[2] # Set to white if black result
return (r<<16|g<<8|b)
matrix = rgbmatrix.RGBMatrix(
width=64, height=64, bit_depth=2,
rgb_pins=[
board.MTX_R1,
board.MTX_G1,
board.MTX_B1,
board.MTX_R2,
board.MTX_G2,
board.MTX_B2
],
addr_pins=[
board.MTX_ADDRA,
board.MTX_ADDRB,
board.MTX_ADDRC,
board.MTX_ADDRD,
board.MTX_ADDRE
],
clock_pin=board.MTX_CLK,
latch_pin=board.MTX_LAT,
output_enable_pin=board.MTX_OE
)
display = framebufferio.FramebufferDisplay(matrix, auto_refresh=True)
display.rotation = 0
while True:
# time.struct_time(tm_year=2023, tm_mon=2, tm_mday=23, tm_hour=14, tm_min=6, tm_sec=29, tm_wday=3, tm_yday=54, tm_isdst=0)
lt = time.localtime()
hour = lt.tm_hour
min = lt.tm_min
#hour = 12 # For testing
#min = 00 # For testing
if last_minute_displayed != min:
last_minute_displayed = min
if min <= 30:
o_min_half = "Past"
hour, o_hour = format_hour(hour)
else:
o_min_half = "Until"
min = 60 - min
hour, o_hour = format_hour(hour + 1)
if min == 15:
o_min = "Quarter"
elif min == 30:
o_min = "Half"
elif min == 0:
o_min = ""
o_min_half = ""
elif min < 10:
o_min = SINGLE_DIGITS[min - 1]
elif min < 20:
o_min = TEN_PLUS[min - 10]
else:
o_min = TENS[(min // 10) - 2]
if min % 10:
o_min += " " + SINGLE_DIGITS[(min % 10) - 1]
if o_min not in [QUARTER, HALF, ""]:
o_min += " " + MINUTE
if min != 1: o_min += "s"
if min > 0 or o_hour in [MIDNIGHT, NOON]:
txt = (o_min + " " + o_min_half + " " + o_hour).strip()
else:
txt = (o_min + " " + o_min_half + " " + o_hour + " " + OCLOCK).strip()
text_list = adt.wrap_text_to_pixels(txt, 60, font=terminalio.FONT)
total_height = 0
max_width = 0
line_list = []
for w in text_list:
line = adtl.Label(
terminalio.FONT,
color=pick_random_color(),
text=w,
scale=1)
line_list.append(line)
zx, zy, zwidth, zheight = line.bounding_box
total_height += zheight
max_width = max(max_width, zwidth)
xwork = ((60 - max_width) // 2) + 2
ywork = ((60 - total_height) // 2) + 2 + 6
current_y = ywork
g = displayio.Group()
for l in line_list:
l.x = xwork
l.y = current_y
zx, zy, zwidth, zheight = l.bounding_box
current_y += zheight
g.append(l)
display.root_group=g
time.sleep(1)
- Install the following modules into the lib folder:
- Connect the Matrix Portal and power connector to the RGB LED panel and then connect the power supply to the Matrix Portal. If there is a pin on the frame of the panel under the Matrix Portal that prevents it from connecting fully, use angle cutters to remove the pin.
- Any of the Adafruit ESP32-S3 Feathers should work. I decided that the best fit would be one of these two. I chose the first for no specific reason.
-or-
- Because I chose an ESP32-S3 in the Feather form factor as a processor, I needed a compatible Featherwing format for the interface to the RGB matrix panel. Be careful, there are multiple versions of these kits based on the processor type it supports.
- Any 5V, 2A or greater power supply with a 'standard' 5.5mm OD, 2.1mm ID positive tip connector to plug into the socket on the Featherwing adapter. For example, I used a 4-amp power supply in case I ever decide to put up a display that lights more pixels and has a higher power draw, but 2A should be enough to power this build.
- The ESP32-S3 stacks on top of the Featherwing, so we need some female headers.
- A 6-inch piece of 18-22AWG solid-core hook-up wire
- Soldering Iron and rosin-core electronics solder
- Angle cutters
- Assembly
- Prep RGB Matrix Featherwing. See: https://learn.adafruit.com/rgb-matrix-featherwing
Be sure you are soldering all the components on the proper side of the PCB and facing in the correct direction. Before soldering any other parts, you will need to install a jumper wire, as seen in the picture. The Featherwing comes from the factory with 4 address lines ("A"-"D"). This build uses a 64x64 pixel panel which requires 5 address lines, so you have to add an additional address line. This wire will provide the path for the "E" address line on Pin 8 of the HUB-75 connector. (If your panel uses Pin 16, move the wire to connect on that pin instead. Adafruit panels all use Pin 8.)
You may use either the male OR female HUB-75 connector, but only one can be soldered on the PCB, so choose the one that meets your needs. They mount on different sides of the board, so again, be sure you're soldering on the correct side and facing in the correct direction (male connector has a notch in it).
- Solder the headers onto the Feather ESP32-S3 facing down and soldered on the top.
- Using a USB cable, install CircuitPython on the ESP32-S3. See: https://learn.adafruit.com/adafruit-esp32-s3-feather/circuitpython
Be sure to update the settings.toml file with your WiFi SSID, WiFi password, and timezone information.
CIRCUITPY_WIFI_SSID = "xxxxxx" CIRCUITPY_WIFI_PASSWORD = "xxxxxx" CIRCUITPY_WEB_API_PASSWORD = "yyyyyy" CIRCUITPY_WEB_API_PORT=80 # UTC_OFFSET, if present, will override TZ, no worldtimeapi.org API query will be done, and no DST adjustment #UTC_OFFSET=-25200 TZ="America/Phoenix"
- Update your code.py to the following:
# SPDX-FileCopyrightText: 2023 Frederick M Meyer
#
# SPDX-License-Identifier: MIT
import ipaddress
import ssl
import wifi
import socketpool
import adafruit_requests
import random
import os
import time
import rtc
import adafruit_ntp
import adafruit_datetime
import displayio
import framebufferio
import rgbmatrix
import board
import digitalio
import terminalio
import adafruit_display_text as adt
import adafruit_display_text.label as adtl
UTC_OFFSET = os.getenv('UTC_OFFSET')
TZ = os.getenv('TZ')
QUARTER = "Quarter"
HALF = "Half"
PAST = "Past"
UNTIL = "Until"
OCLOCK = "O'clock"
NOON = "Noon"
MIDNIGHT = "Midnight"
MINUTE = "Minute"
SINGLE_DIGITS = ["One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine"]
TEN_PLUS = ["Ten", "Eleven", "Twelve", "Thirteen", "Fourteen", "Fifteen", "Sixteen", "Seventeen", "Eighteen", "Nineteen"]
TENS = ["Twenty", "Thirty", "Fourty", "Fifty"]
#time.sleep(15)
neopix_pwr = digitalio.DigitalInOut(board.NEOPIXEL_POWER)
neopix_pwr.direction = digitalio.Direction.OUTPUT
neopix_pwr.value = False
displayio.release_displays()
# Wifi details are in settings.toml file
print("My MAC addr:", [hex(i) for i in wifi.radio.mac_address])
print("Connecting to %s"%os.getenv("CIRCUITPY_WIFI_SSID"))
wifi.radio.connect(os.getenv("CIRCUITPY_WIFI_SSID"), os.getenv("CIRCUITPY_WIFI_PASSWORD"))
print("Connected to %s!"%os.getenv("CIRCUITPY_WIFI_SSID"))
print("My IP address is", wifi.radio.ipv4_address)
pool = socketpool.SocketPool(wifi.radio)
if UTC_OFFSET is None:
requests = adafruit_requests.Session(pool, ssl.create_default_context())
response = requests.get("http://worldtimeapi.org/api/timezone/" + TZ)
response_as_json = response.json()
UTC_OFFSET = response_as_json["raw_offset"] + response_as_json["dst_offset"]
ntp = adafruit_ntp.NTP(pool, server="us.pool.ntp.org", tz_offset=UTC_OFFSET // 3600)
rtc.RTC().datetime = ntp.datetime
last_minute_displayed = -1
def format_hour(hour_num):
hour = hour_num % 24
if hour == 0:
o_hour = MIDNIGHT
elif hour == 12:
o_hour = NOON
else:
if hour > 12:
hour -= 12
elif hour == 0:
hour = 12
if hour < 10:
o_hour = SINGLE_DIGITS[hour - 1]
else:
o_hour = TEN_PLUS[hour - 10]
return hour, o_hour
COLOR_VALUES = [0, 128, 255] # Brighter - Possible color values used for random 0..2 selection
#COLOR_VALUES = [0, 80, 160] # Dimmer - Possible color values used for random 0..2 selection
#COLOR_VALUES = [0, 64, 128] # Dimmer - Possible color values used for random 0..2 selection
def pick_random_color():
# Pick a random color for each line and add it to the display
r = COLOR_VALUES[random.randint(0, 2)]
g = COLOR_VALUES[random.randint(0, 2)]
b = COLOR_VALUES[random.randint(0, 2)]
if not (r | g | b): r = g = b = COLOR_VALUES[2] # Set to white if black result
return (r<<16|g<<8|b)
matrix = rgbmatrix.RGBMatrix(
width=64, height=64, bit_depth=2,
rgb_pins=[board.D6, board.D5, board.D9, board.D11, board.D10, board.D12],
addr_pins=[board.A5, board.A4, board.A3, board.A2, board.A1],
clock_pin=board.D13, latch_pin=board.RX, output_enable_pin=board.TX)
display = framebufferio.FramebufferDisplay(matrix, auto_refresh=True)
display.rotation = 0
while True:
lt = time.localtime()
hour = lt.tm_hour
min = lt.tm_min
#hour = 12 # For testing
#min = 00 # For testing
if last_minute_displayed != min:
last_minute_displayed = min
if min <= 30:
o_min_half = "Past"
hour, o_hour = format_hour(hour)
else:
o_min_half = "Until"
min = 60 - min
hour, o_hour = format_hour(hour + 1)
if min == 15:
o_min = "Quarter"
elif min == 30:
o_min = "Half"
elif min == 0:
o_min = ""
o_min_half = ""
elif min < 10:
o_min = SINGLE_DIGITS[min - 1]
elif min < 20:
o_min = TEN_PLUS[min - 10]
else:
o_min = TENS[(min // 10) - 2]
if min % 10:
o_min += " " + SINGLE_DIGITS[(min % 10) - 1]
if o_min not in [QUARTER, HALF, ""]:
o_min += " " + MINUTE
if min != 1: o_min += "s"
if min > 0 or o_hour in [MIDNIGHT, NOON]:
txt = (o_min + " " + o_min_half + " " + o_hour).strip()
else:
txt = (o_min + " " + o_min_half + " " + o_hour + " " + OCLOCK).strip()
text_list = adt.wrap_text_to_pixels(txt, 60, font=terminalio.FONT)
total_height = 0
max_width = 0
line_list = []
for w in text_list:
line = adtl.Label(
terminalio.FONT,
color=pick_random_color(),
text=w,
scale=1)
line_list.append(line)
zx, zy, zwidth, zheight = line.bounding_box
total_height += zheight
max_width = max(max_width, zwidth)
xwork = ((60 - max_width) // 2) + 2
ywork = ((60 - total_height) // 2) + 2 + 6
current_y = ywork
g = displayio.Group()
for l in line_list:
l.x = xwork
l.y = current_y
zx, zy, zwidth, zheight = l.bounding_box
current_y += zheight
g.append(l)
display.root_group=g
time.sleep(1)
- Install the following modules into the lib folder:
- Connect the Featherwing and power connector to the RGB LED panel and then connect the power supply to the Featherwing. A USB cable is not needed. If you plug the Featherwing directly into the panel, the HUB-75 connector has no tab on it to show the proper direction, be sure it's facing in the correct direction. See picture.
This page (RGB Matrix Word Clocks) was last updated on February 02, 2024.
Text editor powered by tinymce.