METAR Map OLED Add-on

My gift of a METAR map to my flight instructor last year resulted in said map quickly adorning the wall of the flight school hangar, followed by word spreading around the airfield of it being my handiwork, soon followed by an inquiry from another student asking if they could buy one from me. I kind of brushed it off, saying that it wasn’t difficult to build for yourself, here’s the blog post explaining how I did it, etc. Yet people continued asking if they could buy one from me, so I find myself seriously considering producing southeastern Wisconsin METAR maps for fellow flyers. Especially since the transponder in my plane has died (apologies to the KUES tower – it had been working earlier in the year, honest), and what better way to raise funds for replacing the Carter-era radio stack than by unleashing my inner craft fair booth denizen? “Please sir, spare a half shilling for an avionics upgrade?”

I had come across a new project called LiveSectional offering itself as a more turnkey approach to making a METAR map, and what really interested me was the OLED add-on. I downloaded the image as well as the source, but had no luck in getting the software to function after many hours of messing with it. So, what to do, but dive in and roll my own…

I purchased a 128×64 OLED bonnet from Adafruit and dove into testing it out with a RasPi 0 W (as that’s the board I am using for the METAR maps). After going through the learning guide, I started poking at the sample files and was tossed head-first into the world of Python. After a few hours of poking around and hacking up the code with Philip Reuker’s code, I was suitably impressed with Python as a language, especially when realizing “wait, that should not have actually worked” when passing floating point values to a function that drives a bitmapped display. I would have LOVED to have had that sort of simplicity when doing graphics programming a quarter century ago.

My favorite part was just coming up with a simple pointer to indicate wind direction. I initially thought of doing static bitmaps or a lookup table or somesuch, but laziness won out and trigonometry saved the day (I hope my high school teachers are around to bask in my admission of trigonometry being ‘lazy’). This is still a work-in-progress, but it actually works!

#!/usr/bin/env python3

import urllib.request
import xml.etree.ElementTree as ET
import datetime
import math

airportcode = "KOSH"

# That's the configurables, now for the code

print("Running windoled.py at " + datetime.datetime.now().strftime('%d/%m/%Y %H:%M'))

# Retrieve METAR from aviationweather.gov data server
# Details about parameters can be found here: https://www.aviationweather.gov/dataserver/example?datatype=metar
url = "https://www.aviationweather.gov/adds/dataserver_current/httpparam?dataSource=metars&requestType=retrieve&format=xml&hoursBeforeNow=5&mostRecentForEachStation=true&stationString=" + airportcode
print(url)
req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36 Edg/86.0.622.69'})
content = urllib.request.urlopen(req).read()

# Retrieve flying conditions for the airport noted in airportcode
root = ET.fromstring(content)
for metar in root.iter('METAR'):
	stationId = metar.find('station_id').text
	if metar.find('flight_category') is None:
		print("Missing flight condition, skipping.")
		continue
	flightCategory = metar.find('flight_category').text
	windGust = 0
	windSpeed = 0
	windDir = 0
	visibility = 0
	pressure = 0
	lightning = False
	if metar.find('wind_gust_kt') is not None:
		windGust = int(metar.find('wind_gust_kt').text)
	if metar.find('wind_speed_kt') is not None:
		windSpeed = int(metar.find('wind_speed_kt').text)
	if metar.find('wind_dir_degrees') is not None:
		windDir = int(metar.find('wind_dir_degrees').text)
	if metar.find('visibility_statute_mi') is not None:
                visibility = float(metar.find('visibility_statute_mi').text)
	if metar.find('altim_in_hg') is not None:
		pressure = float(metar.find('altim_in_hg').text)
		formatted_pressure = "{:.2f}".format(pressure)
	if metar.find('raw_text') is not None:
		rawText = metar.find('raw_text').text
		lightning = False if rawText.find('LTG') == -1 else True
	print(stationId + ":" + flightCategory + ":" + str(windSpeed) + ":" + str(windGust) + ":" + str(lightning))
print("WindDir:")
print(windDir)
print("WindSpeed:")
print(windSpeed)
print("WindGust:")
print(windGust)
print("visibility:")
print(visibility)
print("flightCategory:")
print(flightCategory)

