Starlink Satellite SMS Notifications with Python, Kubernetes and Twilio

August 21, 2020
Written by
Joyce Lin
Contributor
Opinions expressed by Twilio contributors are their own

Starlink Satellite SMS Notifications with Python, Kubernetes and Twilio

SpaceX is launching thousands of Starlink satellites to assemble a giant interconnected constellation in space. If you look up at just the right time, you might be lucky enough to spot some.

But how can you know ahead of time when a satellite is going to pass overhead?

You don’t have to count on luck to see these tiny silver ants parading across the night sky. This tutorial shows you how to set up a scheduled job to check if a satellite approaches and send an SMS alert.

Tutorial Requirements

Set up the Python virtual environment

Make a new project directory, and change into the directory from the command line.

$ mkdir starlink-alert
$ cd starlink-alert

Create a virtual environment called venv. Activate the virtual environment, and then install the required Python packages inside the virtual environment. If you’re on Unix or Mac operating systems, enter these commands in a terminal.

$ python3 -m venv venv
$ source venv/bin/activate
(venv) $ pip install twilio python-dotenv datetime pytz skyfield 

If you’re on a Windows machine, enter these commands in a command prompt window.

$ python -m venv venv
$ venv\Scripts\activate
(venv) $ pip install twilio python-dotenv datetime pytz skyfield 

The Python packages we’re using are:

Write the Python script

The purpose of the script is to check if a visible satellite is approaching a given city location. If so, your Twilio phone number sends an SMS alert to your personal phone number.

Copy the following code into a file called tracker.py:

import os
import math
from dotenv import load_dotenv
load_dotenv()
from skyfield.api import Topos, load
from datetime import timedelta
from pytz import timezone
from twilio.rest import Client

# load the satellite dataset from Celestrak
starlink_url = 'https://celestrak.com/NORAD/elements/starlink.txt'
starlinks = load.tle_file(starlink_url)
print ('Loaded', len(starlinks), 'satellites')

# update city location and timezone
location = Topos('37.7749 N', '122.4194 W')
tz = timezone('US/Pacific')

# establish time window of opportunity
ts = load.timescale()
t0 = ts.now()
t1 = ts.from_datetime(t0.utc_datetime()+ timedelta(hours=2))

# loop through satellites to find next sighting
first_sighting = {}
for satellite in starlinks:

   # filter out farthest satellites and NaN elevation
   elevation = satellite.at(t0).subpoint().elevation.km
   isNan = math.isnan(elevation)
   if elevation > 400 or isNan: continue
   print ('considering: {} at {}km'.format(
       satellite.name,
       round(elevation)
   ))

   # find and loop through rise / set events
   t, events = satellite.find_events(location, t0, t1, altitude_degrees=30.0)
   for ti, event in zip(t, events):
      
       # check if satellite visible to a ground observer
       eph = load('de421.bsp')
       sunlit = satellite.at(t1).is_sunlit(eph)
       if not sunlit: continue

       # filter by moment of greatest altitude - culminate
       name = ('rise above 30°', 'culminate', 'set below 30°')[event]
       if (name != 'culminate'): continue
          
       # find earliest time for next sighting
       if (not first_sighting) or (ti.utc < first_sighting['time']):
           first_sighting['time_object'] = ti
           first_sighting['time'] = ti.utc
           first_sighting['satellite'] = satellite

if (first_sighting): 

   # create body for SMS  
   next_sighting = ('next sighting: {} {}'.format(
       first_sighting['satellite'].name,
       first_sighting['time_object'].astimezone(tz).strftime('%Y-%m-%d %H:%M')
   ))

   # send SMS via Twilio if upcoming sighting
   account_sid = os.environ.get('TWILIO_ACCOUNT_SID')
   auth_token = os.environ.get('TWILIO_AUTH_TOKEN')
   client = Client(account_sid, auth_token)

   message = client.messages.create(
       body=next_sighting,
       from_=os.environ.get('TWILIO_PHONE_NUMBER'),
       to=os.environ.get('MY_PHONE_NUMBER')
   )

   print ('Message sent:', message.sid, next_sighting)

else:

   print ('No upcoming sightings')

Update rows 16 to 17 with your own geographic coordinates and timezone. You can find your latitude and longitude here. See all the timezones by running the following code in a Python shell. Once you find the timezone that applies to your location, exit the Python shell.

(venv) $ python
>>> import pytz
>>> pytz.all_timezones
>>> exit()

The tracker.py script relies on a Python package called Skyfield to handle some of the complex space computations. The script loads the satellite dataset, looping through the satellites to find the next good sighting. Skyfield figures out when a satellite rises and sets near you and sees if the satellite is sunlit for maximum visibility. If there are any sightings predicted within the next two hours, Twilio sends the details with an SMS to your phone number.

To enable the Twilio SMS, store your credentials and other configuration information in a file called .env (notice the dot in front of this filename).

TWILIO_ACCOUNT_SID=<your-account-sid>
TWILIO_AUTH_TOKEN=<your-auth-token>
TWILIO_PHONE_NUMBER=<your-twilio-phone-number>
MY_PHONE_NUMBER=<your-personal-phone-number>

To find your Twilio Account SID and Auth Token, log into the Twilio Console, then click on "Settings" in the sidebar and scroll down to "API Credentials". You will also need your Twilio phone number to send the SMS.

