RGB LED Sectional Chart Weather

I worked on this project for my brother. It was inspired by something he found online. The idea is to mount a paper aviation sectional chart (it’s like a special map) on the wall and place RGB LEDs at the locations of airports you care about on the chart. Every so often, a Pi checks the weather conditions at those airports and changes the colors of the LEDs, allowing you to see the weather conditions at the various airports in your area at a glance, right there on the wall.

I designed a custom circuit board for interfacing with a NeoPixel light strand I got from Amazon. The circuit uses a level-shifted chip to adjust the Pi’s GPIO output 3v3 voltage to the 5v level expected by the NeoPixels. The circuit also incorporates a barrel-plug adapter and a 5x20mm fuse holder. It’s pictured below and can be viewed on EasyEDA here: https://easyeda.com/ditchwater/sectional-chart-board

I wrote some Python code that will hit the US’s aviation weather web service to get the weather data and update the lights. I’ll post the code here. I made a last-minute untested change to it, but it should be pretty close to working. The code is permissively licensed (https://opensource.org/licenses/MIT). It is based on the CircuitPython library here: https://github.com/adafruit/Adafruit_CircuitPython_NeoPixel.

Note: Make sure you pass the station identifiers to the web service in all capitals. Otherwise, you may get a confusing error saying station_id is not a valid field.

# Copyright 2019 Kyle Hansen
# 
# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
# 
# The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
# 
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

import json
import urllib.request
import urllib.parse
from neopixel import *
import xml.etree.ElementTree as etree
from ledgfx import gfx

PURPLE = (155,0,155)
RED    = (15,0,0)
BLUE   = (0,0,155)
GREEN  = (0,155,0)

# LED strip configuration:
LED_COUNT      = 18      # Number of LED pixels.
LED_PIN        = 18      # GPIO pin connected to the pixels (18 uses PWM!).
#LED_PIN        = 10      # GPIO pin connected to the pixels (10 uses SPI /dev/spidev0.0).
LED_FREQ_HZ    = 800000  # LED signal frequency in hertz (usually 800khz)
LED_DMA        = 5      # DMA channel to use for generating signal (try 10)
LED_BRIGHTNESS = 255     # Set to 0 for darkest and 255 for brightest
LED_INVERT     = False   # True to invert the signal (when using NPN transistor level shift)
LED_CHANNEL    = 0       # set to '1' for GPIOs 13, 19, 41, 45 or 53


def get_color(ceiling, visibility):

    try:
        visibility = float(visibility)
    except Error as e:
        print('ERROR could not parse visibility: %s, defaulting to 10 SM' % visibility)
        visibility = 10.0
        
    if visibility < 1:
        return PURPLE
    elif visibility >= 1 and visibility < 3:
        return RED
    elif visibility >= 3 and visibility < 5:
        return BLUE

    # at this point visibility must be OK, so check ceiling
    if ceiling is None:
        return GREEN
        
    try:
        ceiling = int(ceiling)
    except:
        print('ERROR could not parse ceiling: %s, defaulting to 3000' % ceiling)
        ceiling = 3000
        
    if ceiling < 500:
        return PURPLE
    elif ceiling >= 500 and ceiling < 1000:
        return RED
    elif ceiling >= 1000 and ceiling < 3000:
        return BLUE
    else:
        return GREEN

with open('airports.json') as f:
  airports_json = f.read().upper()
airports = json.loads(airports_json)

request_string = 'https://aviationweather.gov/adds/dataserver_current/httpparam?dataSource=metars&requestType=retrieve&format=xml&stationString=%s&hoursBeforeNow=2&mostRecentForEachStation=constraint&fields=station_id,sky_cover,cloud_base_ft_agl,visibility_statute_mi' % '%20'.join([airport for airport, led_index in airports.items()])
print(request_string)
 
f = urllib.request.urlopen(request_string)
response = f.read().decode('utf-8')
print(response)
root = etree.fromstring(response)

# Create NeoPixel object with appropriate configuration.
strip = Adafruit_NeoPixel(LED_COUNT, LED_PIN, LED_FREQ_HZ, LED_DMA, LED_INVERT, LED_BRIGHTNESS, LED_CHANNEL)
# Intialize the library (must be called once before other functions).
strip.begin()
for i in range(LED_COUNT):
    strip.setPixelColorRGB(i, 0, 0, 0)

for metar in root.iter('METAR'):
    station_id = metar.find('station_id').text.upper()
    print(station_id)
    ceiling = metar.find('sky_condition').get('cloud_base_ft_agl')
    visibility = metar.find('visibility_statute_mi').text
    print('Visibility, sm : %s' % visibility)
    print('Ceiling, ft agl: %s' % ceiling)
    print('Setting light %s to %s' % (airports[station_id], get_color(ceiling, visibility)))
    color = get_color(ceiling, visibility)
    strip.setPixelColorRGB(airports[station_id], color[0], color[1], color[2])

strip.show()



What a fascinating modern age we live in

I have entered the modern era with a 3D printer. In particular, it’s an “Original Prusa i3 MK3,” kit version. Assembling the kit was not complicated or confusing, but it was a serious test of my limited dexterity and took about 20 hours of focused work.

Nevertheless, the printer is assembled and working, and in case it ever helps anyone, problems with X-axis length errors and XYZ calibration can possibly be resolved by loosening the screws holding the back-plate on the extruder assembly. That plate puts pressure on the X-axis bearings and can cause your extruder to not slide as far as it should on the X axis. If your Auto Home check is correct in Y but not X, maybe look into this.

Below are some pictures of my first couple results and some videos showing the printer in action.

What’s goin’ on?

I haven’t posted in a while, so here’s a brief update of what I’m doing.

  • Space game: I’m prototyping destructible terrain for this. A brainstorming session with my creative director yielded a slightly new direction for the game that involves mountains
  • Motor: I have smaller-gauge wire on order to hopefully wind coils that eliminate the need for the big resistors and make better use of supplied power. These were meant to arrive yesterday, but a huge snowstorm interfered with mail deliveries.

I hope to have some destructible terrain and/or new coil progress to show soon.

Parabolic motion

I worked on the motion code for the Sky Commando tonight. Originally I calculated his starting velocity and acceleration from the usual equation for a parabola, with the origin located at the player’s crosshair and a starting reference point at x = -crosshairLocation.X. The idea was to get the Commando to swoop in from the side of the screen and cross right through the middle of the crosshair.

Instead, the commando kept swooping in just above it. I decided I should scale the motion updates by the amount of time elapsed since the previous update, but the framerate is pretty much dead on at 60fps, so that made no real difference other than to affect the speed of all the objects on-screen.

After playing with it for a while, I realized the Commando was reaching the x-coordinate of the crosshair before the y-coordinate. I had his X velocity locked at ‘2’, and changing it to ‘1’ made everything to work. I should have realized this sooner–that the way I was computing his y-velocity and acceleration was assuming a unit x-velocity (and zero x-acceleration).

However, this still wasn’t quite what I wanted. I could scale the velocity and acceleration correctly to still achieve the crosshair interception, but if the crosshair was at the left side of the screen, the commando swooped in much too quickly (because of the steep derivative of such a narrow parabola). So I changed things around to compute the angle of the parabola’s derivative at the Commando’s starting point, then scale that to a unit vector and apply a constant “commando speed” to that unit vector to achieve constant total speed in whatever direction the Commando flew.

But there was one more problem: now the acceleration was again off. It assumed either a unit X-velocity or one scaled by a constant–not one that depends on an arbitrary launch angle.

The solution to this problem is really satisfying: because there’s zero acceleration in the x direction (because we neglect air resistance), the Commando will always reach the x-coordinate of the crosshair in (crosshairLocation / x-velocity) time units. That means if we want him to also correctly close the y-distance (which he’d have to in order to fly through the middle of the crosshair and not above or below it), his y-acceleration should be whatever value causes his y-velocity to reach 0 in (crosshairLocation / x-velocity) time steps.  This isn’t totally intuitive, but if we examine the parameterized equation for velocity (in just the y dimension):

v = v0 + at

and then solve it for a:

a = (v – v0) / t

we see we can compute any acceleration we want, given a starting velocity v0, a target velocity v, and a time quantity t. Well, v0 is just the y-velocity computed at the start of the Commando’s trajectory (which happens to be commandoSpeed * sin(launch angle)), and t is (crosshairLocation / x-velocity). So what is the target velocity v? Since we want the Commando to reach the crosshair right at the bottom of the parabola, the target velocity v (which, remember, is the y-velocity) is 0. So we put these values into the equation and compute our acceleration a, and that acceleration causes the Commando’s y-velocity to decrease to 0 at time t, which is also when he reaches the x-coordinate of the crosshair.  If his y-velocity is 0, he must be at the bottom of the parabola, which also happens to be the location of the crosshair as defined in the first paragraph.

Once the Commando reaches the crosshair, the code clamps his y-velocity to 0 so he can just cruise at a constant altitude. I’ll probably ultimately have him deploy a parachute or something.

Calculation for intercepting a moving target (leading shots)

I worked on the space game more tonight. In particular, I made the logic that guides an alien toward a citizen a bit more sophisticated by implementing moving target interception. Without this, if the alien just moves along the A to B path to the citizen, the alien ends up lagging behind the citizen because the citizen is in motion.

I described the derivation of this calculation in LaTeX a while back for a friend who was curious about it. I’ll attach the PDF here for anyone who’s interested.

Moving Target Interception Derivation PDF

Otherwise, the aliens now pick a target, descend to it, and freeze it in place to abduct it. I need to create an abduction animation to use. Maybe I’ll do that next time.

A soft interlude

At the start of the winter, I set a goal for myself to create a simple PC game I could play in the living room on my Steam Link with my son. Winter wears on, and I’ve been so focused on the motor and other hardware projects, I really haven’t done much with this goal.

Today I set aside the motor project (which is waiting on the 4mm parts from England) and worked a bunch on the game.

The game will be like Space Invaders or Galaga, but you shoot at the invading aliens from stationary positions, like Missile Command. The aliens’ goal is to descend and abduct citizens, who run in hapless panic back and forth across the bottom of the screen. Ideally you kill the aliens before they abduct anyone, but if someone does get picked up, you can safely rescue them by deploying a Sky Commando. Sky Commandos swoop in with wingsuits from the nearest side of the screen, and if you aim their arc correctly, they’ll grab onto the alien ship and attack it from within, destroying it and rescuing any abductees. Sky Commandos are a limited but reusable resource, which just means you can only have a finite number of them in play at a time.

The game is meant to be played with a gamepad since I intend to play it in the living room. I’ll release it for free if it shapes up into something worth releasing.

Today’s progress was implementing the alien AI that allows them to identify and move in on a potential abductee. Otherwise, I have the game engine in place (using the entity/component/system, or ECS, model) with an observer pattern to inform systems about entities they should care about or stop caring about. I’m implementing it all in C# using MonoGame. Currently the player can shoot at aliens with a rapid-fire weapon, and as I said, aliens will now pick a citizen to abduct and descend to get them.