Close
0%
0%

Weather Radar!

NWS radar images & weather data, in a retro analog meter case!

Public Chat
Similar projects worth following
Merging National Weather Service radar images, Stamen Toner maps, and OpenWeather data, the Weather Radar! is a Raspberry Pi and Blinka powered weather radar viewer… housed in a retro analog meter case I found in the shed.

Overview:

When those summer storms or winter blizzards come rolling in, it can be handy to look at a weather radar to determine their direction and possible intensity. Not wanting to have another open tab on my web browser, I made the Weather Radar as a permeant retro radar viewer for my desk!

Connected to Wifi, the Radar downloads raw NWS radar images, overlays them on a map, creates and then plays looping animations of the 1-hour precipitation layer. With NWS forecasts and OpenWeather data, the Radar also shows current weather conditions and the forecast for the next few days. 

A toggle switch and 5-way navigation switch allow switching between modes/pages, while a continuous rotation potentiometer acts as a zoom knob for the map. 

Background

Originally the Weather Radar began as a small CircuitPython microcontroller project that quickly got out of hand (as well as available RAM) using static radar images from the old NWS website. 

While I upgraded to use a RaspberryPi Zero instead, the NWS upgraded their radar webpage to feature HTML5, an interactive map, and Open Geospatial Consortium (OGC) compliant layers. These layers are especially handy because they can provide us with raw radar images for a particular area via a simple URL request (using Web Map Service WMS or Web Feature Service WFS protocols).

The NWS helpfully provides a page of the OGC compliant layers they offer, including alerts, warnings, and layers for the 200+ weather radar stations).

Methodology:

For a given latitude and longitude in the USA, the Radar:

  • Obtains the nearest radar station ID (using the Weather API service)
  • Uses the radar station ID (e.g. KJAX) to obtain metadata and times for previous radar layers (e.g. XML GetCapabilities document for the 1 hour precipitation layer for the radar station in Jacksonville).
  • Generates a tiled base map using the GeoTiler library (using a given zoom level, map size, and map centre).
  • Uses all of this information to make a WMS request and download the last 5 - 10 radar images. 
  • For each time frame, combines the base map, radar image, and other layers and annotations using the Pillow imaging library.
  • Displays the combined image for each time frame, to make a looping animation.
  • Uses the latitude and longitude to get OpenWeather and NWS data for some extra forecasting jazz.
A) Base map is generated using starting latitude longitude coordinates (example is Seattle with a black and white Stamen Toner map).
B) Radar image is added on top with slight transparency. Also a marker for our starting lat long position.
C) Now combined with labels, annotation, and weather data. A circle overlay is added to make it blend better with the analog meter case.

Code:

The Weather Radar! is written in Python with Blinka support!

It's also an ongoing project that evolves with the changing of the seasons and with whatever new stuff I learn… which is to say that its code is a muddled hot mess. 

Despite this, you can find cleaner example code over on the Weather Radar! github page!

Hardware:

Schematic here!

Case:

The case is from an old analog meter I found discarded in my shed 5 years ago. I'm not entirely sure what it measured, because the battery...

Read more »

Portable Network Graphics (PNG) - 233.99 kB - 06/10/2021 at 04:51

Preview
Download

