Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,19 @@ RAINFALL_ENABLED = True

[![Example Weather Chart](https://raw.githubusercontent.com/ColinWaddell/FlightTracker/refs/heads/master/assets/weather.jpg)](https://raw.githubusercontent.com/ColinWaddell/FlightTracker/refs/heads/master/assets/weather.jpg)

### On/off switch
A toggle switch can be wired to a GPIO on the Raspberry Pi which can then turn the display on or off, as well as pause or resume API calls.

Connect your switch to pull the GPIO pin to ground when flipped on, since the pin will
be configured with the pullup resistor enabled.

To enable this, add the following to your `config.py`. Adjust `ON_OFF_SWITCH_GPIO_PIN`
to match your setup, and make sure it is not the same GPIO pin as the loading LED.

```
ON_OFF_SWITCH_GPIO_PIN = 19
```

# License Update:
As of April 2025, Flight Tracker is released under the GNU General Public License v3.0

Expand Down
36 changes: 34 additions & 2 deletions display/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import os
import sys

from gpiozero import Button
from setup import frames
from utilities.animator import Animator
from utilities.overhead import Overhead
Expand All @@ -17,7 +19,6 @@
from rgbmatrix import graphics
from rgbmatrix import RGBMatrix, RGBMatrixOptions


def callsigns_match(flights_a, flights_b):
get_callsigns = lambda flights: [f["callsign"] for f in flights]
callsigns_a = set(get_callsigns(flights_a))
Expand Down Expand Up @@ -48,6 +49,11 @@ def callsigns_match(flights_a, flights_b):
# If there's no experimental config data
LOADING_LED_ENABLED = False

try:
# Attempt to load on/off switch config
from config import ON_OFF_SWITCH_GPIO_PIN
except (ModuleNotFoundError, NameError, ImportError):
ON_OFF_SWITCH_GPIO_PIN = 0

class Display(
WeatherScene,
Expand All @@ -61,6 +67,12 @@ class Display(
Animator,
):
def __init__(self):
# Setup on/off switch, if configured
if ON_OFF_SWITCH_GPIO_PIN:
self.on_off_switch = Button(ON_OFF_SWITCH_GPIO_PIN, bounce_time=0.1)
self.on_off_switch.when_pressed = self.enable_display
self.on_off_switch.when_released = self.disable_display

# Setup Display
options = RGBMatrixOptions()
options.hardware_mapping = "adafruit-hat-pwm" if HAT_PWM_ENABLED else "adafruit-hat"
Expand All @@ -77,7 +89,8 @@ def __init__(self):
options.pixel_mapper_config = ""
options.show_refresh_rate = 0
options.gpio_slowdown = GPIO_SLOWDOWN
options.disable_hardware_pulsing = True
# Only disable hardware pulsing if we are not root
options.disable_hardware_pulsing = (os.getuid() != 0)
options.drop_privileges = True
self.matrix = RGBMatrix(options=options)

Expand Down Expand Up @@ -159,8 +172,27 @@ def grab_new_data(self, count):
):
self.overhead.grab_data()

def enable_display(self):
if not self.enabled:
# only enable if currently off
print("Switch on")
self.enabled = True

def disable_display(self):
if self.enabled:
# only disable if currently on
print("Switch off")
self.enabled = False
# blank the display as well
self.canvas.Clear()

def run(self):
try:
# If enabled, read the initial state of the on/off switch
# so that the state doesn't get inverted when it's off during poweron
if ON_OFF_SWITCH_GPIO_PIN and not self.on_off_switch.is_pressed:
self.disable_display()

# Start loop
print("Press CTRL-C to stop")
self.play()
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ Brotli==1.1.0
certifi==2025.7.9
charset-normalizer==3.4.2
FlightRadarAPI==1.4.0
gpiozero==2.0.1
idna==3.10
requests==2.32.4
RPi.GPIO==0.7.1
soupsieve==2.7
typing_extensions==4.14.1
urllib3==2.5.0
2 changes: 1 addition & 1 deletion scenes/clock.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def __init__(self):

@Animator.KeyFrame.add(frames.PER_SECOND * 1)
def clock(self, count):
if len(self._data):
if len(self._data) or self._reset_scene:
# Ensure redraw when there's new data
self._last_time = None

Expand Down
2 changes: 1 addition & 1 deletion scenes/date.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def __init__(self):

@Animator.KeyFrame.add(frames.PER_SECOND * 1)
def date(self, count):
if len(self._data):
if len(self._data) or self._reset_scene:
# Ensure redraw when there's new data
self._last_date = None

Expand Down
2 changes: 1 addition & 1 deletion scenes/day.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def __init__(self):

@Animator.KeyFrame.add(frames.PER_SECOND * 1)
def day(self, count):
if len(self._data):
if len(self._data) or self._reset_scene:
# Ensure redraw when there's new data
self._last_day = None

Expand Down
19 changes: 8 additions & 11 deletions scenes/loadingled.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from gpiozero import LED
from utilities.animator import Animator
from setup import frames
from time import sleep
import RPi.GPIO as GPIO
import sys

# Attempt to load config data
Expand All @@ -22,10 +22,8 @@ def __init__(self):

def gpio_setup(self):
try:
GPIO.setwarnings(False)
GPIO.setmode(GPIO.BCM)
GPIO.setup(LOADING_LED_GPIO_PIN, GPIO.OUT)
GPIO.output(LOADING_LED_GPIO_PIN, GPIO.HIGH)
self.led = LED(LOADING_LED_GPIO_PIN)
self.led.on()
self.gpio_setup_complete = True
except:
print("Error initializing GPIO", file=sys.stderr)
Expand All @@ -40,12 +38,11 @@ def loading_led(self, count):

if self.overhead.processing:
if self.gpio_setup_complete:
GPIO.output(
LOADING_LED_GPIO_PIN,
GPIO.HIGH if count % 2 else GPIO.LOW
)

if count % 2:
self.led.on()
else:
self.led.off()
else:
# Not processing, leave LED on
if self.gpio_setup_complete:
GPIO.output(LOADING_LED_GPIO_PIN, GPIO.HIGH)
self.led.on()
2 changes: 1 addition & 1 deletion scenes/weather.py
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ def rainfall(self, count):
if not RAINFALL_ENABLED:
return

if len(self._data):
if len(self._data) or self._reset_scene:
# Don't draw if there's plane data
# and force a redraw when this is visible
# again by clearing the previous drawn data
Expand Down
66 changes: 44 additions & 22 deletions utilities/animator.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ def __init__(self):
self.frame = 0
self._delay = DELAY_DEFAULT
self._reset_scene = True
self._enabled = True

self._register_keyframes()

Expand All @@ -37,29 +38,43 @@ def reset_scene(self):

def play(self):
while True:
while self._enabled:
for keyframe in self.keyframes:
# If divisor == 0 then only run once on first loop
if self.frame == 0:
if keyframe.properties["divisor"] == 0:
keyframe()

# Otherwise perform normal operation
if (
self.frame > 0
and keyframe.properties["divisor"]
and not (
(self.frame - keyframe.properties["offset"])
% keyframe.properties["divisor"]
)
):
if keyframe(keyframe.properties["count"]):
keyframe.properties["count"] = 0
else:
keyframe.properties["count"] += 1

self._reset_scene = False
self.frame += 1
sleep(self._delay)

# Animator became disabled, wait until it is enabled again
while not self._enabled:
sleep(self._delay)

# Animator enabled again, start over
self.frame = 0
self._reset_scene = True
for keyframe in self.keyframes:
# If divisor == 0 then only run once on first loop
if self.frame == 0:
if keyframe.properties["divisor"] == 0:
keyframe()

# Otherwise perform normal operation
if (
self.frame > 0
and keyframe.properties["divisor"]
and not (
(self.frame - keyframe.properties["offset"])
% keyframe.properties["divisor"]
)
):
if keyframe(keyframe.properties["count"]):
keyframe.properties["count"] = 0
else:
keyframe.properties["count"] += 1

self._reset_scene = False
self.frame += 1
sleep(self._delay)
if keyframe.properties["divisor"]:
# Force an update for each scene - otherwise it could
# take minutes for some scenes to return
keyframe(0)

@property
def delay(self):
Expand All @@ -69,6 +84,13 @@ def delay(self):
def delay(self, value):
self._delay = value

@property
def enabled(self):
return self._enabled

@enabled.setter
def enabled(self, value: bool):
self._enabled = value

if __name__ == "__main__":

Expand Down