Once you've finished setting up your QT Py ESP32-S2 with CircuitPython, you can access the code and necessary libraries by downloading the Project Bundle.
To do this, click on the Download Project Bundle button in the window below. It will download to your computer as a zipped folder.
# SPDX-FileCopyrightText: 2025 Liz Clark for Adafruit Industries # SPDX-License-Identifier: MIT import os import time import ssl import board import wifi import socketpool import microcontroller import neopixel import adafruit_requests from adafruit_ticks import ticks_ms, ticks_add, ticks_diff # FarmSense API for moon phase # https://www.farmsense.net/api/astro-widgets/ url = "https://api.farmsense.net/v1/moonphases/?d=" # Adafruit IO time server for UNIX time, no API key needed time_url = "https://io.adafruit.com/api/v2/time/seconds" # connect to wifi try: wifi.radio.connect(os.getenv('CIRCUITPY_WIFI_SSID'), os.getenv('CIRCUITPY_WIFI_PASSWORD')) except TypeError: print("Could not find WiFi info. Check your settings.toml file!") raise pool = socketpool.SocketPool(wifi.radio) requests = adafruit_requests.Session(pool, ssl.create_default_context()) # neopixels, 49 total OFF = (0, 0, 0) ON = (255, 255, 255) RED = (255,0,0) pixel_pin = board.A3 num_pixels = 49 pixels = neopixel.NeoPixel(pixel_pin, num_pixels, brightness=0.1, auto_write=False) pixels.fill(0) # phases of the moon NEW_MOON = 0 WAXING_CRESCENT = 1 FIRST_QUARTER = 2 WAXING_GIBBOUS = 3 FULL_MOON = 4 WANING_GIBBOUS = 5 THIRD_QUARTER = 6 WANING_CRESCENT = 7 DARK_MOON = 8 RED_MOON = 9 # strings that match return from API phase_names = ["New Moon", "Waxing Crescent", "First Quarter", "Waxing Gibbous", "Full Moon", "Waning Gibbous", "Third Quarter", "Waning Crescent","Dark Moon","Red Moon"] # functions for each moon phase to light up based on neopixel orientation def set_new_moon(): pixels.fill(OFF) pixels.show() def set_waxing_crescent(): pixels.fill(OFF) for i in range(31, 44): pixels[i] = ON pixels.show() def set_first_quarter(): pixels.fill(OFF) for i in range(24, 49): pixels[i] = ON pixels.show() def set_waxing_gibbous(): pixels.fill(OFF) for i in range(0, 4): pixels[i] = ON for i in range(18, 49): pixels[i] = ON pixels.show() def set_full_moon(): pixels.fill(ON) pixels.show() def set_waning_gibbous(): pixels.fill(OFF) for i in range(0, 30): pixels[i] = ON for i in range(44, 49): pixels[i] = ON pixels.show() def set_third_quarter(): pixels.fill(OFF) for i in range(0, 24): pixels[i] = ON pixels.show() def set_waning_crescent(): pixels.fill(OFF) for i in range(5, 18): pixels[i] = ON pixels.show() def set_dark_moon(): pixels.fill(OFF) for i in range(9,14): pixels[i] = ON pixels.show() def set_red_moon(): pixels.fill(RED) pixels.show() # match functions with phases phase_functions = { NEW_MOON: set_new_moon, WAXING_CRESCENT: set_waxing_crescent, FIRST_QUARTER: set_first_quarter, WAXING_GIBBOUS: set_waxing_gibbous, FULL_MOON: set_full_moon, WANING_GIBBOUS: set_waning_gibbous, THIRD_QUARTER: set_third_quarter, WANING_CRESCENT: set_waning_crescent, DARK_MOON: set_dark_moon, RED_MOON: set_red_moon } # test function, runs through all 8 in order def demo_all_phases(delay=1): for phase in range(9): print(f"Setting phase: {phase_names[phase]}") phase_functions[phase]() time.sleep(delay) demo_all_phases() # takes response from API, matches to function, runs function def set_moon_phase(phase): phase_lower = phase.lower() error_check = 0 for i, name in enumerate(phase_names): if phase_lower == name.lower(): error_check = 1 phase_functions[i]() print(f"Moon phase set to: {name}") if error_check == 0: print("ERROR") set_red_moon() #error indicator if API responce is unexpected # time keeping, fetches API every 6 hours timer_clock = ticks_ms() timer = (6 * 3600) * 1000 first_run = True while True: try: if first_run or ticks_diff(ticks_ms(), timer_clock) >= timer: # get unix time unix_time = requests.get(time_url) # update farmsense request with UNIX time url = f"https://api.farmsense.net/v1/moonphases/?d={unix_time.text}" # get the JSON response response = requests.get(url) json_response = response.json() # isolate phase info print("-" * 40) print(json_response[0]['Phase']) print("-" * 40) # run function to update neopixels with current phase set_moon_phase(json_response[0]['Phase']) response.close() time.sleep(1) first_run = False # reset clock timer_clock = ticks_add(timer_clock, timer) # pylint: disable=broad-except except Exception as e: print("Error:\n", str(e)) print("Resetting microcontroller in 10 seconds") time.sleep(10) microcontroller.reset()
Upload the Code and Libraries to the QT Py
After downloading the Project Bundle, plug your QT Py into the computer's USB port with a known good USB data+power cable. You should see a new flash drive appear in the computer's File Explorer or Finder (depending on your operating system) called CIRCUITPY. Unzip the folder and copy the following items to the QT Py's CIRCUITPY drive.
- lib folder
- code.py
Your QT Py ESP32-S2 CIRCUITPY drive should look like this after copying the lib folder and the code.py file.