Save your updates to the .env file, and then run the script from the command line. If you're running the script during the day, you might determine a satellite is passing overhead, but not be able to see it very well in the daylight. Once you set up a CronJob, you can specify when to run the script for maximum satellite visibility, like at dusk or dawn.

(venv) $ python tracker.py

After you try the script and confirm that it is working, create a requirements file to store the dependencies, so that we can install them in the next step.

(venv) $ pip freeze > requirements.txt

Now, let’s create a Docker image to run the script.

Create a Docker image and push to Docker Hub

Make sure Docker is installed and running on your machine. Add the following code in a file called Dockerfile (notice there’s no file extension) that describes how to build our image.

# creates a layer from the python:3 Docker image
FROM python:3

# copy and install dependencies
COPY requirements.txt /
RUN pip install -r requirements.txt

# add script
COPY tracker.py /

# define the command to run the script
CMD [ "python", "./tracker.py" ]

Next we are going to build and run a Docker image for the Python script from the terminal. In the following commands, the image we are building is called python-starlink and the container is called starlink_test. Since we don’t want to include our environment variables in the container for security reasons, we run the container using the --env-file flag in the command line. If you’re on a Linux operating system, precede these docker commands with sudo or create a docker group.

$ docker build --tag python-starlink .
$ docker run --name starlink_test --env-file=.env --rm python-starlink 

Once you verify the container runs successfully, share the Docker image on the Docker Hub registry so it can be accessed and run from the cloud. Log into your Docker Hub account from the command line with docker login. Then create a repository and push your image to Docker Hub using the following commands:

$ docker tag python-starlink <Your Docker ID>/python-starlink:latest
$ docker push <Your Docker ID>/python-starlink:latest

In the above commands, Your Docker ID is the username you have registered with Docker Hub.

In the next step, let’s schedule a Kubernetes CronJob to run the Python script.

Deploy a CronJob on Kubernetes

Make sure you are logged into KubeSail. KubeSail has public YAML examples. I made a CronJob template called satellites that has two kinds of Kubernetes resources:

  • Secrets to securely store environment variables
  • CronJob to complete a task on a schedule

Go to the satellites template. Under “Required Variables”, replace the default DOCKER-IMAGE with the base image you just pushed to Docker Hub. Then input the remaining environment variables.

Input required variables and inspect the underlying YAML for resources

If you want to inspect the YAML, click “Edit Yaml” to expand the code editor and review the underlying configuration for each resource. This YAML describes the resource CronJob.

apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: starlink
  labels:
    app: starlink
spec:
  schedule: "0/30 19-21 * * *"
  jobTemplate:
    spec:
      template:
        spec:
           restartPolicy: Never
          containers:
            - name: starlink-tracker
              image: "{{DOCKER-IMAGE|joycelin79/python-starlink:latest}}"
              envFrom:
                - secretRef:
                    name: starlink-secret
              imagePullPolicy: Always
  successfulJobsHistoryLimit: 3
  failedJobsHistoryLimit: 1

This YAML describes the resource Secret to store our environment variables.

apiVersion: v1
kind: Secret
metadata:
  name: starlink-secret
  labels:
    app: starlink
stringData:
  TWILIO_ACCOUNT_SID: "{{TWILIO-ACCOUNT-SID}}"
  TWILIO_AUTH_TOKEN: "{{TWILIO-ACCOUNT-SID}}"
  TWILIO_PHONE_NUMBER: "{{TWILIO-PHONE-NUMBER}}"
  MY_PHONE_NUMBER: "{{MY-PHONE-NUMBER}}"

When you’re ready, hit “Launch Template” to deploy a cluster to your Kubernetes namespace on KubeSail.

In the sidebar under “Resources”, you can see all the resources running in this Kubernetes context including the scheduled jobs.

Resources running in this Kubernetes context

This is also where you can update the deployment. For example, update your environment variables in the Secret resource. To trigger a job every minute while you’re testing, update the YAML configuration for the CronJob resource and set the schedule to "* * * * *"(surrounded in double quotes) and Apply your changes.

To edit the underlying Python script, save the changes locally in tracker.py. Then build and push the updated image to Docker Hub like we did before. The Kubernetes CronJob in KubeSail automatically pulls the latest version in Docker Hub because we specified imagePullPolicy: Always in our container configuration.

And that’s it!

Conclusion

Creating an SMS notification is pretty straightforward. There’s a bunch of ways to do this - and now you know how to schedule a CronJob on Kubernetes.

For further exploration, you can:

  • Adjust the criteria: Each batch of Starlink satellites spreads out over time traveling to a higher elevation. If you want to increase the sensitivity of your tracker to see more satellites, adjust the elevation in your script. Or update the cron schedule to run when satellites are most visible from the ground - at dusk and dawn.
  • Change the deployment: Set up a different Kubernetes context in KubeSail to deploy the CronJob to a cluster running locally or on another cloud provider. Or experiment with other CronJob options in Kubernetes.
  • Track something else: If satellites aren’t your thing, swap it out for a new dataset of celestial bodies.

Let me know if you spot something out of this world!

Joyce likes coding, cats, and second lunches. Find her on Twitter and LinkedIn.