OK, let's load up our PyPortal with the hurricane tracker code. You'll need a few additional libraries, as mentioned below. You'll also need the BMP files for the map and icons. And finally, there's the code itself.
Note - the hurricane tracker version provided here is for the Atlantic Ocean only. The NOAA JSON data source also covers the Eastern and Central Pacific. We think adapting this code for those regions would make for a fun knowledge building exercise.
Libraries
In addition to all the libraries needed for the PyPortal (see PyPortal CircuitPython Setup), you'll also need these libraries:
- adafruit_display_shapes
- simpleio
Make sure your CIRCUITPY/lib folder contains them.
This is the sprite sheet bitmap used for the storm icons. Save this as storm_icons.bmp in your CIRCUITPY folder:
# SPDX-FileCopyrightText: 2020 Carter Nelson for Adafruit Industries
#
# SPDX-License-Identifier: MIT
import time
import math
import board
import displayio
import terminalio
from simpleio import map_range
import adafruit_imageload
from adafruit_pyportal import PyPortal
from adafruit_display_text.label import Label
from adafruit_display_shapes.line import Line
# --| User Config |---------------------------------------------------
UPDATE_RATE = 60 # minutes
MAX_STORMS = 3 # limit storms
NAME_COLOR = 0xFFFFFF # label text color
NAME_BG_COLOR = 0x000000 # label background color
ARROW_COLOR = 0x0000FF # movement direction arrow color
ARROW_LENGTH = 15 # movement direction arrow length
LAT_RANGE = (45, 5) # set to match map
LON_RANGE = (-100, -40) # set to match map
# --------------------------------------------------------------------
URL = "https://www.nhc.noaa.gov/CurrentStorms.json"
JSON_PATH = ["activeStorms"]
# setup pyportal
pyportal = PyPortal(
status_neopixel=board.NEOPIXEL,
default_bg="/map.bmp",
)
# setup display group for storms
icons_bmp, icons_pal = adafruit_imageload.load(
"/storm_icons.bmp", bitmap=displayio.Bitmap, palette=displayio.Palette
)
for i, c in enumerate(icons_pal):
if c == 0xFFFF00:
icons_pal.make_transparent(i)
storm_icons = displayio.Group()
pyportal.root_group.append(storm_icons)
STORM_CLASS = ("TD", "TS", "HU")
# setup info label
info_update = Label(
terminalio.FONT,
text="1984-01-01T00:00:00.000Z",
color=NAME_COLOR,
background_color=NAME_BG_COLOR,
)
info_update.anchor_point = (0.0, 1.0)
info_update.anchored_position = (10, board.DISPLAY.height - 10)
pyportal.root_group.append(info_update)
# these are need for lat/lon to screen x/y mapping
VIRTUAL_WIDTH = board.DISPLAY.width * 360 / (LON_RANGE[1] - LON_RANGE[0])
VIRTUAL_HEIGHT = board.DISPLAY.height * 360 / (LAT_RANGE[0] - LAT_RANGE[1])
Y_OFFSET = math.radians(LAT_RANGE[0])
Y_OFFSET = math.tan(math.pi / 4 + Y_OFFSET / 2)
Y_OFFSET = math.log(Y_OFFSET)
Y_OFFSET = (VIRTUAL_WIDTH * Y_OFFSET) / (2 * math.pi)
Y_OFFSET = VIRTUAL_HEIGHT / 2 - Y_OFFSET
def update_display():
# pylint: disable=too-many-locals
# clear out existing icons
while len(storm_icons):
_ = storm_icons.pop()
# get latest storm data
try:
resp = pyportal.network.fetch(URL)
storm_data = pyportal.network.process_json(resp.json(), (JSON_PATH,))[0]
except RuntimeError:
return
print("Number of storms:", len(storm_data))
# parse the storm data
for storm in storm_data:
# don't exceed max
if len(storm_icons) >= MAX_STORMS:
continue
# get lat/lon
lat = storm["latitudeNumeric"]
lon = storm["longitudeNumeric"]
# check if on map
if (
not LAT_RANGE[0] >= lat >= LAT_RANGE[1]
or not LON_RANGE[0] <= lon <= LON_RANGE[1]
):
continue
# OK, let's make a group for all the graphics
storm_gfx = displayio.Group()
# convert to sreen coords
x = int(map_range(lon, LON_RANGE[0], LON_RANGE[1], 0, board.DISPLAY.width - 1))
y = math.radians(lat)
y = math.tan(math.pi / 4 + y / 2)
y = math.log(y)
y = (VIRTUAL_WIDTH * y) / (2 * math.pi)
y = VIRTUAL_HEIGHT / 2 - y
y = int(y - Y_OFFSET)
# icon type
if storm["classification"] in STORM_CLASS:
storm_type = STORM_CLASS.index(storm["classification"])
else:
storm_type = 0
# create storm icon
icon = displayio.TileGrid(
icons_bmp,
pixel_shader=icons_pal,
width=1,
height=1,
tile_width=16,
tile_height=16,
default_tile=storm_type,
x=x - 8,
y=y - 8,
)
# add storm icon
storm_gfx.append(icon)
# add a label
name = Label(
terminalio.FONT,
text=storm["name"],
color=NAME_COLOR,
background_color=NAME_BG_COLOR,
)
name.anchor_point = (0.0, 1.0)
name.anchored_position = (x + 8, y - 8)
storm_gfx.append(name)
# add direction arrow
angle = math.radians(storm["movementDir"])
xd = x + int(ARROW_LENGTH * math.sin(angle))
yd = y - int(ARROW_LENGTH * math.cos(angle))
arrow = Line(x, y, xd, yd, color=ARROW_COLOR)
storm_gfx.append(arrow)
# add the storm graphics
storm_icons.append(storm_gfx)
# update time
info_update.text = storm["lastUpdate"]
# debug
print(
"{} @ {},{}".format(
storm["name"], storm["latitudeNumeric"], storm["longitudeNumeric"]
)
)
# no storms? at least say something
if not len(storm_icons):
print("No storms in map area.")
storm_icons.append(
Label(
terminalio.FONT,
scale=4,
x=50,
y=110,
text="NO STORMS\n IN AREA",
color=NAME_COLOR,
background_color=NAME_BG_COLOR,
)
)
# --------------------------------------------------------------------
# M A I N
# --------------------------------------------------------------------
update_display()
last_update = time.monotonic()
while True:
now = time.monotonic()
if now - last_update > UPDATE_RATE * 60:
print("Updating...")
update_display()
last_update = now
Make sure your PyPortal CIRCUITPY drive has these files in the right directories:
Page last edited January 21, 2025
Text editor powered by tinymce.