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