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…