NeoPixel Sprite Weather Display
2023-03-21 | By Adafruit Industries
License: See Original Project
Courtesy of Adafuit
Guide by Liz Clark
Overview
With all of the weather API's and display options, it can be overwhelming to decide which approach to take when building a weather display. This project takes the less is more, or smol is more, approach.
You can use the tiny yet mighty 5x5 NeoPixel Grid BFF add-on board with a QT Py ESP32-S2 to display the current weather conditions and temperature. The CircuitPython code is using the Open-Meteo API to fetch current weather data. Open-Meteo is a free and open-source weather API that does not require an API key, making it fast to get started with.
The current weather condition is shown with a 5x5 sprite on the NeoPixel Grid. There are sprites for sunny, partly cloudy, clouds, rain, thunderstorms and snow with day and night versions for each.
The current temperature scrolls across the display after showing the sprite. The color of the text changes depending on the temperature. Lower temperatures are represented with cooler colors (green, blue, and purple) and higher temperatures are represented with warmer colors (yellow, orange, and red).
Prerequisite Guides
Adafruit 5x5 NeoPixel Grid BFF
Parts
- Adafruit QT Py ESP32-S2 WiFi Dev Board with STEMMA QT
- Adafruit 5x5 NeoPixel Grid BFF Add-On for QT Py and Xiao
- Break-away 0.1" 36-pin strip male header - Rainbow Combo 10 Pack
- 20-pin 0.1" Female Headers - Rainbow Color Mix - 5 pack
- Pink and Purple Woven USB A to USB C Cable - 1 meter long
CircuitPython
CircuitPython is a derivative of MicroPython designed to simplify experimentation and education on low-cost microcontrollers. It makes it easier than ever to get prototyping by requiring no upfront desktop software downloads. Simply copy and edit files on the CIRCUITPY drive to iterate.
CircuitPython QuickStart
Follow this step-by-step to quickly get CircuitPython running on your board.
Download the latest version of CircuitPython for this board via circuitpython.org
Click the link above to download the latest CircuitPython UF2 file.
Save it wherever is convenient for you.
Plug your board into your computer, using a known-good data-sync cable, directly, or via an adapter if needed.
Click the reset button once (highlighted in red above), and then click it again when you see the RGB status LED(s) (highlighted in green above) turn purple (approximately half a second later). Sometimes it helps to think of it as a "slow double-click" of the reset button.
For this board, tap reset and wait for the LED to turn purple, and as soon as it turns purple, tap reset again. The second tap needs to happen while the LED is still purple.
Once successful, you will see the RGB status LED(s) turn green (highlighted in green above). If you see red, try another port, or if you're using an adapter or hub, try without the hub, or different adapter or hub.
If double-clicking doesn't work the first time, try again. Sometimes it can take a few tries to get the rhythm right!
A lot of people end up using charge-only USB cables and it is very frustrating! Make sure you have a USB cable you know is good for data sync.
If after several tries, and verifying your USB cable is data-ready, you still cannot get to the bootloader, it is possible that the bootloader is missing or damaged. Check out the Install UF2 Bootloader page for details on resolving this issue.
You will see a new disk drive appear called QTPYS2BOOT.
Drag the adafruit_circuitpython_etc.uf2 file to QTPYS2BOOT.
The BOOT drive will disappear, and a new disk drive called CIRCUITPY will appear.
That's it!
Create Your settings.toml File
If you've worked on WiFi projects with CircuitPython before, you're probably familiar with the secrets.py file. This file is a Python file that is stored on your CIRCUITPY drive that contains all of your secret WiFi information, such as your SSID, SSID password and any API keys for IoT services.
As of CircuitPython 8.0.0-beta.6, there is support for a settings.toml file. Similar to secrets.py, the settings.toml file separates your sensitive information from your main code.py file.
settings.toml File Example
Here is an example on how to format your settings.toml file.
# Comments are supported
CIRCUITPY_WIFI_SSID="guest wifi"
CIRCUITPY_WIFI_PASSWORD="guessable"
CIRCUITPY_WEB_API_PORT=80
CIRCUITPY_WEB_API_PASSWORD="passw0rd"
test_variable="this is a test"
thumbs_up="\U0001f44d"
In a settings.toml file, it's important to keep these factors in mind:
- Strings are wrapped in double quotes; ex: "your-string-here"
- Integers are not quoted and may be written in decimal with optional sign ( +1, -1, 1000) or hexadecimal (0xabcd)
- Floats, octal (0o567) and binary (0b11011) are not supported
- Use \u escapes for weird characters, \x and \ooo escapes are not available in .toml files
- Example: \U0001f44d for thumbs up emoji and \u20ac for € (EUR sign)
- Unicode emoji, and non-ASCII characters, stand for themselves as long as you're careful to save in "UTF-8 without BOM" format
When your settings.toml file is ready, you can save it in your text editor with the .toml extension.
Accessing Your settings.toml Information in code.py
In your code.py file, you'll need to import the os library to access the settings.toml file. Your settings are accessed with the os.getenv() function. You'll pass your settings entry to the function to import it into the code.py file.
import os
print(os.getenv("test_variable"))
In the upcoming CircuitPython WiFi examples, you'll see how the settings.toml file is used for connecting to your SSID and accessing your API keys.
Code the Weather Display
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 as a zipped folder.
# SPDX-FileCopyrightText: 2022 Liz Clark for Adafruit Industries
# SPDX-License-Identifier: MIT
import os
import ssl
import time
import board
import wifi
import socketpool
import fontio
import neopixel
import simpleio
from adafruit_display_text.bitmap_label import Label
from adafruit_bitmap_font import bitmap_font
from displayio import Bitmap
from rainbowio import colorwheel
from adafruit_ticks import ticks_ms, ticks_add, ticks_diff
import adafruit_requests
from weather_codes import weather_codes
# minimum expected temperature
min_temp = 0
# maximum expected temperature
max_temp = 100
# first daylight hour
daytime_min = 7
# last daylight hour
daytime_max = 17
# latitude
lat = 42.36
# longitude
long = -71.06
# temp unit for API request
temperature_unit = "fahrenheit"
# temp unit for display
temp_unit = "F"
# API request to open-meteo
weather_url = "https://api.open-meteo.com/v1/forecast?"
# pass latitude and longitude
weather_url = "latitude=%d&longitude=%d&timezone=auto" % (lat, long)
# pass temperature_unit
weather_url = "¤t_weather=true&temperature_unit=%s&windspeed_unit=mph" % temperature_unit
# connect to SSID
wifi.radio.connect(os.getenv('CIRCUITPY_WIFI_SSID'), os.getenv('CIRCUITPY_WIFI_PASSWORD'))
pool = socketpool.SocketPool(wifi.radio)
requests = adafruit_requests.Session(pool, ssl.create_default_context())
def get_the_weather():
# make the API request
response = requests.get(weather_url)
# packs the response into a JSON
response_as_json = response.json()
print()
# prints the entire JSON
print(response_as_json)
print()
# gets current weather code
w = int(response_as_json['current_weather']['weathercode'])
# gets temperature
t = response_as_json['current_weather']['temperature']
temp_int = int(t)
t_c = simpleio.map_range(temp_int, min_temp, max_temp, 255, 0)
# gets time
json_time = response_as_json['current_weather']['time']
n_t = json_time.rsplit("T", 1)[-1]
n_t = int(n_t[:2])
return w, t, t_c, n_t
# initial API call
weather, temp, temp_color, new_time = get_the_weather()
# font edit code by Jeff Epler
tom_thumb = bitmap_font.load_font("tom-thumb.pcf", Bitmap)
_glyph_keys = ['bitmap', 'tile_index', 'width', 'height', 'dx', 'dy', 'shift_x', 'shift_y']
def patch_glyph(base, **kw):
d = {}
for k in _glyph_keys:
d[k] = kw.get(k, getattr(base, k))
return fontio.Glyph(**d)
class PatchedFont:
def __init__(self, base_font, patches):
self.base_font = base_font
self.patches = patches
def get_glyph(self, glyph):
g = self.base_font.get_glyph(glyph)
patch = self.patches.get(glyph)
if patch is not None:
#print("patching", repr(chr(glyph)), g)
g = patch_glyph(g, **patch)
#print("patched", g)
return g
def get_bounding_box(self):
return self.base_font.get_bounding_box()
font = PatchedFont(tom_thumb,
{
32: {'shift_x': 1, 'dx': 0},
105: {'dx': 0, 'shift_x': 2},
33: {'dx': 0, 'shift_x': 2},
})
# thank you Jeff for this PatchedFont() function!
# temperature for scrolling text
label = Label(text=" %s°%s " % (temp, temp_unit), font=font)
text = label.bitmap
# create 5x5 neopixels
pixels = neopixel.NeoPixel(board.A3, 5*5, brightness=.08, auto_write=False)
# count for pixels when drawing bitmaps
count = 0
# arrays to pack assets from weather_codes helper
# weather condition code
codes = []
# bitmaps for daytime
day_images = []
# bitmaps for nighttime
night_images = []
for i in weather_codes:
codes.append(i['code'])
day_images.append(i['day_img'])
night_images.append(i['night_img'])
# checks if it's day or night based on hour
def day_or_night(t):
if t in range(daytime_min, daytime_max):
z = day_images[weather]
else:
z = night_images[weather]
return z
# initial sprite selection
img = day_or_night(new_time)
# draw bitmap sprite
def draw_sprite(c):
for pixel in img:
pixels[c] = pixel
pixels.show()
c = 1
time.sleep(0.001)
c = 0
# ticks time tracker
clock = ticks_ms()
# 15 minutes in milliseconds
weather_check = 900000
# display current weather sprite & scroll temperature
while True:
# checks the time
if ticks_diff(ticks_ms(), clock) > weather_check:
print("pinging Open-Meteo")
# make the API request with function
# return weather ID, temp, temp color & hour
weather, temp, temp_color, new_time = get_the_weather()
# checks if it's day or night based on hour
# & returns day or night version of sprite
img = day_or_night(new_time)
label.text = " %s°%s " % (temp, temp_unit)
# reset clock
clock = ticks_add(clock, weather_check)
# draw bitmap sprite
draw_sprite(count)
# blocking delay to hold the sprite on the display
time.sleep(5)
# draw scrolling text
for v in range(2):
for i in range(text.width):
# Scoot the old text left by 1 pixel
pixels[:20] = pixels[5:]
# adjust color based on temperature
color = colorwheel(temp_color)
# Draw in the next line of text
for y in range(5):
# Select black or color depending on the bitmap pixel
pixels[20 y] = color * text[i,y]
pixels.show()
time.sleep(.1)
Upload the Code and Libraries to the QT Py ESP32-S2
After downloading the Project Bundle, plug your QT Py ESP32-S2 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 ESP32-S2's CIRCUITPY drive.
- lib folder
- code.py
- weather_codes.py
Your QT Py ESP32-S2 CIRCUITPY drive should look like this after copying the lib folder, weather_codes.py file and the code.py file.
Add Your settings.toml File
As of CircuitPython 8.0.0, there is support for Environment Variables. These Environmental 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.
CIRCUITPY_WIFI_SSID = "your-ssid-here"
CIRCUITPY_WIFI_PASSWORD = "your-ssid-password-here"
The weather_codes.py File
The weather_codes.py file is a helper file that contains the sprite information and assigns the sprites to the weather condition codes that will be returned from Open-Meteo.
The file begins by defining RGB color values that will be used to create the sprites.
y = (255, 125, 0)
o = (0, 0, 0)
a = (0, 75, 125)
w = (255, 255, 255)
v = (127, 0, 255)
b = (0, 0, 255)
z = (0, 0, 25)
g = (25, 25, 25)
The sprites are defined as five-by-five arrays. In total, there are eleven sprite arrays.
sun_bitmap = [
y,a,y,a,y,
a,y,y,y,a,
y,y,y,y,y,
a,y,y,y,a,
y,a,y,a,y,
]
cloud_bitmap = [
a,a,a,w,a,
a,w,w,w,a,
a,w,w,w,a,
a,a,w,w,a,
a,a,a,w,a,
]
# etc
weather_codes is a dictionary that contains the weather codes and the associated daytime and nighttime sprite. The codes are WMO Codes as defined by NOAA. There are 100 possible codes, ranging from 0 to 99. In code.py, these weather codes are used as indexes to access the appropriate sprite.
weather_codes = [
{"code" : 0, "day_img" : sun_bitmap, "night_img" : night_bitmap},
{"code" : 1, "day_img" : sun_bitmap, "night_img" : night_bitmap},
{"code" : 2, "day_img" : sun_bitmap, "night_img" : night_bitmap},
...
{"code" : 99, "day_img" : thunder_bitmap, "night_img" : nightThunder_bitmap}
]
How the CircuitPython Code Works
At the top of the code are variables that can be edited to customize your code.py file for your needs. min_temp and max_temp are used in the map_range() function that determine the color of the temperature text. The min_temp number can be negative if you are in a colder climate. daytime_min and daytime_max are used to determine the hours in which a daytime sprite is shown versus a nighttime sprite. lat and long hold your location's latitude and longitude. Finally, temperature_unit and temp_unit hold either Fahrenheit or Celsius.
# minimum expected temperature
min_temp = 0
# maximum expected temperature
max_temp = 100
# first daylight hour
daytime_min = 7
# last daylight hour
daytime_max = 17
# latitude
lat = 42.36
# longitude
long = -71.06
# temp unit for API request
temperature_unit = "fahrenheit"
# temp unit for display
temp_unit = "F"
The Request URL
The API request to Open-Meteo is passed as a URL. lat, long and temperature_unit are passed to the URL string. Open-Meteo has documentation with more information on building a URL for an API request.
# API request to open-meteo
weather_url = "https://api.open-meteo.com/v1/forecast?"
# pass latitude and longitude
weather_url = "latitude=%d&longitude=%d&timezone=auto" % (lat, long)
# pass temperature_unit
weather_url = "¤t_weather=true&temperature_unit=%s&windspeed_unit=mph" % temperature_unit
Get the Weather
The function get_the_weather() is used to make the API request and return values for the current weather condition, temperature, the color mapped to the current temperature and the current time.
def get_the_weather():
# make the API request
response = requests.get(weather_url)
# packs the response into a JSON
response_as_json = response.json()
print()
# prints the entire JSON
print(response_as_json)
print()
# gets current weather code
w = int(response_as_json['current_weather']['weathercode'])
# gets temperature
t = response_as_json['current_weather']['temperature']
temp_int = int(t)
t_c = simpleio.map_range(temp_int, min_temp, max_temp, 255, 0)
# gets time
json_time = response_as_json['current_weather']['time']
n_t = json_time.rsplit("T", 1)[-1]
n_t = int(n_t[:2])
return w, t, t_c, n_t
# initial API call
weather, temp, temp_color, new_time = get_the_weather()
Import the Sprites
A for statement packs the list entries from weather_codes into three different arrays: codes for the weather code numbers, day_images for the daytime sprites and night_images for the nighttime sprites.
for i in weather_codes:
codes.append(i['code'])
day_images.append(i['day_img'])
night_images.append(i['night_img'])
Day or Night?
The function day_or_night() determines which sprite is shown based on the weather condition and time of day.
# checks if it's day or night based on hour
def day_or_night(t):
if t in range(daytime_min, daytime_max):
z = day_images[weather]
else:
z = night_images[weather]
return z
# initial sprite selection
img = day_or_night(new_time)
Draw the Sprite
The draw_sprite() function draws the weather sprite to the 5x5 grid of NeoPixels.
# draw bitmap sprite
def draw_sprite(c):
for pixel in img:
pixels[c] = pixel
pixels.show()
c = 1
time.sleep(0.001)
c = 0
Tick, tick, tick...
For tracking time, the ticks library is used. ticks manages time in milliseconds, rather than seconds like in time.monotonic(). The API will be called every fifteen minutes, or every 900000 milliseconds.
# ticks time tracker
clock = ticks_ms()
# 15 minutes in milliseconds
weather_check = 900000
The Loop
In the loop, the API is called every 15 minutes. The sprite and scrolling temperature text is updated depending on the data that is returned.
# checks the time
if ticks_diff(ticks_ms(), clock) > weather_check:
print("pinging Open-Meteo")
# make the API request with function
# return weather ID, temp, temp color & hour
weather, temp, temp_color, new_time = get_the_weather()
# checks if it's day or night based on hour
# & returns day or night version of sprite
img = day_or_night(new_time)
label.text = " %s°%s " % (temp, temp_unit)
# reset clock
clock = ticks_add(clock, weather_check)
Scroll the Weather
The core functionality of the display loops continuously. The sprite is drawn to the 5x5 grid and is shown for five seconds. Then, the temperature text is scrolled across the display. Once the text finishes scrolling, the process begins again by drawing the sprite.
# draw bitmap sprite
draw_sprite(count)
# blocking delay to hold the sprite on the display
time.sleep(5)
# draw scrolling text
for v in range(2):
for i in range(text.width):
# Scoot the old text left by 1 pixel
pixels[:20] = pixels[5:]
# adjust color based on temperature
color = colorwheel(temp_color)
# Draw in the next line of text
for y in range(5):
# Select black or color depending on the bitmap pixel
pixels[20 y] = color * text[i,y]
pixels.show()
time.sleep(.1)
3D Printing
The weather display may be assembled with 3D printed parts, described below. The case has two parts: a cloud-shaped lid and a case to house the boards. Both parts print without supports.
The STL files can be downloaded directly here, from Thingiverse or from Printables.
5x5WeatherDisplaySTLfiles.zip
The main case has cutouts for the QT Py USB C port and the reset and boot buttons on the back. It has tiny holes to mount the QT Py header pins to keep the boards properly secured in the case.
The cloud lid has an opening for the 5x5 NeoPixel Grid. Its snap fits onto the main case.
Assembly
Solder socket headers to the 5x5 NeoPixel Grid BFF and solder plug headers to the QT Py ESP32-S2.
Plug the QT Py ESP32-S2 into the NeoPixel Grid BFF. Make sure that the QT Py USB port is oriented properly by referencing the USB labeling and arrow on the back of the BFF.
Insert the boards into the main case by lining up the QT Py header pins with the header pin slots in the bottom of the case.
The boards should be situated in the center of the case with the QT Py USB port accessible via the side cutout.
Close the case with the cloud lid. Make sure that the 5x5 NeoPixel Grid is centered in the cloud's cutout.
That completes the assembly!
Usage
To use the weather display, plug the QT Py ESP32-S2 into a USB-C cable to power it. After connecting to your SSID / WiFi, the QT Py will make a request to the Open-Meteo API and display the current weather condition sprite and the current temperature.
As the incoming data changes from the API, the displayed sprite and temperature reading will update. Additionally, the sprite will change depending on the time of day as each sprite has a daytime and nighttime version.
Update Your Latitude and Longitude
Open-Meteo uses latitude and longitude to pinpoint your location's weather. You can update these values to match your location at the top of the code.py file.
You can use Google Maps to find the latitude and longitude of your location. Search your location and then right-click on the map. The latitude and longitude will appear in a pop-up window on the screen.
Customize the API Request
You can customize the Open-Meteo API request by changing the parameters in the URL. Open-Meteo has documentation and a URL builder to track various pieces of weather data.
Have questions or comments? Continue the conversation on TechForum, DigiKey's online community and technical resource.
Visit TechForum