Saturday, 1 December 2018

KitchenPi: adding a watchdog

Although KitchenPi has been running well for over two and a half years, I do get the occasional blip.


A recent issue caused the system to fall silent.


But this turned into an opportunity to do a couple of simple software modifications.


KitchenPi is a headless internet radio/music player that I built early in 2016. It has just a volume control and a push-button which changes the radio station by simply stepping through a preset list. It is turned on/off by operating 'the power switch on the wall' where the power supply sits.

Raspberry Pi Gambas radio caroline internet radio


KitchenPi can run every day for maybe 8hrs/day, for days & days without problems. Or it could fall silent at any time, but almost always recovers when the power is cycled.

file corruption


Although just switching off the power to a Raspberry Pi is not a recommended mode of operation, I've had almost no problems with this on KitchenPi, largely because the system rarely writes to files. However, when the push-button is pressed, my software program changes to the next station in the list, and the new station number is written to a file. And it was during this button-press operation recently that I suddenly decided to switch the power off, resulting in a silent radio the next time it was switched on.

My Python code that reads this file when the program starts, looks like this:-

#get MusicSource from last time the program was run
file_source = open('/home/pi/LastSource','r')
MusicSource = int(file_source.read())
if MusicSource < 0:
MusicSource = 0 #default to JukeBox

When I checked the LastSource file after powering back up, I found the file was still there, but it was empty. So when the program tried to turn the file contents into an integer, an error caused the program to crash out.

I fiddled around for a while with the Python methods 'isdigit' and 'isinstance' but ended up with this modification:-

#get MusicSource from last time the program was run
file_source = open('/home/pi/LastSource','r')
try:
MusicSource = int(file_source.read())
except:
MusicSource = 0 #default to JukeBox

I tested this modification by deleting the contents of the LastSource file and all seem OK. But it will be interesting to see if my error trap has fully fixed this problem.

adding a watchdog


While I was in the mood, I decided to consider the drop-out problem. As it stands the Python program copes pretty well on those occasions when the internet radio stream is lost. As the mplayer process closes when this happens, its just a case of monitoring the process list and restarting mplayer if its not in the list.

#check whether player is still running
if LoopCount > 300:
LoopCount = 0
PlayerHasStopped = True
for proc in psutil.process_iter():
if proc.name() == PLAYER:
PlayerHasStopped = False
if PlayerHasStopped == True:
MusicSource = SelectSource(MusicSource)

But if my Python program shuts down, it goes unnoticed. When it does shut down, it is probably because an error has occurred. So maybe I should wrap the whole code in an error handler.

However, I decided to write another script (watchdog.sh) to check if the Python process was still running:-

#!/bin/bash
if pgrep  python >/dev/null; then
    echo "running"
else
    killall -r mplayer
    python /home/pi/kitchenPi.py 
fi

The killall command is important because my Python program may have crashed but left mplayer running. Without it, restarting my Python program will create a new instance of mplayer which just goes ahead and plays over the top of the old one!

I thought the echo command would be useful when I ssh into KitchenPi, but of course its useless. The program runs in a session owned by root, so when I use: ssh pi@{ipaddress} I can't see the message. Therefore this code could be cleaned up/simplified.

To run this script I just use a cron job. As my Python program is run by root, I create a cron job by editing (e) for user (u) root with this command from an ssh session:-

sudo crontab -u root -e

...and then add to the end of the file:-

* * * * * /home/watchdog.sh

...where watchdog.sh is the name of my script.

The 5 stars effectively tell the cron job to run every minute. Once again I tested this modification by killing the Python process from the command line. It seemed to behave as expected.

my Python program


Currently the full code for KitchenPi.py looks like this:-

#kitchenPi.py
#---------------
#Plays random music player (Juke Box) or selected internet radio stream
#Pressing button (input 7) switches to next source from list
#SteveDee
#1/4/16
#Last mod: 28-Nov-2018
#==========================================================================

import os
import psutil
#Import the time library
import time
#Import the Raspberry Pi library which controls the GPIO
import RPi.GPIO as GPIO

PLAYER = "mplayer"
MUSIC = "/home/pi/Music"

MusicSource = 0

def SelectSource(mSource):

