Mathias Gibbens
Life's Not Fair -- Being Thankful in the Time of COVID-19

That's not fair!

Last night the huge $2.2 trillion COVID-19 relief bill was finally signed into law. As has been extensively covered the past week, part of this bill includes direct cash payments to most Americans. Free money, cool! But, as details came out, I realized that I won't be receiving any of that cool cash, and my first reaction was "that's not fair!"

After a little bit of thought and reflection, my second reaction was "that's not fair, but I have no standing to complain." As countless parents have told their children, "life isn't fair", and that's not always a bad thing. I am doing well, and as the United States is really starting to be impacted by COVID-19, I am thankful to be in a very secure position:

  • I am still receiving my normal, regular paycheck from $DAYJOB. I have job security, work that I can be performing remotely, and am not in danger of being laid off or furloughed. If worst comes to worst, I've got tons of sick time and vacation I can charge. Unlike so many others, I am not suddenly looking at a lack of income.
  • Additionally, I have plenty of money in savings, and could comfortably live for a couple of months with no additional income. This is exactly what emergency funds are for, and provides as a huge source of mental comfort. For so many Americans even being able to have one month's worth of expenses saved up is such a huge challenge, and I am well beyond that.
  • I'm generally healthy, and not in any of the higher risk categories. I've also seen a few tables of mortality rates by age group and it's practically zero for me. (I can't find a reliable source to cite, so I'm not linking to any of those tables here.) That being said, I am staying home as much as possible, as I don't want to accidentally catch the virus and then pass it on to others at higher risk without realizing it.
  • With technology today, it's easier than ever to stay in contact with people while remaining home. And, working from home is allowing me to check in on several of the local amateur radio nets each day, which is kind of fun to be able to do. If worst comes to worst, I'll still be able to communicate with the wider world with my ham radio license, using nothing more than solar and battery power.

