diff --git a/README.md b/README.md index 1ace123..90a940b 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/display/__init__.py b/display/__init__.py index 47a6a81..b0b6187 100644 --- a/display/__init__.py +++ b/display/__init__.py @@ -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 @@ -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)) @@ -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, @@ -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" @@ -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) @@ -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() diff --git a/requirements.txt b/requirements.txt index 1410f7a..95d065a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 \ No newline at end of file diff --git a/scenes/clock.py b/scenes/clock.py index 46f4106..d38b015 100644 --- a/scenes/clock.py +++ b/scenes/clock.py @@ -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 diff --git a/scenes/date.py b/scenes/date.py index c574770..d6ddeb6 100644 --- a/scenes/date.py +++ b/scenes/date.py @@ -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 diff --git a/scenes/day.py b/scenes/day.py index 51f1769..61c56c5 100644 --- a/scenes/day.py +++ b/scenes/day.py @@ -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 diff --git a/scenes/loadingled.py b/scenes/loadingled.py index ee736e0..a0b68ab 100644 --- a/scenes/loadingled.py +++ b/scenes/loadingled.py @@ -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 @@ -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) @@ -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) \ No newline at end of file + self.led.on() \ No newline at end of file diff --git a/scenes/weather.py b/scenes/weather.py index b2448c8..716fe60 100644 --- a/scenes/weather.py +++ b/scenes/weather.py @@ -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 diff --git a/utilities/animator.py b/utilities/animator.py index 9d668fa..4833aa8 100644 --- a/utilities/animator.py +++ b/utilities/animator.py @@ -18,6 +18,7 @@ def __init__(self): self.frame = 0 self._delay = DELAY_DEFAULT self._reset_scene = True + self._enabled = True self._register_keyframes() @@ -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): @@ -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__":