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)
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()