I wanted a clock that would display the time in large yellow characters and display the week and day. For fun added a 60 RGB neo-pixel ring to tick off the seconds.
The last part was to display the phase of the moon in a fun way and found these to be entertaining.
I cut a sheet of plex-glass to 8x11 sheet. I used miniature self-tapping screws to mount the RGB neo-pixel ring. I used this same kind of screws to mount the 4-digit display and two of the Quad Alphanumerica Displays.
I drill large enough holes to plug the displays up to the Adafruit Qualia ESP32-S3 and daisy chained the rest.
I did solder 3 wires to the RGB neo-pixel ring a crimped a 3-pin connector on the other and plugged it in socket A0.
I used Photopea to create a 3x3 of the moon phases I found on the internet. For the new moon phase I copied the full moon phase and changed its color from yellow to blue. and its a seperate .bmp file.
I am not a very good coder anymore and new to python. I am sure there are better coders out there than me for sure. I hacked most of the code from other projects.
Parts Required for this project:
- Adafruit Qualia ESP32-S3 for TTL RGB-666 Displays (PI - 5800
- Round RGB TTL TFT Display -2.1" (PI - 5806)
- NeoPixel 1/4 60 Ring 5050 RGB LED w/Integrated Drivers (PI -1768)
- Quad Alphanumerica Display - Blue 0.54" Digits w/ I2C Backpack - Stemma QT / Qwiic (PI 1912)
- Assembled Adafruit 0.56: 4-Digit 7 Segment Display -w/ I2c Backpack QT - Yellow (PI 5602)
- Stemma QT JST SH 4-Pin Cable (50mm & 100mm)
# SPDX-FileCopyrightText: 2020 Brent Rubell for Adafruit Industries
#
# 08-17-2023 DWE
#
# SPDX-License-Identifier: MIT
import time
import gc
import ipaddress
import displayio
import ssl
import wifi
import busio
import socketpool
import adafruit_requests
import board
import adafruit_imageload
import dotclockframebuffer
import neopixel
import pwmio
from rtc import RTC
from adafruit_ht16k33 import segments
from framebufferio import FramebufferDisplay
from displayio import release_displays
release_displays()
i2c = busio.I2C(board.SCL, board.SDA)
num_pixels = 60
pixels = neopixel.NeoPixel(board.A0, num_pixels, auto_write=False)
pixels.brightness = 0.01
TWELVE_HOUR = True # If set, use 12-hour time vs 24-hour (e.g. 3:00 vs 15:00)
GREEN = (0, 255, 0)
RED = (255, 0, 0)
BLACK = (0, 0, 0)
BLUE = (0, 0, 255)
refresh_time_update = 60
weekname = {
0: "Sun ",
1: "Mon ",
2: "Tue ",
3: "Wed ",
4: "Thurs ",
5: "Fri ",
6: "Sat ",
}
init_sequence_tl021wvc02 = bytes((
0xff, 0x05, 0x77, 0x01, 0x00, 0x00, 0x10,
0xc0, 0x02, 0x3b, 0x00,
0xc1, 0x02, 0x0b, 0x02,
0xc2, 0x02, 0x00, 0x02,
0xcc, 0x01, 0x10,
0xcd, 0x01, 0x08,
0xb0, 0x10, 0x02, 0x13, 0x1b, 0x0d, 0x10, 0x05, 0x08, 0x07, 0x07, 0x24, 0x04, 0x11, 0x0e, 0x2c, 0x33, 0x1d,
0xb1, 0x10, 0x05, 0x13, 0x1b, 0x0d, 0x11, 0x05, 0x08, 0x07, 0x07, 0x24, 0x04, 0x11, 0x0e, 0x2c, 0x33, 0x1d,
0xff, 0x05, 0x77, 0x01, 0x00, 0x00, 0x11,
0xb0, 0x01, 0x5d,
0xb1, 0x01, 0x43,
0xb2, 0x01, 0x81,
0xb3, 0x01, 0x80,
0xb5, 0x01, 0x43,
0xb7, 0x01, 0x85,
0xb8, 0x01, 0x20,
0xc1, 0x01, 0x78,
0xc2, 0x01, 0x78,
0xd0, 0x01, 0x88,
0xe0, 0x03, 0x00, 0x00, 0x02,
0xe1, 0x0b, 0x03, 0xa0, 0x00, 0x00, 0x04, 0xa0, 0x00, 0x00, 0x00, 0x20, 0x20,
0xe2, 0x0d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xe3, 0x04, 0x00, 0x00, 0x11, 0x00,
0xe4, 0x02, 0x22, 0x00,
0xe5, 0x10, 0x05, 0xec, 0xa0, 0xa0, 0x07, 0xee, 0xa0, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xe6, 0x04, 0x00, 0x00, 0x11, 0x00,
0xe7, 0x02, 0x22, 0x00,
0xe8, 0x10, 0x06, 0xed, 0xa0, 0xa0, 0x08, 0xef, 0xa0, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xeb, 0x07, 0x00, 0x00, 0x40, 0x40, 0x00, 0x00, 0x00,
0xed, 0x10, 0xff, 0xff, 0xff, 0xba, 0x0a, 0xbf, 0x45, 0xff, 0xff, 0x54, 0xfb, 0xa0, 0xab, 0xff, 0xff, 0xff,
0xef, 0x06, 0x10, 0x0d, 0x04, 0x08, 0x3f, 0x1f,
0xff, 0x05, 0x77, 0x01, 0x00, 0x00, 0x13,
0xef, 0x01, 0x08,
0xff, 0x05, 0x77, 0x01, 0x00, 0x00, 0x00,
0x36, 0x01, 0x00,
0x3a, 0x01, 0x60,
0x11, 0x80, 0x64,
0x29, 0x80, 0x32,
))
tft_io_expander = dict(board.TFT_IO_EXPANDER)
#tft_io_expander['i2c_address'] = 0x38 # uncomment for rev B
dotclockframebuffer.ioexpander_send_init_sequence(i2c, init_sequence_tl021wvc02, **tft_io_expander)
i2c.deinit()
tft_pins = dict(board.TFT_PINS)
tft_timings = {
"frequency": 16_000_000,
"width": 480,
"height": 480,
"hsync_pulse_width": 20,
"hsync_front_porch": 40,
"hsync_back_porch": 40,
"vsync_pulse_width": 10,
"vsync_front_porch": 40,
"vsync_back_porch": 40,
"hsync_idle_low": False,
"vsync_idle_low": False,
"de_idle_high": False,
"pclk_active_high": True,
"pclk_idle_high": False,
}
# Loop through each sprite in the sprite sheet
source_index = 0
displayBL = pwmio.PWMOut(board.A1)
i2c = busio.I2C(board.SCL, board.SDA)
# Create the LED segment class.
# This creates a 7 segment 4 character display:
display = segments.Seg7x4(i2c, address=0x74)
display14 = segments.Seg14x4(i2c, address=(0x71, 0x72))
# alphanumeric segment displpay setup
# using two displays together
# Reset Display
display.fill(0)
display14.fill(0)
pixels.full = BLACK
pixels.show()
# Get wifi details and more from a secrets.py file
try:
from secrets import secrets
except ImportError:
print("WiFi secrets are kept in secrets.py, please add them there!")
raise
print("Connecting to %s" % secrets["ssid"])
wifi.radio.connect(secrets["ssid"], secrets["password"])
# print("Connected to %s!" % secrets["ssid"])
# print("My IP address is", wifi.radio.ipv4_address)
pool = socketpool.SocketPool(wifi.radio)
requests = adafruit_requests.Session(pool, ssl.create_default_context())
try:
TIMEZONE = secrets["timezone"] # e.g. 'America/New_York'
except:
TIMEZONE = None # IP geolocation
print("TIME ZONE IS")
print(TIMEZONE)
try:
LATITUDE = secrets['latitude']
LONGITUDE = secrets['longitude']
print('Using stored geolocation: ', LATITUDE, LONGITUDE)
except:
LATITUDE = None
LONGITUDE = None
# Set initial clock time, also fetch initial UTC offset while
# here (NOT stored in secrets.py as it may change with DST).
# pylint: disable=bare-except
def MoonData(year, month, day, lat, log):
""" Class holding lunar data for a given 24-hour period. App uses two
of these -- one for the current day, and one for the following day,
then some interpolations and such can be made. Elements include:
age : Moon phase 'age' at start of period, expressed
from 0.0 (new moon) through 0.5 (full moon) to
1.0 (next new moon).
start_utc_seconds : Epoch time at start of period, UTC
end_utc_seconds : Epoch time at end of period, "
rise_utc_seconds : Epoch time of moon rise within this 24-hour period
set_utc_seconds : Epoch time of moon set within this 24-hour period
"""
# URL does not contain local or UTC time, only date. strftime() is
# not available in CircuitPython, manual conversion to time string
# is needed. Response is moon data for a 24-hour period, based on
# longitude and requested date. Some values within are UTC time,
# others are local. Anything we parse out of this will be converted
# to UTC epoch seconds, period.
utc_offset_string = "-07:00"
moon_url = ('https://api.met.no/weatherapi/sunrise/3.0/moon?lat=' +
str(lat) + '&lon=' + str(log) +
'&date=' + str(year) + '-' +
'{0:0>2}'.format(month) + '-' +
'{0:0>2}'.format(day) +
'&offset=' + utc_offset_string)
# pylint: disable=bare-except
for _ in range(2): # Retries
print('Fetching moon data via', moon_url)
try:
moon_data = requests.get(moon_url)
properties = moon_data.json()['properties']
# 0 = new moon, 90 = Q1, 180 = full moon, 270 = LQ
age = float(properties['moonphase'])
# age = 169
print("AGE :", age)
return int(age)
except:
# Moon server error (maybe), try again after 15 seconds.
# (Might be a memory error, that should be handled different)
time.sleep(5.5)
age = 0
return age
def moonPhase(age):
'''
0 = Waxing cresent
1 = First Quarter
2 = Waxing gibbous
3 = Waxing gibbous
4 = Full Moon
5 = Waning gibbous
6 = Waning gibbous
7 = Last Quarter
8 = Waning cresent
9 = New Moon
'''
print("THE MOON AGE IS: ", age)
if (age >= 345) and (age <= 15): # New Moon #0
moon_sprite = 9
elif (age >= 16) and (age <= 52): # Waxing gibbous #9
moon_sprite = 8
elif (age >= 53) and (age <= 77): # Waxing gibbous #8
moon_sprite = 7
elif (age >= 78) and (age <= 91): # First Quarter # 7
moon_sprite = 6
elif (age >= 92) and (age <= 163): # Waxing gibbous #6
moon_sprite = 5
elif (age >= 163) and (age <= 197): # Full Moon #5
moon_sprite = 4
elif (age >= 188) and (age <= 251): # Waning gibbous #4
moon_sprite = 3
elif (age >= 252) and (age <= 273): # Last Quarter #3
moon_sprite = 2
elif (age >= 274) and (age <= 309): # Waning gibbous #2
moon_sprite = 1
elif (age >= 310) and (age <= 344): # Waning gibbous #1
moon_sprite = 0
else:
moon_sprite = 9 # New Moon
print("THE MOON PHASE IS: ", moon_sprite)
return moon_sprite
def update_time(timezone=None):
"""Update system date/time from WorldTimeAPI public server;
no account required. Pass in time zone string
(http://worldtimeapi.org/api/timezone for list)
or None to use IP geolocation. Returns current local time as a
time.struct_time and UTC offset as string. This may throw an
exception on fetch_data() - it is NOT CAUGHT HERE, should be
handled in the calling code because different behaviors may be
needed in different situations (e.g. reschedule for later).
{
"abbreviation": "MST",
"client_ip": "174.24.76.33",
"datetime": "2023-11-20T14:34:44.464927-07:00",
"day_of_week": 1,
"day_of_year": 324,
"dst": false,
"dst_from": null,
"dst_offset": 0,
"dst_until": null,
"raw_offset": -25200,
"timezone": "America/Denver",
"unixtime": 1700516084,
"utc_datetime": "2023-11-20T21:34:44.464927+00:00",
"utc_offset": "-07:00",
"week_number": 47
}
"""
if timezone: # Use timezone api
time_url = "http://worldtimeapi.org/api/timezone/" + timezone
print(time_url)
else: # Use IP geolocation
time_url = "http://worldtimeapi.org/api/ip"
local_time_string = requests.get(time_url)
print("-" * 40)
time_data = (local_time_string.json()["datetime"], ["dst"], ["utc_offset"])
dayofweek = local_time_string.json()["day_of_week"]
print("UPDATE_TIME_DATA")
print(time_data)
time_struct = parse_time(time_data[0], time_data[1])
print(time_struct)
return time_struct, time_data[2], dayofweek
def parse_time(timestring, is_dst=-1):
"""Given a string of the format YYYY-MM-DDTHH:MM:SS.SS-HH:MM (and
optionally a DST flag), convert to and return an equivalent
time.struct_time (strptime() isn't available here). Calling function
can use time.mktime() on result if epoch seconds is needed instead.
Time string is assumed local time; UTC offset is ignored. If seconds
value includes a decimal fraction it's ignored.
"""
date_time = timestring.split("T") # Separate into date and time
year_month_day = date_time[0].split("-") # Separate time into Y/M/D
hour_minute_second = date_time[1].split("+")[0].split("-")[0].split(":")
return time.struct_time(
(
int(year_month_day[0]),
int(year_month_day[1]),
int(year_month_day[2]),
int(hour_minute_second[0]),
int(hour_minute_second[1]),
int(hour_minute_second[2].split(".")[0]),
-1,
-1,
is_dst,
)
)
def dim_display(status):
if status == True:
display.brightness = 0.01
display14.brightness = 0.01
pixels.brightness = 0.01
displayBL.duty_cycle = 30000
else:
display.brightness = 0.6
display14.brightness = 0.6
pixels.brightness = 0.1
displayBL.duty_cycle = 50000
def display_date(day, dayname):
dayname = weekname[dayname]
display14.marquee(dayname, 0.3, False)
display14.print(f"{day:02d}")
def display_time(hours, mins):
display.fill(0)
if hours < 10:
display.print("0" + str(hours))
else:
if hours < 13:
display.print(hours)
else:
display.print(hours - 12)
display.print(":")
if mins < 10:
display.print("0" + str(mins))
else:
display.print(mins)
def display_sec(secs):
neo_seconds = secs
neo_seconds += 30
neo_seconds = neo_seconds % 60
if secs == 60:
pixels[neo_seconds] = RED
else:
pixels[neo_seconds] = GREEN
pixels[neo_seconds - 1] = BLACK
for x in range(20):
x=x+1
pixels[neo_seconds - x] = BLACK
# account for Date Display update delay
pixels.show()
def refesh_time():
datetime, UTC_OFFSET, dayofweek = update_time(TIMEZONE)
hours = datetime.tm_hour
mins = datetime.tm_min
secs = datetime.tm_sec
month = datetime.tm_mon
year= datetime.tm_year
day = datetime.tm_mday
dayofWeek = dayofweek
return year, month, day, dayofWeek, hours, mins, secs
def displaySprite(spriteNumber=0):
fb = dotclockframebuffer.DotClockFramebuffer(**tft_pins, **tft_timings)
displaysprite = FramebufferDisplay(fb, auto_refresh=False, rotation = 90)
if spriteNumber <= 8:
sprite_sheet, palette = adafruit_imageload.load("/bmps/TheMoons720.bmp",
bitmap=displayio.Bitmap,
palette=displayio.Palette)
else:
sprite_sheet, palette = adafruit_imageload.load("/bmps/NEWBLUEMOON.bmp",
bitmap=displayio.Bitmap,
palette=displayio.Palette)
# Create a sprite (tilegrid)
sprite = displayio.TileGrid(sprite_sheet, pixel_shader=palette,
width = 1,
height = 1,
tile_width = 240,
tile_height = 240)
# Create a Group to hold the sprite
group = displayio.Group(scale=2)
sprite[0] = spriteNumber % 9
# Add the sprite to the Group
group.append(sprite)
# Add the Group to the Display
displaysprite.root_group = group
displaysprite.auto_refresh = True
# Set sprite location
group.x = 0
group.y = 0
# Loop through each sprite in the sprite sheet
source_index = 0
# Initialize Clock and get it started
year, month, day, dayofWeek, hours, mins, secs = refesh_time()
display_sec(secs)
display_time(hours, mins)
display_date(day, dayofWeek)
print(year, month, day, dayofWeek, hours, mins, secs)
age = MoonData(year, month, day, LATITUDE, LONGITUDE)
print("MOONPHASE IS: ", moonPhase(age))
displaySprite(moonPhase(age))
# Start running the clock
while True:
if hours >= 19 or hours <= 6:
dim_display(True)
else:
dim_display(False)
display_sec(secs)
if secs == 60:
mins = mins + 1
if mins > 59:
hours = hours + 1
mins = 0
if hours > 23:
hours = 0
secs = 0
display_time(hours, mins)
if refresh_time_update == 0:
gc.collect()
release_displays()
year, month, day, dayofWeek, hours, mins, secs = refesh_time()
display_time(hours, mins)
display_date(day, dayofWeek)
refresh_time_update = 7200 # Update every 2 hour
age = MoonData(year, month, day, LATITUDE, LONGITUDE)
displaySprite(moonPhase(age))
#time.sleep(0.995)
time.sleep(0.987) # Adjust seconds to account display delays
secs = secs + 1
refresh_time_update = refresh_time_update - 1
This page (Digital / RGB-666 Moon Clock) was last updated on March 28, 2024.
Text editor powered by tinymce.