|
| 1 | +import time |
| 2 | +import math |
| 3 | +import board |
| 4 | +import displayio |
| 5 | +import terminalio |
| 6 | +from simpleio import map_range |
| 7 | +import adafruit_imageload |
| 8 | +from adafruit_pyportal import PyPortal |
| 9 | +from adafruit_display_text.label import Label |
| 10 | +from adafruit_display_shapes.line import Line |
| 11 | + |
| 12 | +# --| User Config |--------------------------------------------------- |
| 13 | +UPDATE_RATE = 60 # minutes |
| 14 | +MAX_STORMS = 3 # limit storms |
| 15 | +NAME_COLOR = 0xFFFFFF # label text color |
| 16 | +NAME_BG_COLOR = 0x000000 # label background color |
| 17 | +ARROW_COLOR = 0x0000FF # movement direction arrow color |
| 18 | +ARROW_LENGTH = 15 # movement direction arrow length |
| 19 | +LAT_RANGE = (45, 5) # set to match map |
| 20 | +LON_RANGE = (-100, -40) # set to match map |
| 21 | +# -------------------------------------------------------------------- |
| 22 | + |
| 23 | +# setup pyportal |
| 24 | +pyportal = PyPortal( |
| 25 | + url="https://www.nhc.noaa.gov/CurrentStorms.json", |
| 26 | + json_path=["activeStorms"], |
| 27 | + status_neopixel=board.NEOPIXEL, |
| 28 | + default_bg="/map.bmp", |
| 29 | +) |
| 30 | + |
| 31 | +# setup display group for storms |
| 32 | +icons_bmp, icons_pal = adafruit_imageload.load( |
| 33 | + "/storm_icons.bmp", bitmap=displayio.Bitmap, palette=displayio.Palette |
| 34 | +) |
| 35 | +for i, c in enumerate(icons_pal): |
| 36 | + if c == 0xFFFF00: |
| 37 | + icons_pal.make_transparent(i) |
| 38 | +storm_icons = displayio.Group(max_size=MAX_STORMS) |
| 39 | +pyportal.splash.append(storm_icons) |
| 40 | +STORM_CLASS = ("TD", "TS", "HU") |
| 41 | + |
| 42 | +# setup info label |
| 43 | +info_update = Label( |
| 44 | + terminalio.FONT, |
| 45 | + text="1984-01-01T00:00:00.000Z", |
| 46 | + color=NAME_COLOR, |
| 47 | + background_color=NAME_BG_COLOR, |
| 48 | +) |
| 49 | +info_update.anchor_point = (0.0, 1.0) |
| 50 | +info_update.anchored_position = (10, board.DISPLAY.height - 10) |
| 51 | +pyportal.splash.append(info_update) |
| 52 | + |
| 53 | +# these are need for lat/lon to screen x/y mapping |
| 54 | +VIRTUAL_WIDTH = board.DISPLAY.width * 360 / (LON_RANGE[1] - LON_RANGE[0]) |
| 55 | +VIRTUAL_HEIGHT = board.DISPLAY.height * 360 / (LAT_RANGE[0] - LAT_RANGE[1]) |
| 56 | +Y_OFFSET = math.radians(LAT_RANGE[0]) |
| 57 | +Y_OFFSET = math.tan(math.pi / 4 + Y_OFFSET / 2) |
| 58 | +Y_OFFSET = math.log(Y_OFFSET) |
| 59 | +Y_OFFSET = (VIRTUAL_WIDTH * Y_OFFSET) / (2 * math.pi) |
| 60 | +Y_OFFSET = VIRTUAL_HEIGHT / 2 - Y_OFFSET |
| 61 | + |
| 62 | + |
| 63 | +def update_display(): |
| 64 | + # clear out existing icons |
| 65 | + while len(storm_icons): |
| 66 | + _ = storm_icons.pop() |
| 67 | + |
| 68 | + # get latest storm data |
| 69 | + try: |
| 70 | + storm_data = pyportal.fetch() |
| 71 | + except RuntimeError: |
| 72 | + return |
| 73 | + print("Number of storms:", len(storm_data)) |
| 74 | + |
| 75 | + # parse the storm data |
| 76 | + for storm in storm_data: |
| 77 | + # don't exceed max |
| 78 | + if len(storm_icons) >= MAX_STORMS: |
| 79 | + continue |
| 80 | + # get lat/lon |
| 81 | + lat = storm["latitudeNumeric"] |
| 82 | + lon = storm["longitudeNumeric"] |
| 83 | + # check if on map |
| 84 | + if ( |
| 85 | + not LAT_RANGE[0] >= lat >= LAT_RANGE[1] |
| 86 | + or not LON_RANGE[0] <= lon <= LON_RANGE[1] |
| 87 | + ): |
| 88 | + continue |
| 89 | + # OK, let's make a group for all the graphics |
| 90 | + storm_gfx = displayio.Group(max_size=3) # icon + label + arrow |
| 91 | + # convert to sreen coords |
| 92 | + x = int(map_range(lon, LON_RANGE[0], LON_RANGE[1], 0, board.DISPLAY.width - 1)) |
| 93 | + y = math.radians(lat) |
| 94 | + y = math.tan(math.pi / 4 + y / 2) |
| 95 | + y = math.log(y) |
| 96 | + y = (VIRTUAL_WIDTH * y) / (2 * math.pi) |
| 97 | + y = VIRTUAL_HEIGHT / 2 - y |
| 98 | + y = int(y - Y_OFFSET) |
| 99 | + # icon type |
| 100 | + if storm["classification"] in STORM_CLASS: |
| 101 | + storm_type = STORM_CLASS.index(storm["classification"]) |
| 102 | + else: |
| 103 | + storm_type = 0 |
| 104 | + # create storm icon |
| 105 | + icon = displayio.TileGrid( |
| 106 | + icons_bmp, |
| 107 | + pixel_shader=icons_pal, |
| 108 | + width=1, |
| 109 | + height=1, |
| 110 | + tile_width=16, |
| 111 | + tile_height=16, |
| 112 | + default_tile=storm_type, |
| 113 | + x=x - 8, |
| 114 | + y=y - 8, |
| 115 | + ) |
| 116 | + # add storm icon |
| 117 | + storm_gfx.append(icon) |
| 118 | + # add a label |
| 119 | + name = Label( |
| 120 | + terminalio.FONT, |
| 121 | + text=storm["name"], |
| 122 | + color=NAME_COLOR, |
| 123 | + background_color=NAME_BG_COLOR, |
| 124 | + ) |
| 125 | + name.anchor_point = (0.0, 1.0) |
| 126 | + name.anchored_position = (x + 8, y - 8) |
| 127 | + storm_gfx.append(name) |
| 128 | + # add direction arrow |
| 129 | + angle = math.radians(storm["movementDir"]) |
| 130 | + xd = x + int(ARROW_LENGTH * math.sin(angle)) |
| 131 | + yd = y - int(ARROW_LENGTH * math.cos(angle)) |
| 132 | + arrow = Line(x, y, xd, yd, color=ARROW_COLOR) |
| 133 | + storm_gfx.append(arrow) |
| 134 | + # add the storm graphics |
| 135 | + storm_icons.append(storm_gfx) |
| 136 | + # update time |
| 137 | + info_update.text = storm["lastUpdate"] |
| 138 | + # debug |
| 139 | + print( |
| 140 | + "{} @ {},{}".format( |
| 141 | + storm["name"], storm["latitudeNumeric"], storm["longitudeNumeric"] |
| 142 | + ) |
| 143 | + ) |
| 144 | + |
| 145 | + # no storms? at least say something |
| 146 | + if not len(storm_icons): |
| 147 | + print("No storms in map area.") |
| 148 | + storm_icons.append( |
| 149 | + Label( |
| 150 | + terminalio.FONT, |
| 151 | + scale=4, |
| 152 | + x=50, |
| 153 | + y=110, |
| 154 | + text="NO STORMS\n IN AREA", |
| 155 | + color=NAME_COLOR, |
| 156 | + background_color=NAME_BG_COLOR, |
| 157 | + ) |
| 158 | + ) |
| 159 | + |
| 160 | + |
| 161 | +# -------------------------------------------------------------------- |
| 162 | +# M A I N |
| 163 | +# -------------------------------------------------------------------- |
| 164 | +update_display() |
| 165 | +last_update = time.monotonic() |
| 166 | +while True: |
| 167 | + now = time.monotonic() |
| 168 | + if now - last_update > UPDATE_RATE * 60: |
| 169 | + print("Updating...") |
| 170 | + update_display() |
| 171 | + last_update = now |
0 commit comments