#RADIO_CAROLINE = "http://sc5.radiocaroline.net:8010" #48k aacplus
RADIO_CAROLINE = "http://sc6.radiocaroline.net:8040/listen.pls" #128k
PLANET_ROCK = "http://tx.sharp-stream.com/icecast.php?i=planetrock.mp3" #112k
RADIO_TWO = "http://www.listenlive.eu/bbcradio2.m3u" #128k
#RADIO_FOUR = "http://www.listenlive.eu/bbcradio4.m3u" #128k
RADIO_FOUR = "http://a.files.bbci.co.uk/media/live/manifesto/audio/simulcast/hls/nonuk/sbr_low/ak/bbc_radio_fourfm.m3u8"
WORLD_SERVICE = "http://wsdownload.bbc.co.uk/worldservice/meta/live/shoutcast/mp3/eieuk.pls" #48k mp3

JukeBox = 0
RadioCaroline = 1
PlanetRock = 2
RadioTwo = 3
RadioFour = 4
WorldService = 5

if mSource > 5:
mSource = JukeBox
if mSource == RadioCaroline:
#start radio
os.system('espeak -ven+f4 -s110 -k5 -a 30 "Radio Caroline"')
#time.sleep(2)
os.system('mplayer -cache 64 -playlist ' + RADIO_CAROLINE + ' &')
elif mSource == PlanetRock:
#start radio
os.system('espeak -ven+m5 -s110 -k5 -a 30 "Planet Rock"')
os.system('mplayer -cache 64 ' + PLANET_ROCK + ' &')
elif mSource == RadioTwo:
#start radio
os.system('espeak -ven+f1 -s110 -k5 -a 30 "BBC Radio two"')
os.system('mplayer -cache 64 -playlist ' + RADIO_TWO + ' &')
elif mSource == RadioFour:
#start radio
os.system('espeak -ven+m1 -s110 -k5 -a 30 "BBC Radio Four"')
os.system('mplayer -cache 128 -playlist ' + RADIO_FOUR + ' &')
elif mSource == WorldService:
#start radio
os.system('espeak -ven+m3 -s110 -k5 -a 30 "The BBC world service"')
os.system('mplayer -cache 64 -playlist ' + WORLD_SERVICE + ' &')
else:
#start Juke-Box
os.system('espeak -ven+f2 -s110 -k9 -a 30 "play that funky music, White boy"')
os.system('mplayer -shuffle -playlist ' + MUSIC + '/playlist &')
return mSource


#Main

#Clear the current GPIO settings
GPIO.cleanup()
#Set mode to use Raspberry Pi pins numbers
GPIO.setmode(GPIO.BOARD)
#Set connector pin 7 to be an input
GPIO.setup(7,GPIO.IN)

#Set volume
os.system('amixer  sset PCM,0 100%')

#update JukeBox playlist
os.system('find '+ MUSIC + ' -type f -iname \*.ogg -o -iname \*.wma -o -iname \*.mp3 > ' + MUSIC + '/playlist')

#get MusicSource from last time the program was run
file_source = open('/home/pi/LastSource','r')
try:
MusicSource = int(file_source.read())
except:
MusicSource = 0 #default to JukeBox

MusicSource = SelectSource(MusicSource)
LoopCount = 0
PlayerHasStopped = False

#Create a LOOP that runs & runs (...until you shutdown)
while True:
LoopCount += 1
switch = GPIO.input(7)
#the switch is normally "high" (True)
if switch == False:
#music source change, so kill player
for proc in psutil.process_iter():
if proc.name() == PLAYER:
proc.kill()
MusicSource += 1
MusicSource = SelectSource(MusicSource)
#save to file
os.system('echo ' + str(MusicSource) + ' > /home/pi/LastSource')
#switch debounce
while switch == False:
time.sleep(0.5)
switch = GPIO.input(7)
#check whether player is still running
if LoopCount > 300:
LoopCount = 0
PlayerHasStopped = True
for proc in psutil.process_iter():
if proc.name() == PLAYER:
PlayerHasStopped = False
if PlayerHasStopped == True:
MusicSource = SelectSource(MusicSource)
#Reduce cpu load
time.sleep(0.2)








No comments:

Post a Comment