A favorite hobby among aviation enthusiasts is tracking and monitoring aircraft in the skies above them.
Identifying incoming aircraft, following their traffic patterns, and gauging how many are in their vicinity are all interesting activities for those who are curious about what is flying overhead.
The online platform OpenSky Network has greatly simplified this pastime by providing real-time location information for thousands of aircraft worldwide, made available through dashboards and APIs.
This project demonstrates how to build a visualization of this live flight data using an IoT display. The display presents a dynamically updating map that charts up-to-the-minute positions of aircraft flying within a user-defined area.
How It WorksAn Adafruit PyPortal board serves as the central component of this project. The board is an IoT device equipped with a 3.2" color display, a Python-compatible microcontroller, and built-in WiFi connectivity.
Once the project code, libraries, and image assets are uploaded onto the PyPortal, the board automatically executes the project's main Python script as soon as it is powered on.
The script begins by first displaying an OpenSky splash image which is shown while initialization and data-gathering steps are completed.
After connecting to WiFi, the script constructs a Geoapify URL for the map image which will serve as the base layer of the visualization.
The bounds for this map are calculated based on a user-defined latitude/longitude center coordinate and extend to include a 20-kilometer radius around that point.
Next, the script utilizes the Adafruit IO image conversion service to convert the map image into BMP format, a requirement for display on the PyPortal.
When the conversion is complete, the script downloads the converted image to the local storage of the board and sets it as the display background.
Finally, the script enters a loop where on each iteration it sends a request to the OpenSky Network API to retrieve flight data within the map bounds, and plots any received aircraft positions over the map image.
This loop is executed every thirty seconds, repeatedly gathering and displaying data, and will run continuously as long as the board is powered on.
The PyPortal is mounted inside a desktop stand for easy viewing. Users can watch and track aircraft live as they traverse across the map, or view it anytime for an instant overview of the area's current aerial activity.
PyPortal SetupBefore proceeding with the project build steps below, users should familiarize themselves with the PyPortal and complete the initial setup outlined in Adafruit's learning guide.
This setup includes installing the latest version of CircuitPython on the board and downloading the Adafruit and Community Library Bundles.
When building this project, it is highly recommended to utilize the Mu code editor. Mu offers access to the PyPortal serial console to conveniently view print messages and errors generated by the code.
Complete project code, libraries, and image assets can be found on GitHub.
The following project instructions assume starting with an empty PyPortal filesystem, with no files present in the connected board's CIRCUITPY
drive.
To access online resources, the project code relies on user-specific configuration values, such as WiFi network settings and private API keys.
These values are stored in a secrets.py
file to separate them from code that may be shared online. This file should be ignored by version control systems to prevent unintentionally exposing these values to others.
1. Create a new file named secrets.py
in the CIRCUITPY
drive.
2. Add the following code to the secrets.py
file:
secrets = {
'ssid' : 'YOUR_SSID',
'password' : 'YOUR_PASSWORD',
'geoapify_key': 'YOUR_GEOAPIFY_KEY',
'aio_username' : 'YOUR_AIO_USERNAME',
'aio_key' : 'YOUR_AIO_KEY',
'opensky_username': 'YOUR_OPENSKY_USERNAME',
'opensky_password': 'YOUR_OPENSKY_PASSWORD'
}
This code defines a Python dictionary containing the required configuration values for this project, which will be later imported by the project's main script.
The dictionary's placeholder values will be updated with user-specific values in upcoming build steps.
Python LibrariesFor the project code to perform complex actions like interacting with board components or making online requests, it requires access to additional code provided by several Adafruit and Community CircuitPython libraries.
1. Create a new directory named lib
in the CIRCUITPY
drive.
2. Download and unzip the Adafruit CircuitPython Bundle that corresponds to the version of CircuitPython installed on the PyPortal.
3. From the bundle's lib
directory, copy the following libraries to the lib
directory on the PyPortal:
- adafruit_binascii.mpy
- adafruit_datetime.mpy
- adafruit_display_text
- adafruit_esp32spi
- adafruit_imageload
- neopixel.mpy
4. Download and unzip the CircuitPython Community Bundle that corresponds to the version of CircuitPyton installed on the PyPortal.
5. From the bundle's lib
directory, copy the following library to the lib
directory of the PyPortal:
- circuitpython_base64.mpy
CircuitPython is configured to include the PyPortal's lib
folder in its search path when retrieving libraries. With these libraries copied over, they will be available for import by the project's Python code.
The project's main Python script will contain the necessary processing steps for building and displaying the flight data visualization. Begin by setting up the initial code base of the script, which will serve as the foundation for the subsequent build steps.
1. Create a new file named code.py
in the CIRCUITPY
drive.
This naming convention is used to ensure that the script is executed automatically. CircuitPython is configured to look for and initiate the execution of any script named code.py
when the PyPortal is powered on.
2. Import the project's required libraries by adding the following to the script:
import math
import time
import board
import busio
import displayio
import terminalio
import neopixel
import digitalio
import adafruit_imageload
from adafruit_esp32spi import adafruit_esp32spi
from adafruit_esp32spi import adafruit_esp32spi_wifimanager
from adafruit_datetime import datetime
from adafruit_display_text import label
from circuitpython_base64 import b64encode
The included libraries will enable the script to build visual elements, connect to the internet, perform complex math, and more.
3. Import the secrets.py
file:
# Import secrets file
try:
from secrets import secrets
except ImportError:
print("Missing secrets.py file!")
raise
User-specific configuration values contained in the secrets.py
file are imported into the main script, allowing them to be utilized in later steps. The script will raise an error if secrets.py
does not exist on the PyPortal.
4. Define the display and main display group:
# Create display and main group
display = board.DISPLAY
main_group = displayio.Group()
display.root_group = main_group
Visual elements, including the splash image, map, and aircraft icons, will be organized into various display groups. This code establishes a main_group
group that will be the parent to all others.
5. Download the project's splash image splash.bmp
from here, and copy it into the CIRCUITPY
directory.
6. Display the splash image by adding the following:
# Display splash image
splash_group = displayio.Group()
image = displayio.OnDiskBitmap("/splash.bmp")
image_sprite = displayio.TileGrid(image, pixel_shader=image.pixel_shader)
splash_group.append(image_sprite)
main_group.append(splash_group)
The splash image serves as a visual placeholder for the PyPortal while the script executes additional configuration steps, to be added in later build steps.
6. Lastly, create an empty loop for the script to enter:
# Processing loop
while True:
pass
When the script reaches this code it will repeat continuously, taking no further actions. Later steps of gathering and displaying updated flight data will take place within this loop.
After saving each of the above code steps to code.py
, the script will automatically run upon resetting the system, and the splash image will be shown on the PyPortal display.
In order to obtain the map image and collect the flight information required for the visualization, the PyPortal must connect to the internet over WiFi.
1. Update the ssid
and password
values in the secrets.py
file with values for a local WiFi network that the PyPortal can access.
2. Add the following code to code.py
above the processing loop:
# Configure WIFI manager
esp32_cs = digitalio.DigitalInOut(board.ESP_CS)
esp32_ready = digitalio.DigitalInOut(board.ESP_BUSY)
esp32_reset = digitalio.DigitalInOut(board.ESP_RESET)
spi = busio.SPI(board.SCK, board.MOSI, board.MISO)
esp = adafruit_esp32spi.ESP_SPIcontrol(spi, esp32_cs, esp32_ready, esp32_reset)
status_light = neopixel.NeoPixel(board.NEOPIXEL, 1, brightness=0.2)
wifi = adafruit_esp32spi_wifimanager.ESPSPI_WiFiManager(esp, secrets, status_light)
This code creates a WiFi manager object which includes methods for establishing an internet connection and making HTTP requests. It also works to automatically reconnect to the network if a connection is dropped.
3. Connect to the WiFi using the WiFi manager:
# Connect WiFi
print("Connecting to wifi...")
wifi.connect()
Calling the manager's connect
method initiates the WiFi connection using the ssid
and password
values passed into it via the secrets
dictionary.
The PyPortal's neopixel LED serves as a status indicator, turning red while connecting to the network and switching to blue upon successful connection.
Generate Custom MapA map image is used as the base layer of the flight data visualization. Users are able to specify any geographic location with a latitude/longitude coordinate, and the script will retrieve a custom map centered on that point.
Geoapify is an online service that offers an API for dynamically generating static maps. The project utilizes this service to build a map image that meets the size, geographic bounds, and style requirements for the visualization.
1. Sign up for a free account on Geoapify.
While free accounts have limited processing credits, this project only generates one map request each time the main script is executed.
2. From the "My Projects" page on Geoapify, click "Add New Project", and create a new project named "flight-tracker".
3. Copy the API key for this project, and paste it as the geoapify_key
value in the secrets.py
file.
This unique key will be included in each request made to the Geoapify static map service, as required by the API.
4. Define a center location coordinate:
# Define map center (New York City)
center_lat = 40.7831
center_lon = -73.9712
This geographic coordinate will become the center of the map image that is retrieved. The coordinate for New York City is used for example.
To feature any other location of interest, simply modify these longitude and latitude values to represent that specific location.
5. Define a map distance:
# Map distance (km)
distance = 20
This value determines the extent of the map coverage, measured in kilometers from the center coordinate.
By adjusting this value, the size of the visualized geographic area can be increased or decreased.
6. Calculate display dimensions:
# Display dimensions
display_width = display.width
display_height = display.height
aspect_ratio = display_width / display_height
In order to properly display the map image, it must be generated with a matching aspect ratio to the PyPortal display.
Here the height and width of the display are used to calculate this ratio.
7. Define the get_bounds
function below the script's import
statements:
def get_bounds(lat, lon, distance, ratio=1):
''' Return min/max bounds for box centered on input
lat/lon coordinate for input distance
Ratio parameter determines the width:height ratio
of the resulting box dimensions
'''
# Set earth radius
EARTH_RADIUS = 6378.1
# Convert to radians
rad_lat = math.radians(lat)
rad_lon = math.radians(lon)
# Calculate angular distance in radians
rad_dist = distance / EARTH_RADIUS
# Calculate distance deltas
delta_lat = rad_dist
delta_lon = math.asin(math.sin(rad_dist) / math.cos(rad_lat))
if ratio < 1:
delta_lat *= ratio
else:
delta_lon *= ratio
# Calculate latitude bounds
min_rad_lat = rad_lat - delta_lat
max_rad_lat = rad_lat + delta_lat
# Calculate longitude bounds
min_rad_lon = rad_lon - delta_lon
max_rad_lon = rad_lon + delta_lon
# Convert from radians to degrees
max_lat = math.degrees(max_rad_lat)
min_lat = math.degrees(min_rad_lat)
max_lon = math.degrees(max_rad_lon)
min_lon = math.degrees(min_rad_lon)
return max_lat, min_lat, max_lon, min_lon
8. Then calculate the map bounds by calling the function:
# Calculate bounds
print("Calculating map bounds...")
lat_max, lat_min, lon_max, lon_min = get_bounds(center_lat, center_lon, distance, ratio=aspect_ratio)
The Geoapify API requires that the geographic area of a requested map be rectangular in shape and defined by a pair of opposite corner coordinates.
Using the center point, distance, and aspect ratio values defined above, the get_bounds
function calculates the latitude and longitude minimums and maximums that will produce a rectangular area based on these inputs.
Note that the distance
value determines the geographic extent of the map along its shorter image dimension, while the extent of the larger dimension is calculated to maintain the defined aspect_ratio
.
9. Define the map API parameters:
# Geoapify map parameters
map_params = {
"style": "klokantech-basic",
"width": display_width * 2,
"height": display_height * 2,
"apiKey": secrets["geoapify_key"],
"format": "png",
"area": "rect:%f,%f,%f,%f" % (lon_max, lat_max, lon_min, lat_min)
}
The Geoapify API provides a range of request parameters for specifying the style and content of a generated map. The project's custom map values are stored here as a Python dictionary.
Previously calculated lon_max
, lat_max
, lon_min
, and lat_min
values, which form the map corner coordinates, are formatted into a string and passed to the API through the area
parameter.
The width
and height
parameters are configured to be twice the size of the display dimensions. This produces smaller labels on the map image but also requires the image to be later resized to the PyPortal display size.
As required by the API, the apiKey
value is set to the geoapify_key
value included in secrets.py
.
10. Define the build_url
function below the script's import
statements:
def build_url(url, params={}):
''' Return URL with formatted parameters added '''
params_str = "&".join(["%s=%s" % (key, value) for key, value in params.items()])
return url + "?" + params_str
7. Build the full Geoapify map URL:
# Build Geoapify map URL
map_url = build_url("https://maps.geoapify.com/v1/staticmap", map_params)
print('Geoapify map URL: ' + map_url)
The parameter values defined in the dictionary need to be formatted as a URL query string and appended to the API base URL.
This is accomplished using the build_url
function, which generates a complete Geoapify request URL from the parameter and base URL inputs.
The full map URL map_url
is printed to the serial console, where it can be copied and pasted into a browser to view the map image that is generated.
The Geoapify map can be downloaded as a PNG image, however, the PyPortal can only display BMP images. Currently, no libraries exist for CircuitPython to convert PNG to BMP using code.
Instead, this project utilizes Adafruit IO's online image conversion service. This service takes in any image URL, performs a conversion, and hosts the converted image online.
1. Sign up for a free account on Adafruit IO.
2. From the "IO" page, click on the yellow "key" icon to view the account username and API key
3. Copy the username and API key, and paste them as the aoi_username
and aoi_key
values in the secrets.py
file.
Both of these values are required when making a request to the Adafruit image conversion service.
4. Define the url_encode
function below the script's import
statements:
def url_encode(string):
''' Return URL encoding of input string '''
encoded_string = ''
for char in string:
if char.isalpha() or char.isdigit() or char in ('-', '_', '.', '~'):
encoded_string += char
else:
encoded_string += '%' + '{:02X}'.format(ord(char))
return encoded_string
5. Then define the conversion parameters:
# Adafruit IO image convert parameters
convert_params = {
"x-aio-key": secrets["aio_key"],
"width": display_width,
"height": display_height,
"output": "BMP16",
"url": url_encode(map_url)
}
The conversion service accepts request parameters specifying the URL of the image to convert, along with the desired output image dimensions and format.
Before being set as the url
parameter value, the map_url
string is formatted using the url_encode
function to remove any special characters.
The height
and width
are set to the display dimensions, so the converted BMP map image will be the same size as the PyPortal display.
As required by the API, the x-aoi-key
value is set to the aio_key
value included in secrets.py
.
6. Build the full Adafruit IO convert URL:
# Build Adafruit IO image convert URL
convert_url = build_url(
f"https://io.adafruit.com/api/v2/{secrets["aio_username"]}/integrations/image-formatter",
convert_params
)
print('Converted Image URL: ' + convert_url)
Again the bulid_url
function is employed to merge the conversion parameters with the service's base URL to form a complete Adafruit IO request URL.
Note that the base URL requires the aio_username
value as part of its template.
The full converted image URL convert_url
is printed to the serial console, where it can be copied and pasted into a browser to view.
Adafruit IO's conversion service provides access to the converted map image through a URL. In order to be displayed, this image must first be downloaded to the PyPortal's local storage.
1. Create a new file named boot.py
in the CIRCUITPY
drive.
2. Add the following code to the boot.py
file, and reset the PyPortal:
import time
import storage
print("**************** WARNING ******************")
print("Using the filesystem as a write-able cache!")
print("This is risky behavior, backup your files!")
print("**************** WARNING ******************")
storage.remount("/", disable_concurrent_write_protection=True)
time.sleep(5)
Downloading the image to the PyPortal requires write access to the device's filesystem. However, CircuitPython denies write access by default.
To enable this functionality, the above boot.py file is saved on the PyPortal. This file is automatically executed when the board starts up, and runs commands to disable CircuitPython's write protection settings.
3. Add the following function below the import
statements in code.py
:
def download_file(url, fname, chunk_size=4096, headers=None):
''' Download file from URL and store locally '''
# Request url
response = wifi.get(url, stream=True)
# Determine content length from response
headers = {}
for title, content in response.headers.items():
headers[title.lower()] = content
content_length = int(headers["content-length"])
# Save streaming data to output file
remaining = content_length
print("Saving data to ", fname)
stamp = time.monotonic()
with open(fname, "wb") as file:
for i in response.iter_content(min(remaining, chunk_size)):
remaining -= len(i)
file.write(i)
if not remaining:
break
response.close()
4. Then call the function to download the converted map image:
# Download converted map image
image_fname = "/map.bmp"
print("Downloading converted image...")
download_file(convert_url, image_fname)
The download_file
function takes in a URL of the file to be downloaded, and an output filename to save it as. A web request for the file is made, and its contents are streamed to the output file.
When downloading is complete, the convert_url
BMP map image will be saved as map.bmp
and accessible in the CircuitPython
drive.
5. Display the converted map image:
# Display converted map image
map_group = displayio.Group()
image = displayio.OnDiskBitmap(image_fname)
image_sprite = displayio.TileGrid(image, pixel_shader=image.pixel_shader)
map_group.append(image_sprite)
main_group.append(map_group)
The locally saved map image is loaded and displayed, replacing the splash image on the PyPortal screen.
OpenSky Network's API provides access to real-time aircraft data. By specifying a bounding box as input, the API returns location metadata for all aircraft currently flying within the defined bounds.
The project collects the most recent aircraft data over the location of interest by querying this API every thirty seconds, using the same geographic bounds that were used to generate the map image.
1. Sign up for a free account on Opensky Network.
2. Update the opensky_username
and opensky_password
values in the secrets.py
file with the Opensky Network account credentials.
These credentials are used to perform Basic Authorization when making a request to the OpenSky Network API.
3. Define the OpenSky Network aircraft search parameters:
# OpenSky search params
opensky_params = {
"lamin": lat_min,
"lamax": lat_max,
"lomin": lon_min,
"lomax": lon_max
}
The Opensky Network API accepts minimum and maximum latitude and longitude values as parameters to define a bounding box search area.
These same values were used to define the geographic area of the map image, ensuring that the search area encompasses the entire map coverage.
4. Build the full OpenSky Network search URL:
# Build OpenSky search URL
opensky_url = build_url(
"https://opensky-network.org/api/states/all",
opensky_params
)
print('OpenSky search URL:' + opensky_url)
Here the search parameters are converted into a query string and attached to the API's base URL to create the full search request URL.
5. Define request headers:
# Build request headers
auth_credentials = secrets["opensky_username"] + ":" + secrets["opensky_password"]
auth_token = b64encode(auth_credentials.encode("utf-8")).decode("ascii")
headers = {'Authorization': 'Basic ' + auth_token}
The OpenSky Network API allows more daily requests for authenticated users, which is necessary when making requests every 30 seconds.
Authentication is achieved by including the account username and password, encoded in base64, within the Authorization
header of each request.
6. Inside the script's while
loop, add code to execute an API request:
# Request Opensky data
print('Requesting data from Opensky...')
response = wifi.get(opensky_url, headers=headers)
data = response.json()
The WiFi manager object is utilized to send a GET request to the search API URL, including the necessary authorization headers.
A successful response from the API returns JSON-formatted search results which the script parses into a Python dictionary.
7. Print an overview of the returned API data:
# Parse Opensky response
states = data["states"]
unix_time = data["time"]
time_str = str(datetime.fromtimestamp(unix_time))
print("Opensky data collected at " + time_str)
print("Number of aircraft inside bounds: %s" % (len(states) if states else 0))
Two items are contained in the results dictionary: states
and time
.
A state vector is a list of all tracking data for an aircraft, including position, altitude, velocity, and identity, captured at a certain point in time.
The states
item is a list containing the state vector of each aircraft obtained by the search, while the time
item is the UNIX time that each state vector is associated with.
Here, an overview of the results is printed to the console, where time
is converted to a date string, and the number of aircraft is determined by the length of the states
list.
8. Add a delay between loops:
# Delay between loops
time.sleep(30)
A sleep time is added to the end of the loop, which controls how often an iteration of the loop will run.
With the above code added, users can open a serial console and see an overview of the aircraft search results printed every 30 seconds.
The project's visualization uses aircraft icons to depict the location and orientation of the real-world aircraft obtained from the search results.
These icons are positioned on the map image by converting the longitude and latitude of each aircraft into pixel coordinates. The orientation of each aircraft determines which rotated icon is displayed.
1. Download the icons sprite sheet image icons.bmp
from here, and copy it into the CIRCUITPY
directory.
The sprite sheet consists of nine variations of a yellow aircraft icon, each rotated at 45-degree intervals, on a green background.
2. Add the following code above the script's while
loop:
# Load aircraft icon sheet
tile_size = 16
icon_sheet, palette = adafruit_imageload.load(
"icons.bmp",
bitmap=displayio.Bitmap,
palette=displayio.Palette,
)
palette.make_transparent(0)
Here the icon sprite sheet bitmap is loaded, along with its palette of colors. The palette's first indexed color, green, is set to be displayed as transparent.
3. Create a group to display the icons:
# Create aircraft display group
aircraft_group = displayio.Group()
main_group.append(aircraft_group)
Aircraft icons are organized into a single display group. This allows for icons to be controlled as an independent layer within the visualization.
4. Within the while
loop, add code to clear the map of all aircraft icons:
# Clear previous aircraft icons
while len(aircraft_group):
aircraft_group.pop(-1)
In each iteration of the loop, when new aircraft data is collected, it becomes necessary to remove any previously displayed icons from the map.
This is achieved by individually removing each existing item from the aircraft display group until the group is empty.
5. Loop through the list of state vectors and extract position values:
# Process aircraft data
if states:
for state in states:
# Get position data
lon = state[5]
lat = state[6]
track = state[10]
The script loops through each state vector element of the states
list and extracts the relevant aircraft position data needed to plot the icon.
Each state vector is a sequential list of aircraft tracking values. Within this list, the aircraft's longitude, latitude, and track (orientation angle) values are accessed using their known indexes.
6. Skip any aircraft with empty position data:
# Skip aircraft with empty data
if not lat or not lon or not track:
continue
In some cases, state vectors may contain null longitude, latitude, or track values. These vectors are excluded from further processing.
7. Add the following map_range
function below the script's import
statements:
def map_range(value, in_min, in_max, out_min, out_max):
''' Map input value to output range '''
return out_min + (((value - in_min) / (in_max - in_min)) * (out_max - out_min))
8. Add the following calculate_pixel_position
function:
def calculate_pixel_position(lat, lon, image_width, image_height, lat_min, lat_max, lon_min, lon_max):
''' Return x/y pixel coordinate for input lat/lon values, for
given image size and bounds
'''
# Calculate x-coordinate
x = map_range(lon, lon_min, lon_max, 0, image_width)
# Calculate y-coordinate using the Mercator projection
lat_rad = math.radians(lat)
lat_max_rad = math.radians(lat_max)
lat_min_rad = math.radians(lat_min)
merc_lat = math.log(math.tan(math.pi/4 + lat_rad/2))
merc_max = math.log(math.tan(math.pi/4 + lat_max_rad/2))
merc_min = math.log(math.tan(math.pi/4 + lat_min_rad/2))
y = map_range(merc_lat, merc_max, merc_min, 0, image_height)
return int(x), int(y)
To accurately position the icon on the map image, X and Y pixel coordinates need to be calculated from the location values of the aircraft.
The calculate_pixel_position
function handles this conversion, utilizing inputs such as the aircraft's latitude and longitude, map image dimensions, and the geographic bounds of the map.
For the X pixel coordinate, the aircraft's longitude value is mapped onto the range of the longitude bounds and scaled to match the width of the image.
To determine the Y coordinate, the calculation must account for the distortion caused by the map's Mercator projection.
The aircraft's latitude value is converted to Mercator projection and mapped onto the range latitude bounds, which are also converted accordingly. The resulting value is then scaled to match the height of the image.
9. Calculate the pixel coordinates:
# Calculate icon x/y coordinate
x, y = calculate_pixel_position(lat, lon, display_width, display_height, lat_min, lat_max, lon_min, lon_max)
Using the calculate_pixel_postion
function, the x
and y
pixel coordinates are calculated from the aircraft's lat
and lon
values.
10. Determine which image tile to display:
# Calculate icon tile index
tile_index = int((track + 23) / 45)
The orientation of an aircraft can range from 0 to 360 degrees, while the available icons only exist for 45-degree intervals.
Here the indexed sprite sheet position is calculated for the icon whose rotation most closely matches the orientation value.
11. Add the icon to the display group:
# Add aircraft icon
icon = displayio.TileGrid(
icon_sheet,
pixel_shader=palette,
tile_width = tile_size,
tile_height = tile_size,
default_tile=tile_index,
x = x,
y = y
)
aircraft_group.append(icon)
Bitmap images are added to display groups as TileGrid
objects.
A new TileGrid is created with the icon sheet bitmap, its palette, the index of the designated icon, and x and y pixel coordinate values. This object is added to the aircraft display group.
With the above blocks of code added to the project's main script, the visualization updates every thirty seconds, showing aircraft icons corresponding to the latest OpenSky Network flight data.
Display TimeThe final element of the project's visualization is a label displaying the date and time at which the latest flight data was collected.
1. Add the following code above the script's while
loop:
# Create time display group
time_group = displayio.Group()
main_group.append(time_group)
This code creates a new display group to hold the time information text as an independent layer of the visualization.
2. Create the text label:
# Create time label
time_label = label.Label(
font = terminalio.FONT,
color=0x000000,
background_color=0xFFFFFF,
anchor_point=(0,0),
anchored_position=(5,5),
padding_top = 2,
padding_bottom = 2,
padding_left = 2,
padding_right = 2
)
time_group.append(time_label)
Text is added to display groups as Label
objects. These objects contain attributes for text font, color, positioning, and more.
Here a new Label object is initialized with black text color, a white background, and a position in the top left corner of the display.
3. Within the script's while
loop, after the OpenSky Network data is parsed, add the following code:
# Update time label
time_label.text = time_str
With each iteration of the while
loop, the text attribute of the label is set as the formatted time value received from the OpenSky Network API.
The display automatically refreshes to show this updated time value when it is set as the attribute value.
After completing these instructions, the PyPortal is ready to be displayed to help track and monitor aircraft activity in any area around the world.
Users can keep the project running to continuously view aircraft positions as they track across the map in near real-time.
The complete project, including scripts, libraries, and image assets, can be found on GitHub.
Comments