# now for the display stuff
from board import SCL, SDA
import busio
from PIL import Image, ImageDraw, ImageFont
import adafruit_ssd1306

# Create the I2C interface.
i2c = busio.I2C(SCL, SDA)
# Create the SSD1306 OLED class.
disp = adafruit_ssd1306.SSD1306_I2C(128, 64, i2c)

# Clear display.
disp.fill(0)
disp.show()

# Create blank image for drawing.
# Make sure to create image with mode '1' for 1-bit color.
width = disp.width
height = disp.height
image = Image.new("1", (width, height))

# Get drawing object to draw on image.
draw = ImageDraw.Draw(image)

# Draw a black filled box to clear the image.
draw.rectangle((0, 0, width, height), outline=0, fill=0)

# Load our font of choice
# Pixellari is a great font, but others can be found at http://www.dafont.com/bitmap.php
#font = ImageFont.load_default()
#font = ImageFont.truetype('Minecraftia-Regular.ttf', 8)
font = ImageFont.truetype('/home/pi/Pixellari.ttf', 16)

# Write our textual info on the right side of the display
TextXStart = 66
TextYStart = -1
TextRowSpacing = 13
draw.text((TextXStart, TextYStart + TextRowSpacing * 0), airportcode, font=font, fill=255)
draw.text((TextXStart, TextYStart + TextRowSpacing * 1), str(windDir) + " deg", font=font, fill=255)
if windGust: 
    draw.text((TextXStart, TextYStart + TextRowSpacing * 2), str(windSpeed) + " G " + str(windGust), font=font, fill=255)
else:
    draw.text((TextXStart, TextYStart + TextRowSpacing * 2), str(windSpeed) + " kts", font=font, fill=255)
draw.text((TextXStart, TextYStart + TextRowSpacing * 3), str(visibility) + " sm", font=font, fill=255)
draw.text((TextXStart, TextYStart + TextRowSpacing * 4), str(formatted_pressure) + " in", font=font, fill=255)

# Figure out how to draw the wind pointer
PointerRadius = 32
ArrowTailSize = 25 # Degrees per side for the arrow tail (How chonky to make the pointer - 10=A Fine Boi; 50=OH LAWD HE COMIN)
ArrowTipX = PointerRadius * (math.sin(math.radians(windDir + 180)))
ArrowTipY = PointerRadius * (math.cos(math.radians(windDir + 180)))
ArrowTailX = PointerRadius * 0.5 * (math.sin(math.radians(windDir)))
ArrowTailY = PointerRadius * 0.5 * (math.cos(math.radians(windDir)))
ArrowTailLeftX = PointerRadius * (math.sin(math.radians(windDir + ArrowTailSize)))
ArrowTailLeftY = PointerRadius * (math.cos(math.radians(windDir + ArrowTailSize)))
ArrowTailRightX = PointerRadius * (math.sin(math.radians(windDir - ArrowTailSize)))
ArrowTailRightY = PointerRadius * (math.cos(math.radians(windDir - ArrowTailSize)))

#translate wind pointer to quadrant IV and get absolute values for Y
ArrowTipX += PointerRadius
ArrowTipY = abs(ArrowTipY - PointerRadius)
ArrowTailLeftX += PointerRadius
ArrowTailLeftY = abs(ArrowTailLeftY - PointerRadius)
ArrowTailX += PointerRadius
ArrowTailY = abs(ArrowTailY - PointerRadius)
ArrowTailRightX += PointerRadius
ArrowTailRightY = abs(ArrowTailRightY - PointerRadius)

#draw wind circle
draw.ellipse((0,0,(PointerRadius * 2), PointerRadius * 2 - 1), outline=255, fill=0)