While it would be nice to get $1,200, the amount of critical aid it would provide me is significantly less than it might give to someone who no longer has a paycheck coming or doesn't have an emergency savings fund for whatever reason. If we have to choose who to help, it should be those who can most benefit by the assistance. We can't just look at the absolute value of money when making decisions like this, but need to consider how much impact it will make on those who receive the money. (This is also why I'm in favor of higher taxes for the very rich, as the impact of higher taxes will have significantly less impact on them than on those who are at the other extreme.)

So, life isn't always fair, and that's not always a bad thing. If you're better off than your neighbor, especially in this time of crisis, give them some help. Keep calm, stay safe, and stay home! Lastly, because this is the Internet, here is the requisite cat picture of Beruthiel looking cute:

Beruthiel looking cute

Posted
Mathias Gibbens
Video of the Week: The Play that Goes Wrong

There's an entire collection of productions that this group has put on, all quite funny... you can find them on various sites online.

Posted
Mathias Gibbens
Tracking the International Space Station

My project this past weekend was utilizing an old RaspberryPi to build a simple tracker for the International Space Station that shows information about upcoming and current passes, as well as lighting up some LEDs whenever the ISS is above the horizon. I think it turned out pretty well, and was a fun excuse to play with some of the GPIO capabilities of the RaspberryPi.

As I previously posted, LEGO has recently released a model of the ISS, which arrived at my place the end of last week. Over all it's a nice display model and was a nice couple hours of building fun. While working on it, I thought it would be fun to utilize an old, original RaspberryPi 1 Model B to construct a tracker display so I could easily see when the ISS is above the horizon (but not necessarily visible in the sky).

First, I took a breadboard and setup a row of LEDs (with resisters!) that I could turn on and off via one of the GPIO pins on the RaspberryPi. There's lots of easy examples of turning LEDs on and off online, and the RPi.GPIO python module makes programmatic control easy.

Next, for a display, I found a SSD1306-based OLED on Amazon for seven bucks. With a resolution of 128x64 pixels, it's got 16 rows of yellow on top, and 48 rows of blue underneath which provides a nice visual separation when displaying information. Adafruit has a nice python library that makes interfacing with the display really easy. The refresh rate isn't more than a few hertz, but that's plenty fast for displaying updates on the position of the ISS.

Close up showing information about an occurring ISS pass
ISS pass display

Lastly, for computing when the ISS would be above the horizon, there's the skyfield python library which "computes positions for the stars, planets, and satellites in orbit around the Earth" that I used for the heavy lifting of doing the math. There are very nice examples of use, which was enough to get me started on my own code to figure out when ISS passes would occur, and as they do to compute additional statistics to display. Installing on a RaspberryPi was just a simple pip install away, although I did have to manually install some underlying libraries that numpy linked to via apt.

And, here's the end result:

LEGO ISS model with tracker and illuminated LEDs
ISS model and tracker
LEGO ISS model in the dark with illuminated LEDs
ISS model and tracker (lights off)

For those who are interested, here's the python code I wrote to power my RaspberryPi:

#!/usr/bin/env python3

### Written by Mathias Gibbens and licensed under the GNU GPLv3.

import adafruit_ssd1306
import busio
import datetime
import time
from board import SCL, SDA
from PIL import Image, ImageDraw, ImageFont
import RPi.GPIO as GPIO
from skyfield import units
from skyfield.api import Topos, load

class Track:
    ### Upstream https://github.com/skyfielders/python-skyfield/pull/281 may implement a lot of this logic

    def __init__(self):
        # stations.txt downloaded from http://celestrak.com/NORAD/elements/stations.txt
        self.satellites = load.tle("stations.txt")
        self.iss = self.satellites["ISS (ZARYA)"]
        self.qth = Topos("35.120398 N", "106.495599 W")
        self.timescale = load.timescale(builtin=True)

    def getPosition(self, time=None):
        """
        @param time a timescale object

        Given a time, return the alt, az, and distance to the ISS
        """
        if time is None:
            time = self.timescale.now()
        diff = self.iss - self.qth
        topocentric = diff.at(time)

        return topocentric.altaz()

    def _resolvePass(self, time):
        """
        @param time a timescale object

        Given a time when the ISS is above the horizon, find the start and end of that pass

        Returns a tuple of the time when next pass begins, its duration, and max altitude
        """

        start = None
        end = None
        maxAlt = -1

        # Find the start
        for i in range(0, -20*60, -1): # Go back up to 20 minutes
            alt, _, _ = self.getPosition(self.timescale.utc(time.year, time.month, time.day, time.hour, time.minute, i))
            if alt.degrees > 0:
                start = datetime.datetime(time.year, time.month, time.day, time.hour, time.minute)
                start += datetime.timedelta(seconds=i)
                if alt.degrees > maxAlt:
                    maxAlt = alt.degrees
            else:
                break
        print("  starts at " + str(start))

        # Find the end
        for i in range(0, 20*60): # Go forward up to 20 minutes
            alt, _, _ = self.getPosition(self.timescale.utc(time.year, time.month, time.day, time.hour, time.minute, i))
            if alt.degrees > 0:
                end = datetime.datetime(time.year, time.month, time.day, time.hour, time.minute)
                end += datetime.timedelta(seconds=i)
                if alt.degrees > maxAlt:
                    maxAlt = alt.degrees
            else:
                break
        print("  ends at " + str(end))

        return (start, str(end - start), str(units.Angle(degrees=maxAlt)))

    def _findNextPass(self, secondsStep, maxSeconds=7*24*60*60):
        """
        @param secondsStep the number of seconds between steps. This controls how coarse of a search to perform.
        @param maxSeconds defaults to one week's worth of seconds beform terminating the search.

        Returns a timescale that occurs during the next pass
        """

        now = self.timescale.now().utc_datetime()
        secondsCounter = 60 # Ensure we don't pick up on the currently ending pass

        while secondsCounter <= maxSeconds:
            alt, _, _ = self.getPosition(self.timescale.utc(now.year, now.month, now.day, now.hour, now.minute, secondsCounter))
            if alt.degrees > 0:
                return self.timescale.utc(now.year, now.month, now.day, now.hour, now.minute, secondsCounter)

            secondsCounter += secondsStep

        return None

    def getNextPass(self):
        """
        Returns a tuple of the time when next pass begins, its duration, and max altitude
        """

        print("Computing next pass...")

        nextPass = None
        # There's a tradeoff here in terms of how quickly we find the next pass versus a chance of skipping a pass that's shorter than the current granularity and possibly selecting a later pass as we contine to search
        for granularity in (300, 145, 60):
            nextPass = self._findNextPass(granularity)
            if nextPass is not None:
                break

        print("Next pass will be occuring during " + str(nextPass.utc_datetime()))

        return self._resolvePass(nextPass.utc_datetime())

class LED:
    def __init__(self):
        GPIO.setmode(GPIO.BCM)
        GPIO.setwarnings(False)
        GPIO.setup(18, GPIO.OUT)

    def on(self):
        GPIO.output(18, GPIO.HIGH)

    def off(self):
        GPIO.output(18, GPIO.LOW)

class Display:
    def __init__(self):
        self.i2c = busio.I2C(SCL, SDA)
        self.display = adafruit_ssd1306.SSD1306_I2C(128, 64, self.i2c)

        self.font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf", 8)

        self.image = Image.new("1", (self.display.width, self.display.height))
        self.draw = ImageDraw.Draw(self.image)

        self.display.fill(0)
        self.display.show()

    def clear(self):
        self.draw.rectangle((0, 0, self.display.width, self.display.height), outline=0, fill=0)

    def show(self):
        self.display.image(self.image)
        self.display.show()

    def drawText(self, row, text):
        self.draw.text((0, row*8), text, font=self.font, fill=255)

d = Display()
led = LED()
iss = Track()

d.drawText(2, "Computing next pass...")
d.show()
led.off()

_nextPass, _nextDuration, _nextMaxAlt = iss.getNextPass()
_computeNextPass = False

while True:
    d.clear()

    alt, az, distance = iss.getPosition()

    if alt.degrees > 0:
        _computeNextPass = True
        led.on()

        d.drawText(0, "ISS is above the horizon")
        d.drawText(1, "Pass duration: " + _nextDuration)
        d.drawText(2, "Altitude: " + str(alt))
        d.drawText(3, "Azimuth:  " + str(az))
        d.drawText(4, "Distance: " + str(int(distance.km)) + " km")
        d.drawText(7, "      " + datetime.datetime.now().isoformat(timespec="seconds"))

        d.show()

        time.sleep(0.25)
    else:
        if _computeNextPass:
            d.drawText(2, "Computing next pass...")
            d.show()
            led.off()
            _nextPass, _nextDuration, _nextMaxAlt = iss.getNextPass()
            _computeNextPass = False
            d.clear()

        d.drawText(0, "Next ISS pass in " + str(_nextPass - datetime.datetime.now()).split(".")[0])
        d.drawText(1, "Pass duration: " + _nextDuration)
        d.drawText(2, "Max alt: " + _nextMaxAlt)
        d.drawText(7, "      " + datetime.datetime.now().isoformat(timespec="seconds"))

        d.show()

        time.sleep(0.95) # Just a little less than a second due to compute delays
Mathias Gibbens
Video of the Week: The Talos Principle

The Talos Principle cover If you enjoyed Portal, you'll probably like The Talos Principle as well. It's similar being a first-person puzzle game, but it's also very different, delving into a decent amount of philosophy. If you prefer just solving puzzles, you can do so; there's also a pretty decent story plot you can discover as the game progresses. You can easily follow a linear play through, while solving extra (sometimes quite hard!) puzzles opens up other plot lines. Plus, there's a ton of easter eggs throughout.

I've really enjoyed this game, as it's got very good puzzles, some easy and some challenging, all built with fairly simple building blocks. As the title implies, there's a lot of philosophy that's presented as you play, and it brings up some fairly interesting questions.

The game's available on Steam with an additional DLC that's pretty good as well. If you don't want to spring for the full price, it's occasionally on sale as well.

Posted