Add Your settings.toml File
As of CircuitPython 8.0.0, there is support for Environment Variables. Environment variables are stored in a settings.toml file. Similar to secrets.py, the settings.toml file separates your sensitive information from your main code.py file. Add your settings.toml file as described in the Create Your settings.toml File page earlier in this guide. You'll need to include your CIRCUITPY_WIFI_SSID
and CIRCUITPY_WIFI_PASSWORD
. Adafruit IO credentials and other data are not needed for this project.
CIRCUITPY_WIFI_SSID = "your-ssid-here" CIRCUITPY_WIFI_PASSWORD = "your-ssid-password-here"
How the Code Works
The moon phase information is fetched from the FarmSense API. It is a free API that supplies agricultural data such as day lengths, frost dates and, most importantly for this project, moon phases. For the moon phase API, you supply the request with the current Unix timestamp. You'll use the Adafruit IO time API to fetch the Unix timestamp. You do not need to include your Adafruit IO key to use this API.
# FarmSense API for moon phase # https://www.farmsense.net/api/astro-widgets/ url = "https://api.farmsense.net/v1/moonphases/?d=" # Adafruit IO time server for UNIX time, no API key needed time_url = "https://io.adafruit.com/api/v2/time/seconds" # connect to wifi try: wifi.radio.connect(os.getenv('CIRCUITPY_WIFI_SSID'), os.getenv('CIRCUITPY_WIFI_PASSWORD')) except TypeError: print("Could not find WiFi info. Check your settings.toml file!") raise pool = socketpool.SocketPool(wifi.radio) requests = adafruit_requests.Session(pool, ssl.create_default_context())
NeoPixels
The NeoPixels are setup on pin A3. Two colors are defined: OFF
(all pixels off) and ON
(white). To start, the NeoPixels are turned off to reset any pixels.
# neopixels, 49 total OFF = (0, 0, 0) ON = (255, 255, 255) pixel_pin = board.A3 num_pixels = 49 pixels = neopixel.NeoPixel(pixel_pin, num_pixels, brightness=0.1, auto_write=False) pixels.fill(0)
Phases of the Moon
The phases of the moon are returned as strings from the API. The list phase_names
is created to store these strings. Variables for the phases are also created.
# phases of the moon NEW_MOON = 0 WAXING_CRESCENT = 1 FIRST_QUARTER = 2 WAXING_GIBBOUS = 3 FULL_MOON = 4 WANING_GIBBOUS = 5 THIRD_QUARTER = 6 WANING_CRESCENT = 7 # strings that match return from API phase_names = ["New Moon", "Waxing Crescent", "First Quarter", "Waxing Gibbous", "Full Moon", "Waning Gibbous", "Third Quarter", "Waning Crescent"]
Each phase has a function that turns the necessary NeoPixels on or off to display the phase. For example, set_new_moon()
turns all of the pixels off. set_third_quarter()
sets half of the pixels on.
def set_new_moon(): pixels.fill(OFF) pixels.show() ... def set_third_quarter(): pixels.fill(OFF) for i in range(0, 24): pixels[i] = ON pixels.show()
These functions are matched to the phase and phase index with the phase_functions
dictionary.
# match functions with phases phase_functions = { NEW_MOON: set_new_moon, WAXING_CRESCENT: set_waxing_crescent, FIRST_QUARTER: set_first_quarter, WAXING_GIBBOUS: set_waxing_gibbous, FULL_MOON: set_full_moon, WANING_GIBBOUS: set_waning_gibbous, THIRD_QUARTER: set_third_quarter, WANING_CRESCENT: set_waning_crescent }
The set_moon_phase()
function takes the current phase returned from the API and runs the matching NeoPixel function to set the NeoPixels.
# takes response from API, matches to function, runs function def set_moon_phase(phase): phase_lower = phase.lower() for i, name in enumerate(phase_names): if phase_lower == name.lower(): phase_functions[i]() print(f"Moon phase set to: {name}")
The Loop
In the loop, ticks
is used to keep time. Every 6 hours, the Unix timestamp is fetched from the Adafruit IO time API. The timestamp is packed into the FarmSense API request as an f-string. The FarmSense API returns a JSON file. The 'Phase'
index contains the current phase of the moon as a string. This string is passed to the set_moon_phase()
function to update the NeoPixels.
if first_run or ticks_diff(ticks_ms(), timer_clock) >= timer: # get unix time unix_time = requests.get(time_url) # update farmsense request with UNIX time url = f"https://api.farmsense.net/v1/moonphases/?d={unix_time.text}" # get the JSON response response = requests.get(url) json_response = response.json() # isolate phase info print("-" * 40) print(json_response[0]['Phase']) print("-" * 40) # run function to update neopixels with current phase set_moon_phase(json_response[0]['Phase']) response.close() time.sleep(1) first_run = False # reset clock timer_clock = ticks_add(timer_clock, timer)
Page last edited March 17, 2025
Text editor powered by tinymce.