#draw wind pointer if there is wind and fill it if conditions are VFR
if windSpeed:
  if flightCategory == 'VFR':
    draw.polygon(
      [(ArrowTipX, ArrowTipY), (ArrowTailLeftX, ArrowTailLeftY), (ArrowTailX, ArrowTailY), (ArrowTailRightX, ArrowTailRightY)],
      outline=255,
      fill=1,
    )
  else:
    draw.polygon(
      [(ArrowTipX, ArrowTipY), (ArrowTailLeftX, ArrowTailLeftY), (ArrowTailX, ArrowTailY), (ArrowTailRightX, ArrowTailRightY)],
      outline=255,
      fill=0,
    )

# Display image.
disp.image(image)
disp.show()

print()
print("Done")

The arrowhead gets filled if conditions are VFR, and the font can certainly be changed to add more rows of data if desired. Yes, the code is ugly, but…

Naturally, after I wrote all this up, I found that Philip had just added his own OLED output capability to his code… Welp, great minds think alike! Er, at least, that’s what I keep telling myself…

Making a METAR Map

The METAR (METeorological Aerodrome Report) is a staple for pilots wondering what the weather is like at a given airport. While a full report looks like incomprehensible output from a 1960’s teletype (because, well, that’s pretty much exactly what it is), pilots are lazy and sometimes just like having a “yeah/nah” appraisal of weather rather […]

The METAR (METeorological Aerodrome Report) is a staple for pilots wondering what the weather is like at a given airport. While a full report looks like incomprehensible output from a 1960’s teletype (because, well, that’s pretty much exactly what it is), pilots are lazy and sometimes just like having a “yeah/nah” appraisal of weather rather than something like
KOSH 290304Z 10012KT 3SM BR OVC004 02/01 A2990 OMG WTF LOL

Laziness aside, it’s pretty handy to get an idea of flying conditions across a region by looking at a weather map such as the one provided at aviationweather.gov or via various forms of flight planning software. Green, blue, red, and purple indicators give a quick indication of current cloud ceiling/visibility for reporting airports. In the past couple of years, making a live lighted wall map to provide this information has become a popular craft project for flyers, and commercial versions have become available as well.

I’ve been wanting to make a METAR map for my flight instructor ever since first seeing examples online, and finally got around to building one. My primary guide in this was Philip Rueker’s blog post as well as his GitHub project files (though there were several other sites that also provided information and inspiration). Rather than rehash those excellent instructions, this post is regarding my own approach to building.

First off, shadow boxes are much more expensive than regular picture frames. I decided to try to keep the depth of lights and electronics to a minimum so that I could use a standard picture frame. I wound up getting a 20×28 frame from Michael’s for the purpose (and made sure to wait for a good coupon or sale, as $57 is a ridiculous price for a few pieces of MDF and a sheet of glass).

While pretty much every other METAR map build I’ve seen just has through holes for the LEDs, I didn’t want to use this approach for two reasons. First, I wanted to keep the map intact so that it would just be backlit (and thus allowing replacement in the future should there be a big airspace change – sectional maps are only valid for 6 months, after all). Second, I needed to keep the components as low profile as possible. Thus, I decided to forego the WS2811 LED strings used by others and instead opted for the far more compact WS2812B PCB ‘chip’ LEDs.

The picture frame used a fiberboard backing, which I figured would be a fine substrate to mount the LEDs to. I used an outdated sectional map cut just larger than the frame backing, and gave the back of the map a light mist of 3M spray adhesive. After letting the adhesive dry off for about 10 minutes (I didn’t want it to be sticky, just tacky), I carefully applied it to the fiberboard backing, aligning a longitudinal line with a corner of the backing (ensuring that I could remove the map and replace it with a new one).

After trimming the map to the backing, I then zoomed in on the online weather map at aviationweather.gov and marked off every airport on the paper map that showed up online. 38 airports in total would need to be represented. Once I had them all tagged, I used a 1/4″ brad point drill bit and a section of clear acrylic sheet on top of the map/backing to drill a hole cleanly through each airport location.

After that, I marked the backside of the backing board with each airport identifier and name. Then, I removed the old map and made sure each hole was cleared of swarf. I prepared a current sectional chart with spray adhesive, let it tack up, and applied it to the backing board in the same fashion as before, leaving an unscathed map on top (after each application, I used a razor knife to trim the map to the size of the backing board – would have been much more difficult to try getting it sized right beforehand).

