Monday, June 26, 2017

Adding a Soft Shutdown Switch to Headless Raspberry Pi

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%

13 comments:

  1. Very nice and instructive writeup. Thanks for sharing.

    BTW: By the looks of the hookups, that RPI looks like it's connected to everything including the kitchen sink. What's the primary purpose of it?

    ReplyDelete
    Replies
    1. Thanks. Currently that Pi’s main job is to monitor my smoke detectors and email/text me if they alert. I am currently working on another project to allow that Pi to control sprinklers using an electronic solenoid valve, which is what the relays are for. It looks busier than it needs to be because when I needed one optoisolator circuit for the smoke detectors I built four of them so I would have them for future use. When I needed one relay I installed four, so when I need another in the future it will already be there. I’m hoping by the end of this year to have the GPIO pins on this Pi maxed out monitoring and controlling different systems around my house. I’m up for suggestions if anyone can come up with a good “smart home” function for this Pi.

      Delete
  2. I like the missiles launch switch. Nice touch.

    ReplyDelete
    Replies
    1. Thanks. Unfortunately my wife made me remove the actual missiles.

      Delete
  3. What did you end up using to record the audio? Mine always come out sounding robotic and unconvincing.

    ReplyDelete
    Replies
    1. I used the following website to record the audio: http://www.fromtexttospeech.com/ It translates text to audio and then allows you to download an mp3. There are several websites like this out there, I just liked the voices on this one the best.

      Delete
  4. Nice project, thanks for the write up. You may want to add a resistor between the GPIO and the base of the NPN to limit the current being drawn out of the GPIO pin. RPi outputs are not the most robust. It will work for a while without the resistor but could affect long term operation.

    ReplyDelete
    Replies
    1. Holy cow Dan, nice catch! When I originally bread-boarded the circuit I did have a resistor in there, but somehow it was forgotten when I soldered up the final circuit. I wanted to measure the current draw on the GPIO pin before I replied to your comment, and I was shocked... 39mA!!! I can't believe the GPIO pin supplied that without malfunctioning (I think the recommended max draw is 13mA). Moreover, I can't believe I didn't burn out the LED. Thanks for pointing that out. I added a 1k ohm resistor (soldered inline on the jumper wire) and now I'm pulling a respectable 2.5mA from the GPIO. I have also edited the blog post to include this information. Thanks again.

      Delete
  5. Loving it...i have similar script, it copies itself and makes entry in rc.local, only mine doesnt talk and doesnt abort. I instead have led that flashes certain pattern on shutdown and different on reboot, depends on how long button has been pressed for.
    Very nice and superb choice of button

    ReplyDelete
  6. Just an FYI, pulling pin 5 to ground will cause the Pi to boot. So you could use pin 5 as a soft-off button as well as an on button.

    ReplyDelete
    Replies
    1. This is a great point, and good advice to anyone trying to do a similar project. This was pointed out to me when I first posted this on Reddit, and I did test it to verify that it works. Unfortunately I have plans to use the I2C port for another project (pins 3 & 5), so that is not an option for me. Instead I soldered pin headers to the run holes on the Pi to add a boot button.

      Delete
    2. So you would just switch the pin and the code to pin 5 right? You wouldn't need to change the circuit at all right?

      Delete
    3. Yes, the circuit wouldn't change at all, you would just need to move the button input to pin 5. In my code, I used BCM numbering, so if you do the same, pin 5 is GPIO-3. You would only need to change "BUTTON_PIN = 3".

      Delete