View all 8 components

  • Vibration alerts!

    Thornhill!06/10/2021 at 04:34 0 comments

    Howdy folks!

    Now that we're entering the stormy part of the year, I thought it was about time the Weather Radar had the ability to capture my attention when severe weather was heading my way (other than all the colourful alerts that flash across the display).

    I decided on adding a pancake-style vibration motor because 1) it's less grating than a tone alarm, 2) it would completely validate my spontaneous purchase of a pancake-style vibration motor a few weeks prior.

    Hardware

    To drive the motor I'm using a DRV2605L Haptic Motor Controller breakout from Adafruit, which comes with several inbuilt vibration effects as well as a CircuitPython library which makes this 178% easier. It uses I2C, so I chained it to the other I2C boards I'm using.

    The vibration motor is a slightly beefy one I got from Electronic Goldmine (although it's no longer in stock!) and can be driven from 3 - 6 volts. 

    Looking inside the Weather Radar to reveal its electronics. White text and arrows point out the newly added DRV2605L vibration motor driver (blue PCB), and the round pancake vibration motor (circular disk, stuck using double-sided tape to the lower lefthand side of the case).

    Code

    Thanks to Adafruit's DRV2605L product tutorial, the code is pretty simple!

    After initialising the board and the DRV2605 chip, I created a vibrate function (so I could define different vibration sequences in the future). 

    My "alert" sequence uses several of the inbuilt effects, starting with a ramping up transition (effect #113) so I don't startle myself or my partner when the 750ms & 1000ms alerts kick in (effects #15, #16). 

    import board
    import adafruit_drv2605
    
    i2c = board.I2C()
    drv = adafruit_drv2605.DRV2605(i2c)
    drv.library = 3 #TS2200_library_C
    def
    vibrate(vibration_sequence): """Play a vibration sequence."""
    if vibration_sequence in [0,"0","alert"]: drv.sequence[0] = adafruit_drv2605.Effect(113) drv.sequence[1] = adafruit_drv2605.Effect(119) drv.sequence[2] = adafruit_drv2605.Pause(1) drv.sequence[3] = adafruit_drv2605.Effect(15) drv.sequence[4] = adafruit_drv2605.Pause(1) drv.sequence[5] = adafruit_drv2605.Effect(16) else: pass drv.play()

    In my main loop, the Weather Radar checks for local weather warnings every 5-10 mins (depending on weather conditions) and makes a list of them.

    If the list is populated (because of a severe thunderstorm or tornado watch/warning) the alert vibration sequence runs.

    ### Get local warnings and alerts
    local_warnings, local_alerts = get_all_alerts(coordinates=(lat_long[1],lat_long[0]))
    
    ### If there's any local warnings (severe weather)
    ### run the alert sequence
    if len(local_warnings) > 0:
        vibrate("alert")
    else:
        pass

    Here's an example of it and the visual alerts in use, letting me know of a severe thunderstorm warning over a part of Montana (after setting the local coordinates for that area).

    Going Further

    With so many vibration effects, I might experiment and add some subtle haptic feedback, especially when using the navigation switch. 

  • Improvements!

    Thornhill!02/13/2021 at 03:34 0 comments

    I've slowly been making improvements over the past few weeks!

    Code

    The code is becoming more refined with every new problem I find (including when my local NWS radar station was down for 5 days for repairs). 

    • The navigation switch is more response, the zoom potentiometer is more zoomy, and things are failing more gracefully than before.

    • There's now example code! For those that would like to see how I'm obtaining and displaying NWS radar images, there's now example code on the project's GitHub page. Currently it doesn't show the button handling or the OpenWeather/NWS forecast stuff, but as soon as I can stop myself from testing out new features, I'll debug everything and add those elements too!

    • In addition to OpenWeather data, I'm now using NWS forecast data to provide a longer, text-based forecast for every forecast period. This is mostly handy for getting nuance from the forecast that OpenWeather doesn't provide (especially snow and ice accumulations!).

      • Depending on the mood of the servers, this can sometimes be temperamental to get. I'm having a lot of fun with Try, Except, and Else statements.

      • The forecast URLs can be easily obtained from the latitude/longitude point file that gets read when the Weather Radar gets the local radar station based on the target latitude and longitude. 
    Examples of the UI for the weather forecast pages.
    Left: Forecast page showing current conditions and conditions for the next few hours at the top. Underneath a short summary for the weather tomorrow. Right: A detailed text-based forecast from NWS that takes up the full screen, with a background colour that's determined by the average temperature for that forecast period (here it's dark blue to indicate a cold night).

    Hardware

    • I'm now using the Omzlo PiWatcher TB to monitor the Raspberry Pi and give me an external on/off button on the rear of the case.

    • Along with the on/off button, I made a cover and put an external micro USB connector on the rear (instead of the hole I had before). This makes everything a lot neater and allows me to easily disconnect the USB cable.

    • I 3D printed a black bezel for the hole for the navigation switch.

    • Inside, there's now a mount for the PCF8591 and the PiWatcher.

    • Because the Weather Radar spends most if it's time on my desk (looking very pretty and spitting out errors I hadn't considered), I've been propping it up to give it a slight angle. Instead of using whatever random thing I had on my desk, I designed and printed a leg to do this for me. 
      Rear of the Weather Radar!
      Rear of the Weather Radar, showing the 3D printed cover that adds an external on/off button and a micro USB connector.
    Looking at the new leg for the Weather Radar.
    The Weather Radar has exactly one leg to stand on and it's this one. Working in the same way as a keyboard leg, this black 3D printed part tilts the Weather Radar at a 20 degree angle, which is perfect for viewing when on my desk. The leg can also fold up to lay flat.

    Also in this picture, the black bezel for the navigation switch!

    Future stuff!

    Mostly a to do list for me:

    • Refine and tidy up more of the code… and release it on Github!
    • Clean up some aspects of the UI. 
    • Incorporate more data! 
      • I've been taking a look at the NWS graphical forecasts and thinking how I can include those images into the Weather Radar too!
      • I'd like to add 511 traffic data for parts of a monthly commute where current road conditions help me to plan which route I'd like to take. Fortunately this looks like it'll be relatively easy to obtain from my state's Department of Transportation!
      • Threading! It would be super handy to download new radar images while also displaying the previous images too. 

