Recently I had to pull one of my Raspberry Pis out of service to make some hardware changes to it. Because this Pi runs headless, in order to avoid potentially corrupting the SD card I had to log into the Pi from another computer and issue the shutdown command. While this is only a minor inconvenience, it occurred to me that I could write a short script to shutdown the Pi when a button was pushed. Since I already had the soldering iron out and the Pi on the bench I decided to run with the idea, and have a little fun with it while I was at it.
The basic solution to this is actually very simple, but I prefer to take simple things and make them unnecessarily complicated and over-engineered. It's just my nature, but I realize not everyone shares my prerogative. So in the interest of making this post as useful as possible, I am going to briefly explain the easy way first, and then I'll show you the solution I ended up using.
The Easy Way
All we really need to make this work is a momentary-on push button switch wired to a GPIO pin and a short Python script. I used a typical pull-up resistor configuration for the button. If you have never done this before use the diagram is to the right. The Python script will use the RPi.GPIO module to set edge detection on the GPIO pin, and then use a threaded callback function to initiate shutdown. To do the actual shutdown I will use the Python module os to interact directly with the operating system. The code is short and sweet. Feel free to copy or republish it. Here it is:
#!/usr/bin/env python
import RPi.GPIO as GPIO
import os
from time import sleep
def initiate_shutdown(channel):
os.system("shutdown now -h")
BUTTON_PIN = 21
GPIO.setmode(GPIO.BCM) # Sets GPIO pins to BCM numbering
GPIO.setup(BUTTON_PIN, GPIO.IN) # Sets pin to input
GPIO.add_event_detect(BUTTON_PIN, GPIO.FALLING, callback=initiate_shutdown, bouncetime=500) # Sets edge detection on pin
# infinite loop
while True:
sleep(60)
So now, when the button is pushed, the Raspberry Pi will execute a soft shutdown, just as I set out to accomplish. But what happens if the button is accidentally pushed, perhaps I should add in a short delay before shutting down with a way to abort the procedure. But this machine is headless, so I would need to devise a way to alert someone that the button was pushed, probably using light or sound. And I don't want to waste any more GPIO pins than would be absolutely necessary. It was becoming clear to me that this project needed to be tweaked a bit, so I went back to the drawing board and came up with a solution that fit my situation much better.
The Fun Way
In the end I decided to use a button with a built in LED that would stay on when the Pi was up and running. I used a second GPIO pin wired to an NPN-transistor to control the LED in the button (once again a very standard way to control an LED, but the diagram is to the right if you need it. *EDIT: Not pictured in the diagram to the right is a current limiting resistor between the GPIO pin and the transistor. I used a 1k ohm resistor, which limited the current draw from the GPIO pin to 2.5mA. Thanks to Dan Koellen who pointed this out in the comments.) When the button is pushed the LED will flash for 30 seconds to show that the shutdown procedure has begun, and it will go off when the Pi has shutdown. To abort the shutdown procedure I will use the same shutdown-button so I don't waste a third GPIO pin. When pressed the second time, shutdown is canceled and the LED goes back to always-on. I also decided that I needed to have an audible notification, and a voice could be more fun than a simple beep or buzzer. There are plenty of text-to-speech websites that allow you to download the voice as an MP3 for free, so I found one that I liked and recorded soundbites for shutdown-initiated and shutdown-aborted.
For the Python script, I thought I could use the same threaded callback design that I used in the code for the easy solution, but for some reason I couldn't get event detection to work inside the callback function. Instead I went with a slightly less elegant, but just as effective solution of putting an if statement inside an infinite loop to test for event detection. There are plenty of ways to play audio using Python, but since I had already imported the os module to shutdown, I decided to use command line functionality to play the mp3. For this I had to install mpg123 using apt-get install mpg123. If you do something similar and use a different method to play the audio file, just be sure to background it or run it in a different thread. Otherwise your program will stop and wait for the audio file to finish playing. Anyway, enough blathering about the code. Here it is. Fell free to copy it if you like, and leave any questions in the comments if you have any issues getting it to work for your specific situation.
#!/usr/bin/env python
import RPi.GPIO as GPIO
import os
from time import sleep
def play_audio1():
os.system('mpg123 -q shutdown30.mp3 &')
# print 'playing audio1'
def play_audio2():
sleep (1)
os.system('mpg123 -q aborted.mp3 &')
# print 'playing audio2'
def stop_audio():
os.system('pkill mpg123')
# print 'stoped audio'
def initiate_shutdown():
GPIO.remove_event_detect(BUTTON_PIN)
GPIO.add_event_detect(BUTTON_PIN, GPIO.FALLING, bouncetime=500)
play_audio1()
for x in range (1, 30):
GPIO.output(LED_PIN, 0)
sleep(0.5)
GPIO.output(LED_PIN, 1)
sleep(0.5)
if GPIO.event_detected(BUTTON_PIN):
break
else:
os.system("shutdown now -h")
stop_audio()
play_audio2()
GPIO.remove_event_detect(BUTTON_PIN)
GPIO.add_event_detect(BUTTON_PIN, GPIO.FALLING, bouncetime=500)
GPIO.output(LED_PIN, 1)
# Begining of Program
BUTTON_PIN = 21
LED_PIN = 20
GPIO.setmode(GPIO.BCM)
GPIO.setup(BUTTON_PIN, GPIO.IN)
GPIO.setup(LED_PIN, GPIO.OUT)
GPIO.output(LED_PIN, 1)
GPIO.add_event_detect(BUTTON_PIN, GPIO.FALLING, bouncetime=500)
while True:
sleep(1)
if GPIO.event_detected(BUTTON_PIN):
initiate_shutdown()
And that is that. The soft shutdown button is now functional. Here is the end product (the button is on the right hand side):
And here is a short video of the button in action. Sorry it's a little shaky. I either need a tripod for my phone or less caffeine:
EDIT: I'm adding the following link for anyone interested in learning more about using the RPi.GPIO module in Python to interact with the Pis GPIO pins: https://sourceforge.net/p/raspberry-gpio-python/wiki/Inputs/
EDIT: One thing I forgot to mention is that in order to have the script run on start-up I added the script to the /etc/rc.local file. If you use this method be sure to use the full path to the file, and run it using sudo so that it can control the GPIO pins. Also be sure to add an ampersand to the end of the line to background the script since it uses an infinite loop. The line in my rc.local file looks like this:
sudo /home/pi/soft-shut/soft-shut.py &
EDIT: When I first ran this script with the audio files I noticed that the volume was really low. Initially I thought it was the result of buying the cheapest little speakers I could find, but it turned out that by default the volume for the Pi's audio-out is not turned all the way up. This is easily remedied with the following command that only needs to be run once:
amixer set PCM -- 100%