Once you've finished setting up your Matrix Portal S3 with CircuitPython, you can access the code and necessary libraries by downloading the Project Bundle.
To do this, click on the Download Project Bundle button in the window below. It will download to your computer as a zipped folder.
# SPDX-FileCopyrightText: 2023 Trevor Beaton for Adafruit Industries
#
# SPDX-License-Identifier: MIT
import os
import ssl
import time
import board
import wifi
import terminalio
import socketpool
import adafruit_requests
import displayio
import rgbmatrix
import framebufferio
import adafruit_display_text.label
from displayio import OnDiskBitmap, TileGrid, Group
# Release any existing displays
displayio.release_displays()
# --- Matrix Properties ---
DISPLAY_WIDTH = 128
DISPLAY_HEIGHT = 64
# 432 Minutes - 7.2 Hours
NETWORK_CALL_INTERVAL = 25920
# --- Icon Properties ---
ICON_WIDTH = 26 # Width of the icons
ICON_HEIGHT = 26 # Height of the icons
# Calculate the gap between icons
gap_between_icons = 5
GAP_BETWEEN_ICONS = 15 # Gap between the icons
NUMBER_OF_ICONS = 2 # Number of icons to display
PLACEHOLDER_ICON_PATH = "/airline_logos/placeholder.bmp"
# --- Text Properties ---
TEXT_START_X = ICON_WIDTH + 4
TEXT_RESET_X = 170
FONT = terminalio.FONT
TEXT_COLOR = 0x22FF00 # e.g., Green
# Initialize the main display group
main_group = Group()
# Initialize the icon group (this remains static on the display)
static_icon_group = Group()
# Sample Bounding Box
bounding_box = {
"min_latitude": 40.633013, # Southernmost latitude
"max_latitude": 44.953469, # Northernmost latitude
"min_longitude": -111.045360, # Westernmost longitude
"max_longitude": -104.046570, # Easternmost longitude
}
# --- Matrix setup ---
BIT_DEPTH = 2
matrix = rgbmatrix.RGBMatrix(
width=DISPLAY_WIDTH,
height=DISPLAY_HEIGHT,
bit_depth=BIT_DEPTH,
rgb_pins=[
board.MTX_B1,
board.MTX_G1,
board.MTX_R1,
board.MTX_B2,
board.MTX_G2,
board.MTX_R2,
],
addr_pins=[
board.MTX_ADDRA,
board.MTX_ADDRB,
board.MTX_ADDRC,
board.MTX_ADDRD,
board.MTX_ADDRE,
],
clock_pin=board.MTX_CLK,
latch_pin=board.MTX_LAT,
output_enable_pin=board.MTX_OE,
tile=1,
serpentine=True,
doublebuffer=True,
)
display = framebufferio.FramebufferDisplay(matrix, auto_refresh=True)
# --- Wi-Fi setup ---
wifi.radio.connect(
os.getenv("CIRCUITPY_WIFI_SSID"), os.getenv("CIRCUITPY_WIFI_PASSWORD")
)
print(f"Connected to {os.getenv('CIRCUITPY_WIFI_SSID')}")
# --- Networking setup ---
context = ssl.create_default_context()
with open("/ssl.com-root.pem", "rb") as certfile:
context.load_verify_locations(cadata=certfile.read())
pool = socketpool.SocketPool(wifi.radio)
requests = adafruit_requests.Session(pool, context)
# --- Icon Positioning ---
total_icons_height = (ICON_HEIGHT * NUMBER_OF_ICONS) + (
GAP_BETWEEN_ICONS * (NUMBER_OF_ICONS - 1)
)
# Function to scroll objects
def scroll_text_labels(text_labels):
for label in text_labels:
label.x -= 1 # Move label left.
if label.x < -300: # If label has moved off screen.
label.x = TEXT_RESET_X
def construct_query_string(params):
return "&".join(f"{key}={value}" for key, value in params.items())
def fetch_flight_data():
print("Running fetch_flight_data")
base_url = "https://aeroapi.flightaware.com/aeroapi/flights/search"
query_prefix = "-latlong+\""
query_suffix = (
str(bounding_box['min_latitude']) + "+" +
str(bounding_box['min_longitude']) + "+" +
str(bounding_box['max_latitude']) + "+" +
str(bounding_box['max_longitude']) + "\"")
query = query_prefix + query_suffix
params = {
"query": query,
"max_pages": "1",}
headers = {
"Accept": "application/json; charset=UTF-8",
"x-apikey": os.getenv("AERO_API_KEY"), # Replace with your actual API key
}
full_url = f"{base_url}?{construct_query_string(params)}"
response = requests.get(full_url, headers=headers)
if response.status_code == 200:
json_response = response.json() # Parse JSON only once
return process_flight_data(json_response) # Process flights and return
else:
print(f"Request failed with status code {response.status_code}")
if response.content:
print(f"Response content: {response.content}")
return []
def process_flight_data(json_data):
# Initialize an empty list to hold processed flight data
processed_flights = []
for flight in json_data.get("flights", []):
# Use 'get' with default values to avoid KeyError
flight_info = {
"ident": flight.get("ident", "N/A"),
"ident_icao": flight.get("ident_icao", "N/A"),
"fa_flight_id": flight.get("fa_flight_id", "N/A"),
"actual_off": flight.get("actual_off", "N/A"),
"actual_on": flight.get("actual_on", "N/A"),
"origin_code": flight.get("origin", {}).get("code", "UnknownA"),
"origin_city": flight.get("origin", {}).get("city", "UnknownB"),
"origin_country": flight.get("origin", {}).get("country", "UnknownC"),
"destination_code": flight.get("destination", {}).get("code", "UnknownD")
if flight.get("destination")
else "UnknownE",
"destination_city": flight.get("destination", {}).get("city", "UnknownH")
if flight.get("destination")
else "Unknown Destination",
"destination_country": flight.get("destination", {}).get(
"country", "UnknownZ"
)
if flight.get("destination")
else "UnknownG",
"altitude": flight.get("last_position", {}).get("altitude", "N/A"),
"groundspeed": flight.get("last_position", {}).get("groundspeed", "N/A"),
"heading": flight.get("last_position", {}).get("heading", "N/A"),
"latitude": flight.get("last_position", {}).get("latitude", "N/A"),
"longitude": flight.get("last_position", {}).get("longitude", "N/A"),
"timestamp": flight.get("last_position", {}).get("timestamp", "N/A"),
"aircraft_type": flight.get("aircraft_type", "N/A"),
}
# Only add flight_info if the 'ident_icao' is present and not 'N/A'
if flight_info["ident_icao"] != "N/A":
processed_flights.append(flight_info)
return processed_flights
def create_text_labels(flight_data, Ypositions):
local_text_labels = []
for i, flight in enumerate(flight_data):
y_position = Ypositions[i] + GAP_BETWEEN_ICONS
# Since 'country' is not present, use 'origin_city' and 'destination_city' instead
origin_city = flight.get("origin_city", "Unknown City")
destination_city = flight.get("destination_city", "Unknown City")
# Construct the display text for each flight
single_line_text = (
f"{flight['ident']} | From: {origin_city} To: {destination_city}"
)
text_label = adafruit_display_text.label.Label(
FONT, color=TEXT_COLOR, x=TEXT_START_X, y=y_position, text=single_line_text
)
local_text_labels.append(text_label)
return local_text_labels
def create_icon_tilegrid(ident):
airline_code = ident[:3].upper() # Use the first three characters of 'ident'
icon_path = f"/airline_logos/{airline_code}.bmp"
try:
icon_bitmap = OnDiskBitmap(icon_path)
except OSError:
print(f"Icon for {airline_code} not found. Using placeholder.")
icon_bitmap = OnDiskBitmap(PLACEHOLDER_ICON_PATH)
icon_tilegrid = TileGrid(icon_bitmap, pixel_shader=icon_bitmap.pixel_shader, x=0, y=0)
return icon_tilegrid
def update_display_with_flight_data(flight_data, icon_group, display_group):
# Clear previous display items
while len(display_group):
display_group.pop()
# Clear previous icon items
while len(icon_group):
icon_group.pop()
# Limit flight data to the adjusted number of icons
flight_data = flight_data[:NUMBER_OF_ICONS]
# Calculate the y position for each icon
y_positions = [
gap_between_icons + (ICON_HEIGHT + gap_between_icons) * i
for i in range(NUMBER_OF_ICONS)
]
# Create text labels for up to NUMBER_OF_ICONS flights
text_labels = create_text_labels(flight_data, y_positions)
# Add text labels to the display group first so they are behind icons
for label in text_labels:
display_group.append(label)
# Load icons and create icon tilegrids for up to NUMBER_OF_ICONS flights
for i, flight in enumerate(flight_data):
# Calculate the y position for each icon
y_position = y_positions[i]
# Load the icon dynamically
icon_tilegrid = create_icon_tilegrid(flight["ident"])
if icon_tilegrid:
icon_tilegrid.y = y_position
icon_group.append(icon_tilegrid)
# Add the icon group to the main display group after text labels
display_group.append(icon_group)
# Show the updated group on the display
display.root_group = display_group
display.refresh()
return text_labels
def display_no_flights():
# Clear the previous group content
while len(main_group):
main_group.pop()
# Create a label for "Looking for flights..."
looking_label = adafruit_display_text.label.Label(
FONT, color=TEXT_COLOR, text="LOOKING FOR FLIGHTS", x=8, y=DISPLAY_HEIGHT // 2
)
main_group.append(looking_label)
# Update the display with the new group
display.root_group = main_group
display.refresh()
display_no_flights()
flight_json_response = fetch_flight_data()
# Check if we received any flight data
if flight_json_response:
flight_data_labels = update_display_with_flight_data(
flight_json_response, static_icon_group, main_group
)
last_network_call_time = time.monotonic()
while True:
# Scroll the text labels
scroll_text_labels(flight_data_labels)
# Refresh the display
display.refresh(minimum_frames_per_second=0)
current_time = time.monotonic()
# Check if NETWORK_CALL_INTERVAL seconds have passed
if (current_time - last_network_call_time) >= NETWORK_CALL_INTERVAL:
print("Fetching new flight data...")
new_flight_data = fetch_flight_data()
if new_flight_data:
# If flight data is found, update the display with it
new_text_labels = update_display_with_flight_data(
new_flight_data, static_icon_group, main_group
)
else:
# If no flight data is found, display the "Looking for flights..." message
display_no_flights()
# Reset the last network call time
last_network_call_time = current_time
# Sleep for a short period to prevent maxing out your CPU
time.sleep(1) # Sleep for 1 second
Upload the Code and Libraries to the Matrix Portal S3
After downloading the Project Bundle, plug your Matrix Portal S3 into the computer's USB port with a known good USB data+power cable. You should see a new flash drive appear in the computer's File Explorer or Finder (depending on your operating system) called CIRCUITPY. Unzip the folder and copy the following items to the Matrix Portal S3's CIRCUITPY drive.
- lib folder
- airline_logos folder
- code.py
- ssl.com-root.pem
Your Matrix Portal S3 CIRCUITPY drive should look like this after copying the lib folder, airline_logos folder, ssl.com-root.pem file and the code.py file.
Importing Necessary Libraries and Modules
This code snippet is essential for setting up our project with multiple features. Using these libraries together allows the fetching and displaying live data from the internet onto the Matrix.
import os import ssl import time import board import wifi import terminalio import socketpool import adafruit_requests import displayio import rgbmatrix import framebufferio import adafruit_display_text.label from displayio import OnDiskBitmap, TileGrid, Group
Using Bounding Box
A bounding box, is a rectangular area defined by two longitudes (east-west lines) and two latitudes (north-south lines). It effectively creates a box on the map that includes a specific area of interest. Here's what each term within the bounding box definition means:
-
min_latitude: The southernmost latitude of the box (bottom edge). -
max_latitude: The northernmost latitude of the box (top edge). -
min_longitude: The westernmost longitude of the box (left edge). -
max_longitude: The easternmost longitude of the box (right edge).
# Sample Bounding Box
bounding_box = {
"min_latitude": 40.633013, # Southernmost latitude
"max_latitude": 44.953469, # Northernmost latitude
"min_longitude": -111.045360, # Westernmost longitude
"max_longitude": -104.046570, # Easternmost longitude
}
For this project, the goal is to find flights I can see from my location. I visited boundingbox.klokantech.com/ to get bounding box coordinates in my area.
Setting Up Matrix Display
An RGBMatrix object is created with dimensions 128x64 and passed to FramebufferDisplay.
# --- Matrix setup ---
BIT_DEPTH = 2
matrix = rgbmatrix.RGBMatrix(
width=DISPLAY_WIDTH,
height=DISPLAY_HEIGHT,
bit_depth=BIT_DEPTH,
rgb_pins=[
board.MTX_B1,
board.MTX_G1,
board.MTX_R1,
board.MTX_B2,
board.MTX_G2,
board.MTX_R2,
],
addr_pins=[
board.MTX_ADDRA,
board.MTX_ADDRB,
board.MTX_ADDRC,
board.MTX_ADDRD,
board.MTX_ADDRE,
],
clock_pin=board.MTX_CLK,
latch_pin=board.MTX_LAT,
output_enable_pin=board.MTX_OE,
tile=1,
serpentine=True,
doublebuffer=True,
)
display = framebufferio.FramebufferDisplay(matrix, auto_refresh=True)
Root Certificate Issue for MatrixPortal S3
The MatrixPortal S3 (running CircuitPython 8.2.7 ) recently faced an issue where it could not retrieve JSON data via HTTPS and gave an OSError: Failed SSL handshake error. This problem indicates that the MatrixPortal cannot establish a secure connection with the server, which is necessary for HTTPS communication.
To resolve this issue, it's essential to have a Root Certificate. The certificate is provided in the given project folder. This will ensure that you can continue to receive JSON data securely. Be sure to move the Root Certificate file (root_cert.pem) to the root directory of your CIRCUITPY drive.
Setting Up Network Connection
This portion focuses on collecting WiFi credentials and establishing a connection using the SSID and password provided through settings.toml. If you've not yet set up your credentials, be sure to do so here: Setting up your Credentials.
This section also sets up the network context with the necessary SSL certificates and prepares for making HTTPS requests.
# --- Wi-Fi setup ---
wifi.radio.connect(
os.getenv("CIRCUITPY_WIFI_SSID"), os.getenv("CIRCUITPY_WIFI_PASSWORD")
)
print(f"Connected to {os.getenv('CIRCUITPY_WIFI_SSID')}")
# --- Networking setup ---
context = ssl.create_default_context()
with open("/ssl.com-root.pem", "rb") as certfile:
context.load_verify_locations(cadata=certfile.read())
pool = socketpool.SocketPool(wifi.radio)
requests = adafruit_requests.Session(pool, context)
Scrolling Text
The function scroll_text_labels is designed to move a list of text label objects horizontally to the left across the display screen. For each label in the text_labels list, it decrements the horizontal position (label.x) by 1 pixel, creating a scrolling effect.
# Function to scroll objects
def scroll_text_labels(text_labels):
for label in text_labels:
label.x -= 1 # Move label left.
if label.x < -300: # If label has moved off screen.
label.x = TEXT_RESET_X
Fetching Flight Data
The fetch_flight_data function retrieves flight information from the FlightAware AeroAPI within a specified bounding box.
def fetch_flight_data():
print("Running fetch_flight_data")
base_url = "https://aeroapi.flightaware.com/aeroapi/flights/search"
query_prefix = "-latlong+\""
query_suffix = (
str(bounding_box['min_latitude']) + "+" +
str(bounding_box['min_longitude']) + "+" +
str(bounding_box['max_latitude']) + "+" +
str(bounding_box['max_longitude']) + "\"")
query = query_prefix + query_suffix
params = {
"query": query,
"max_pages": "1",}
headers = {
"Accept": "application/json; charset=UTF-8",
"x-apikey": os.getenv("AREO_API_KEY"), # Replace with your actual API key
}
full_url = f"{base_url}?{construct_query_string(params)}"
response = requests.get(full_url, headers=headers)
if response.status_code == 200:
json_response = response.json() # Parse JSON only once
return process_flight_data(json_response) # Process flights and return
else:
print(f"Request failed with status code {response.status_code}")
if response.content:
print(f"Response content: {response.content}")
return []
def process_flight_data(json_data):
# Initialize an empty list to hold processed flight data
processed_flights = []
for flight in json_data.get("flights", []):
# Use 'get' with default values to avoid KeyError
flight_info = {
"ident": flight.get("ident", "N/A"),
"ident_icao": flight.get("ident_icao", "N/A"),
"fa_flight_id": flight.get("fa_flight_id", "N/A"),
"actual_off": flight.get("actual_off", "N/A"),
"actual_on": flight.get("actual_on", "N/A"),
"origin_code": flight.get("origin", {}).get("code", "UnknownA"),
"origin_city": flight.get("origin", {}).get("city", "UnknownB"),
"origin_country": flight.get("origin", {}).get("country", "UnknownC"),
"destination_code": flight.get("destination", {}).get("code", "UnknownD")
if flight.get("destination")
else "UnknownE",
"destination_city": flight.get("destination", {}).get("city", "UnknownH")
if flight.get("destination")
else "Unknown Destination",
"destination_country": flight.get("destination", {}).get(
"country", "UnknownZ"
)
if flight.get("destination")
else "UnknownG",
"altitude": flight.get("last_position", {}).get("altitude", "N/A"),
"groundspeed": flight.get("last_position", {}).get("groundspeed", "N/A"),
"heading": flight.get("last_position", {}).get("heading", "N/A"),
"latitude": flight.get("last_position", {}).get("latitude", "N/A"),
"longitude": flight.get("last_position", {}).get("longitude", "N/A"),
"timestamp": flight.get("last_position", {}).get("timestamp", "N/A"),
"aircraft_type": flight.get("aircraft_type", "N/A"),
}
# Only add flight_info if the 'ident_icao' is present and not 'N/A'
if flight_info["ident_icao"] != "N/A":
processed_flights.append(flight_info)
return processed_flights
Creating Text Labels
The function create_text_labels is responsible for generating a list of label objects that display flight information on the screen. The vertical positioning of each flight's labels on the screen is determined by the Y positions array and a predefined gap. Each label contains the flight's unique identifier, as well as its origin and destination cities.
def create_text_labels(flight_data, Ypositions):
local_text_labels = []
for i, flight in enumerate(flight_data):
y_position = Ypositions[i] + GAP_BETWEEN_ICONS
# Since 'country' is not present, use 'origin_city' and 'destination_city' instead
origin_city = flight.get("origin_city", "Unknown City")
destination_city = flight.get("destination_city", "Unknown City")
# Construct the display text for each flight
single_line_text = (
f"{flight['ident']} | From: {origin_city} To: {destination_city}"
)
text_label = adafruit_display_text.label.Label(
FONT, color=TEXT_COLOR, x=TEXT_START_X, y=y_position, text=single_line_text
)
local_text_labels.append(text_label)
return local_text_labels
Creating Airline Icons
This function create_icon_tilegrid generates airline icons based on flight identifiers. It retrieves the airline code from the identifier and attempts to load the corresponding icon file for display.
def create_icon_tilegrid(ident):
airline_code = ident[:3].upper() # Use the first three characters of 'ident'
icon_path = f"/airline_logos/{airline_code}.bmp"
try:
file = open(icon_path, "rb")
icon_bitmap = OnDiskBitmap(file)
except OSError:
print(f"Icon for {airline_code} not found. Using placeholder.")
file = open(PLACEHOLDER_ICON_PATH, "rb")
icon_bitmap = OnDiskBitmap(file)
icon_tilegrid = TileGrid(icon_bitmap, pixel_shader=icon_bitmap.pixel_shader, x=0, y=0)
return icon_tilegrid
Update Display With Flight Data
This update_display_with_flight_data function displays new flight data. This function takes two parameters: flight_data, a list of dictionaries with data about each flight, and icon_group and display_group, which are groups of display elements on the LED matrix.
def update_display_with_flight_data(flight_data, icon_group, display_group):
# Clear previous display items
while len(display_group):
display_group.pop()
# Clear previous icon items
while len(icon_group):
icon_group.pop()
# Limit flight data to the adjusted number of icons
flight_data = flight_data[:NUMBER_OF_ICONS]
# Calculate the y position for each icon
y_positions = [
gap_between_icons + (ICON_HEIGHT + gap_between_icons) * i
for i in range(NUMBER_OF_ICONS)
]
#...[]
Show "Display No Flights" text
The display_no_flights function handles situations when no flight data is available.
def display_no_flights():
# Clear the previous group content
while len(main_group):
main_group.pop()
# Create a label for "Looking for flights..."
looking_label = adafruit_display_text.label.Label(
FONT, color=TEXT_COLOR, text="LOOKING FOR FLIGHTS", x=8, y=DISPLAY_HEIGHT // 2
)
main_group.append(looking_label)
# Update the display with the new group
display.show(main_group)
display.refresh()
while True:
# Scroll the text labels
scroll_text_labels(flight_data_labels)
# Refresh the display
display.refresh(minimum_frames_per_second=0)
current_time = time.monotonic()
# Check if NETWORK_CALL_INTERVAL seconds have passed
if (current_time - last_network_call_time) >= NETWORK_CALL_INTERVAL:
print("Fetching new flight data...")
new_flight_data = fetch_flight_data()
if new_flight_data:
# If flight data is found, update the display with it
new_text_labels = update_display_with_flight_data(
new_flight_data, static_icon_group, main_group
)
else:
# If no flight data is found, display the "Looking for flights..." message
display_no_flights()
# Reset the last network call time
last_network_call_time = current_time
# Sleep for a short period to prevent maxing out your CPU
time.sleep(1) # Sleep for 1 second
Main Loop
In the main program loop, flight information text labels are scrolled across the display using the scroll_text_labels function. This provides a dynamic view and is achieved by moving each label leftward and wrapping it back to the right once it scrolls off the screen.
while True:
# Scroll the text labels
scroll_text_labels(flight_data_labels)
# Refresh the display
display.refresh(minimum_frames_per_second=0)
current_time = time.monotonic()
Depending on whatNETWORK_CALL_INTERVALis set to, the program checks if it's time to fetch new flight data. If the interval has passed, it calls fetch_flight_data() to retrieve the latest information.
After fetching new data, the display is updated with new flight details using the update_display_with_flight_data function. If no new data is available or if an error occurs, a placeholder message, "Looking for flights," is displayed to inform the viewer that the search is ongoing.
NETWORK_CALL_INTERVAL:
print("Fetching new flight data...")
new_flight_data = fetch_flight_data()
if new_flight_data:
# If flight data is found, update the display with it
new_text_labels = update_display_with_flight_data(
new_flight_data, static_icon_group, main_group
)
else:
# If no flight data is found, display the "Looking for flights..." message
display_no_flights()
# Reset the last network call time
last_network_call_time = current_time
Finally, the loop concludes with a brief sleep statement, which pauses the program for one second.
time.sleep(1) # Sleep for 1 second
Page last edited February 25, 2025
Text editor powered by tinymce.