View all 2 project logs

  • 1
    Code breakdown: Overview

    I have example code on the Weather Radar github demonstrating how to obtain National Weather Service radar images and merge them with basemaps using the GeoTiler library.

    For a more friendly, step-by-step walkthrough, let's breakdown the code!

    I thought it might be nice to break that code down and go through it step by step!

  • 2
    Libraries, Secrets, Display

    Libraries

    The Weather Radar uses several libraries, which you'll need to download first.

    import time
    from datetime import datetime
    import pytz
    
    import os
    import json
    import math
    import requests
    import logging
    
    from PIL import Image, ImageDraw, ImageFont
    from io import BytesIO
    import xmltodict
    import geotiler
    import numpy as np
    
    #Adafruit & CircuitPython libraries
    import board
    import digitalio
    import adafruit_rgb_display.ili9341 as ili9341
    
    #Secrets! (openweather API & lat long coordinates)
    from secrets import secrets

    Secrets

    You'll also need to populate the secrets.py file:

    • The NWS api requires a header with User-Agent and contact email to be sent with every request, so these need to be filled in.
    • Coordinates (in latitude, longitude format) should be of the point of interest. These are used to get the closest radar station as well as local warnings & alerts.
    • If you know it, add the radar station ID as a fallback in case the coordinates don't work.
      secrets = {
          'header' : {
                      'User-Agent': 'DIY radar viewer',
                      'Contact': 'EMAIL_ADDRESS'
                      },
          'coordinates' : (47.168598,-123.559299), #lat, long
          'station': '' #station ID fallback, e.g. klgx
      }

    Initialise Display

    Here we define some of the pins and settings for the TFT screen, and initialise the display (check out some of the examples on the Adafruit RGB display library).

    CURR_DIR = f"{os.path.dirname(__file__)}/"
    
    cs_pin = digitalio.DigitalInOut(board.CE0)
    dc_pin = digitalio.DigitalInOut(board.D25)
    reset_pin = digitalio.DigitalInOut(board.D24)
    BAUDRATE = 24000000
    spi = board.SPI()
    disp = ili9341.ILI9341(
        spi,
        rotation=270,
        cs=cs_pin,
        dc=dc_pin,
        rst=reset_pin,
        baudrate=BAUDRATE,
    )
  • 3
    Getting Radar Station data

    Now for some functions and settings that we'll run only once during start up.

    Get radar Station ID

    We'll use the location_to_station function to take the lat long coordinates from the secrets file and make a request to api.weather.gov for that particular point. In return we'll get the assigned radar station ID for that location, the location city, state, timezone, and forecast URL

    def location_to_station():
        ''' Get the ID of the nearest radar station from lat long coordinates.
            Also nearest population centre, state, time zone, and forecast URLs
    
            Returns the station code! (must be lowercase)
        '''
        global station
        global forecast_url
        global timeZone
    
        ######################################
        # Try to get the lat long point file #
        ######################################
        point_url = f"https://api.weather.gov/points/{lat_long[0]},{lat_long[1]}"
    
        try:
            response = requests.get(point_url, headers=headers, timeout=5)
        except:
            print("Connection Problems getting Lat/Long point data!")
            response = False
    
        if response:
            point_file = json.load(BytesIO(response.content))
            point_file = point_file['properties']
    
            station = point_file['radarStation'].lower()
            location_city = point_file['relativeLocation']['properties']['city']
            location_state = point_file['relativeLocation']['properties']['state']
            timeZone = point_file['timeZone']
            forecast_url = point_file['forecast']
    
            print(f"{location_city} ({location_state}) -- {station.upper()} -- {timeZone}")
        else:
            print(f"Unable to get location or radar ({response})")
            get_station_data(secrets["station"]) #Use a fallback if it doesn't work!
    
        return station

    Bounding Coordinates

    Now with a station code, we can use that and the radar layer we're interested in (in this case, 'bohp' which is is the 1 hour precipitation layer) to make a request for the WMS GetCapabilities xml document from the NWS (e.g. this one for Jacksonville, FL). This will give us the SW and NE bounding coordinates that we'll use later to make basemaps and download the actual radar images.

    Here's  a page of the OGC compliant layers from the NWS, including alerts, warnings, and layers for the 200+ weather radar stations).

    lat_long = secrets['coordinates']
    headers = secrets['header']
    layer = 'bohp'
    
    location_to_station()
    
    capabilities_url = f'https://opengeo.ncep.noaa.gov:443/geoserver/{station}/{station}_{layer}/wms?SERVICE=WMS&VERSION=1.3.0&REQUEST=GetCapabilities'
    
    #Get the SW and NE coordinates from the WMS GetCapabilities file
    minx, miny, maxx, maxy = get_bounding_coordinates(capabilities_url)
    def get_bounding_coordinates(url):
        ''' Get and return the bounding coordinates of a WMS layer.
            Param url: The url to the GetCapabilities xml
    
            Returns minx, miny, maxx, maxy
    
        '''
    
        ################################
        # Get the GetCapabilities file #
        ################################
        try:
            response = requests.get(url, headers=headers, timeout=5 )
        except:
            print("Connection Problems: Getting Bounding coordinates")
            response = False
    
        ################################
        # Get the bounding coordinates #
        ################################
        if response:
            xml_file = BytesIO(response.content)
            capabilties_dict = xmltodict.parse(xml_file.read()) #Parse the XML into a dictionary
            bounding_coordinates = capabilties_dict["WMS_Capabilities"]["Capability"]["Layer"]["EX_GeographicBoundingBox"]
    
            minx = bounding_coordinates['westBoundLongitude']
            maxx = bounding_coordinates['eastBoundLongitude']
            miny = bounding_coordinates['southBoundLatitude']
            maxy = bounding_coordinates['northBoundLatitude']
            return minx, miny, maxx, maxy
        else:
            print(f"Couldn't get GetCapabilities file ({response})")
            return 0, 0, 0, 0
    

View all 3 instructions

Enjoy this project?

Share

Discussions

Mike wrote 04/27/2022 at 16:58 point

This might actually be one of the coolest projects I've seen. Can't wait to get cracking on this one!

  Are you sure? yes | no

paul.liepertz wrote 11/11/2021 at 22:38 point

Awesome Job, I am keen to replicate this project with my son.

  Are you sure? yes | no

Øystein wrote 02/22/2021 at 10:13 point

Very well done! Thank you for taking the time to write an engaging text.

  Are you sure? yes | no

Thornhill! wrote 02/23/2021 at 23:01 point

Thank you :)

  Are you sure? yes | no

Similar Projects

Does this project spark your interest?

Become a member to follow this project and never miss any updates