Then it was time for the drudgery. Namely, hot-gluing 38 tri-color LED PCBs to the rear of the backing board. And then soldering them all together in a chain. Six solder pads per LED. Honestly, the process wasn’t that bad and I did it over the course of a few evenings. With that completed, I could finally think about powering the whole project. While Philip Rueker chose to run his directly from the RasPi’s GPIO port, he was only dealing with 22 LEDs. With this map being 38 LEDs deep, I opted to have them powered directly from a separate 5V supply. So, how to power the RasPi and LED chain effectively? Well, the RasPi has a micro-USB connector for power. And USB itself runs at 5 volts… A bit of digging through boxes of old hardware yielded a powered USB hub that didn’t need an active host in order for the ports to be powered.

I 3D printed mounting brackets for the RasPi and USB hub. With yet more hot glue to secure them to the backing board, the hardware side was complete. Data and 0V ran from the RasPi GPIO to the LED chain, while 5V and 0V ran from another port on the USB hub to the chain (I didn’t need a diode or level shifter to bring the signal line voltage closer to the LED supply voltage).

The USB hub was a little bit taller than hoped, so I used self-adhesive wall hooks on each corner of the frame to give everything a little more wall clearance (and serve as a way to hold up the power cord).

Much to my shock and amazement, everything seemed to be working great. It only took a little bit of tweaking the metar.py file to complete the project (the LEDs I used have an RGB addressing scheme rather than the WS2811’s GRB order).

Not what I wanted to see weather-wise this weekend, but 100% what I wanted to see project-wise!

Edit: The system wasn’t as stable as hoped, and a strange reboot loop would result within an hour or so of startup. A bit of googling pointed to a feeble power supply as a culprit. I figured a big beefy linear supply wall wart coupled with a substantially warm voltage regulator on the USB hub PCB would equate to an inefficient yet thoroughly reliable voltage source. Yet replacing it with a cheapo dual-port USB wall outlet cube eliminated all issues. Huh, sometimes “they don’t make ’em like they used to” turns out to be a compliment regarding the newcomer!

I also made a few final tweaks to the software side of things. My home airport (where the map will live at) doesn’t have any sort of automated weather reporting system, but I still installed an LED for it. I set rc.local to illuminate that LED in white right away as an indication that the system had booted. After that, I added a “sleep 10” line before running the startup.sh script that initially fires up metar.py (I found that the RasPi wasn’t necessarily yet connected to WiFi, and you might have to wait for cron to kick in before any LEDs would light up as a result of the API lookup failing). Finally, I modified metar.py to show our airport as white (I have it as an entry in the airports file, but since it doesn’t have weather reporting, the LED is normally off). I probably know as much about python in a zoological respect as I do in a programming respect, so it took me a little bit of trial and error to finally figure something out. In the end, inserting a line of “pixels[27] = (85,85,85)” just before the final “pixels.show()” command did the trick.

Jeppesen TechStar Flight Computer Manual

Recently a friend gave me their duffel bag of old student pilot training materials (they kept their log book – hopefully they might pick up training again at some point!) and among the various old books, binders, and brick-a-brac was this gem: It’s an old handheld flight computer, and not the ‘whiz wheel’ E6B circular […]

Recently a friend gave me their duffel bag of old student pilot training materials (they kept their log book – hopefully they might pick up training again at some point!) and among the various old books, binders, and brick-a-brac was this gem:

It’s an old handheld flight computer, and not the ‘whiz wheel’ E6B circular slide rule we’re accustomed to, but an actual electronic flight calculator. I had been pondering getting one for my knowledge test just because there’s a handful of cross-country planning questions that require you to work backwards on the E6B from the standard “set wind direction, mark wind speed, set course, etc.” process, and it’s pretty confusing. Whereas with an electronic flight computer, you key in what you know and the magic silicon pixies do the rest for you.

For a 26-year-old device, it still does handily, but it seems like nobody out on the webernets has a manual for it (searches turn up a load of spam links). Fortunately, I found the manual tucked into a pocket, so I scanned it and am posting it here for others to hopefully find: