Search code examples
pythonraspberry-pi

How can I use a button to terminate a python script on Raspberri Pi midway through its execution?


Using a Raspberry Pi, I have built a device which queries an API, and then sets of an alarm and some (very annoying) flashing lights if the API returns a different result ID to the previous call.

For the purposes of illustration, let's say the API returns a DM from a social media platform.

I also have a button, and I would like to use the button to terminate the entire process if it's inconvenient for an alarm and some (very annoying) flashing lights to be going off at that particular time (eg I'm in a meeting).

However, I'm not especially experienced with Python, and I'm struggling to understand how I can interrupt the script mid-way through it's execution.

Can anyone advise of a way in which I could detect a button push, using the GPIO input, and stop the device from playing audio and flashing its lights?

Here's the code I've got so far. In the interests of brevity I've stripped out anything which isn't essential to the question at hand.


# (dependencies have been imported)

GPIO.setmode(GPIO.BCM)
GPIO.setup(26, GPIO.OUT)
GPIO.setup(20, GPIO.OUT)
GPIO.setup(21, GPIO.OUT)


# get the latest API response and store it

response = requests.get('api.call?params')
messageID = response['result'][0]['id']


# loop the query

try:
  while True:
    
    time.sleep(30)
    
    response = requests.get('api.call?params')
    data = response['result'][0]

    
    # if it's a new message, set off the alarm

    if(data['id'] != messageID): 
      messageID = data['id']

      fanfare = AudioSegment.from_mp3('fanfare.mp3')
      
      
      # turn on lights

      GPIO.output(26, False)
      GPIO.output(21, False)
      GPIO.output(20, False)

      play(fanfare)

      # Here is where I'm having the problem
      # I'm not sure how I can stop the fanfare
      # And skip to the lights being turned off (below),
      # or invoke a function that does so

      GPIO.output(26, False)
      GPIO.output(21, False)
      GPIO.output(20, False)
      


Solution

  • Presumably you would like the music to stop playing when you press the button. In order to do that, you'll probably want to use something other than pydub.playback for playing the music. I would suggest pygame, which has facilities to start/pause/unpause/stop playing music from an MP3 file; e.g:

    import pygame
    import time
    
    pygame.mixer.init()
    pygame.mixer.music.load('fanfare.mp3')
    pygame.mixer.music.play()
    time.sleep(2)
    pygame.mixer.music.stop()
    

    It would be convenient to wrap your "alarm" functionality into a class; we can have class methods for starting and stopping the alarm that handle both the LEDs and the music. Maybe something like this:

    import threading
    import time
    import pygame
    
    import RPi.GPIO as GPIO
    
    class Alarm:
        def __init__(self, led_pins, music):
            self.led_pins = led_pins
            self.music = music
            self.active = False
            pygame.mixer.music.load(self.music)
    
        def _raise_alarm(self):
            for pin in self.led_pins:
                GPIO.output(pin, 1)
            pygame.mixer.music.play()
            while pygame.mixer.music.get_busy():
                time.sleep(0.01)
            self.stop()
    
        def start(self):
            self.active = True
            t_alarm = threading.Thread(target=self._raise_alarm, daemon=True)
            t_alarm.start()
    
        def stop(self):
            self.active = False
            for pin in self.led_pins:
                GPIO.output(pin, 0)
            pygame.mixer.music.stop()
    

    When you call the start method, this turns on the LEDs and starts playing music in a background thread. After the music finishes playing, it will turn off the LEDs. You can also stop it prematurely by calling the stop method.

    We can see how that works with code like (assuming the above code is in alarm.py):

    import alarm
    import pygame
    import time
    
    import RPi.GPIO as GPIO
    
    GPIO.setmode(GPIO.BCM)
    GPIO.setup(4, GPIO.OUT)
    
    pygame.mixer.init()
    
    a = alarm.Alarm([4], 'fanfare.mp3')
    a.start()
    input("press RETURN to stop the alarm")
    a.stop()
    

    Next, we need to integrate button detection into your main loop. One way to do that is with the GPIO.add_event_detect and GPIO.event_detected methods, which you can use to asynchronously detect the button press. We can do something like this:

    while True:
    
        time.sleep(check_interval)
    
        new_message_id = get_message_id()
        if new_message_id != last_message_id:
            print("START ALARM")
            alarm.start()
            last_message_id = new_message_id
            GPIO.add_event_detect(button_pin, GPIO.FALLING)
    
            while alarm.active:
                if GPIO.event_detected(4):
                    print("STOP ALARM")
                    alarm.stop()
                time.sleep(0.1)
    
            GPIO.remove_event_detect(button_pin)
    

    While the alarm is active, we loop watching for the button. If the button is pressed, we cancel the alarm immediately.

    Putting that all together, we get:

    # Support pygame support message
    import os
    os.environ["PYGAME_HIDE_SUPPORT_PROMPT"] = "1"
    
    import pygame
    import requests
    import threading
    import time
    
    import RPi.GPIO as GPIO
    
    from alarm import Alarm
    
    button_pin = 4
    led_pin = 17
    music_file = "fanfare.mp3"
    endpoint_url = "http://localhost:8000/endpoint"
    check_interval = 10
    
    GPIO.setmode(GPIO.BCM)
    GPIO.setwarnings(False)
    GPIO.setup(button_pin, GPIO.IN, pull_up_down=GPIO.PUD_UP)
    GPIO.setup(led_pin, GPIO.OUT)
    
    
    # This probably needs updating for your particular API
    def get_message_id():
        res = requests.get(endpoint_url)
        res.raise_for_status()
        return res.json()["value"]
    
    
    def main():
        pygame.mixer.init()
    
        # I only have a single LED handy, but you would presumably
        # update this to use multiple pins.
        alarm = Alarm([led_pin], music_file)
        last_message_id = get_message_id()
    
        while True:
            time.sleep(check_interval)
    
            new_message_id = get_message_id()
            if new_message_id != last_message_id:
                print("START ALARM")
                alarm.start()
                last_message_id = new_message_id
                GPIO.add_event_detect(button_pin, GPIO.FALLING)
    
                while alarm.active:
                    if GPIO.event_detected(4):
                        print("STOP ALARM")
                        alarm.stop()
    
                GPIO.remove_event_detect(button_pin)
    
    
    if __name__ == "__main__":
        main()