בס״ד

Building a Command Line Alarm Clock for Linux with Bash

2015-06-30 10:37

I've often wanted my computer to act as my alarm. It would need to automatically resume from suspend at a specified time and begin playing music with gradually increasing volume.

The complete script can be found on GitHub, at this Gist

The Script, Explained Bit by Bit

Instead of dumping the entire script in one obnoxious block, I'm inclined to walk you through it, section by section, function by function. This, I should think, will provide a more interesting and educational read.

Default Values

Usually, we humans are fairly regular and scheduled (or should be), thus we begin our script with default values, as follows:

#!/bin/bash

__script_version="1.2"


#-----------------------------------------------------------------------
#  Default values
#-----------------------------------------------------------------------

human_time="6:20 tomorrow"
media_url="http://live.toker.fm:6060/"

In case Bash is new to you (maybe read some introductory materials first) variables are defined by a bare word follow immediately by an equal sign then immediately again by the value.

http://live.toker.fm:6060/ is the stream URL for the Internet radio station toker.fm

Gradual Volume

Here we have a function, which we'll call later, to gradually increase the volume. We'll call it using gradual & to ensure it isn't blocking and the script can continue while it runs.

#===  FUNCTION  ================================================================
#         NAME:  gradual
#  DESCRIPTION:  Increase the volume gradually to 100.
#===============================================================================
function gradual ()
{
	ponymix set-volume 10 > /dev/null

	current_volume=$(ponymix get-volume)
	while [[ current_volume -lt 100 ]]
	do
		sleep 0.5
		current_volume=$(ponymix increase 1)
	done

}    # ----------  end of function gradual  ----------

The gradual function is simple. We set the volume to 10%, then loop while the volume is less than 100%, increasing the volume 1% every half a second.

For those new to Bash, we use > /dev/null to do IO redirection and send the output to oblivion, otherwise known as the null device, which is conveniently accessible via the file located at /dev/null.

ponymix

In order to control the volume from the shell, we need a command line mixer program. I chose ponymix because I use PulseAudio and it was the first option I saw. You could also use pulseaudio-ctl.

If you use something other than PulseAudio, like Alsa, you'll need to figure that out on your own. Perhaps try this advice

Fallback

In the case where the media can't be played, for whatever reason, we use the fallback function to produce a series of jarring beeps and boops, sure to awaken you from even the deepest of slumbers.

#===  FUNCTION  ================================================================
#         NAME:  fallback
#  DESCRIPTION:  Play obnoxious speaker-test tones.
#===============================================================================
function fallback ()
{
	got_input=142

	until [ $got_input -eq 0 ]
	do
		frequency=$(shuf -i 200-600 -n 1)
		speaker-test -f $frequency -t sine -l 1 > /dev/null &

		disown $! # avoid useless kill messages

		duration=$(echo "$(shuf -i 5-25 -n 1) * 0.1" | bc)
		read -t $duration # play beep for .5 to 2.5 seconds
		got_input=$?

		pkill -9 --ns $$ speaker-test
	done

}    # ----------  end of function fallback  ----------

Fallback is the most complex code in this script, so bear with me, and if you grasp this, the rest will be calm waters.

We begin by acquiring a frequency with shuf. In this context, I am using shuf to shuffle a range of integers with the -i option, and pull out only the first one with the -n 1 option. This acts like a random number generator.

At this point we use the speaker-test command with the -t sine option to produce a tone at the given frequency. Boom we've got sound!

Now we disown the speaker-test process. Otherwise, when I kill -9 it, my shell agressively informs me the process has been axed, and I'm trying to avoid extraneous output.

By default, speaker-test tones play for far too long, so we need to generate a duration. We once again leverage shuf, but this time we want a floating point value. For that we use the bc command, piping in a string containing some multiplication.

Finally, we use that duration to pause waiting for user input. When we receive it, or the timer runs out, we agressively kill the running instances of "speaker-test" that belong to the current scope, by using the --ns $$ option.

If we didn't receive input, we try again with another irritating tone of a different frequency and duration.

Arguments

In case we don't want to use the default values, defined above, we include the option to pass the script explicit media and time values.

#===  FUNCTION  ================================================================
#         NAME:  usage
#  DESCRIPTION:  Display usage information.
#===============================================================================
function usage ()
{
	echo "Usage :  $0 [options] [--]

	Options:
	-h|help       Display this message
	-v|version    Display script version
	-m|media      The audio url, to be played by mpv
	-t|time       The human readable time and date for the alarm"

}    # ----------  end of function usage  ----------


#-----------------------------------------------------------------------
#  Handle command line arguments
#-----------------------------------------------------------------------

while getopts ":hvm:t:" opt
do
  case $opt in

	h|help     )  usage; exit 0   ;;

	v|version  )  echo "$0 -- version $__script_version"; exit 0   ;;

	m|media    )  media_url=$OPTARG   ;;

	t|time     )  human_time=$OPTARG   ;;

	* )  echo -e "\n  Option does not exist : $OPTARG\n"
		  usage; exit 1   ;;

  esac    # --- end of case ---
done
shift $(($OPTIND-1))

I honestly don't entirely understand this. It was mostly auto-completed for me by YouCompleteMe. If you want to know more about how getopts works, try this tutorial, or search on the Googles.

Suspend and Resume

For my purposes, I want my computer to sleep through the night, just like me. However, because time repeats, I need to make sure I know in exactly how long the alarm will sound.

#-----------------------------------------------------------------------
#  Suspend the machine, and wake at the given time
#-----------------------------------------------------------------------

unix_time=$(date +%s -d "$human_time")
seconds=$(($unix_time - $(date +%s)))

if [ $? -ne 0 ]
then
	exit 1
fi

hours_minutes="$(($seconds / 3600)) hours and $((($seconds / 60) % 60)) minutes"

read -p "Set alarm for $hours_minutes from now? [y/n] " go

if [ "$go" == n ]
then
	exit 0
fi

sudo rtcwake -m mem -t $unix_time > /dev/null
sleep 30 # give time to restore network connectivity

The line defining our unix_time variable, is one of the few places you'll notice > /dev/null is missing. This is intentional. I actually want any errors to print to the console before we exit 1. After all, for wrong media we have a fallback, but there's no fallback for wrong time.

The date command, for our purposes, takes a human readable date and time, like "7 tomorrow" or "8 hours", and transform it into Unix time, by using the +%s (seconds since 1970-01-01 00:00:00 UTC) format option.

We also need to make sure we are setting the alarm for the correct time. We do so by calculating the difference between the given time and now. We then display it in a readable hour minute format and request confirmation.

At this point we use rtcwake to suspend, and automatically resume, by leveraging the power of ACPI Wakeup. While this is a complex subject, using rtcwake is very simple.

It requires three things: to run as root, which we can accomplish using sudo; the type of suspend to activate, which for us is suspend to RAM (mem); and to time to awaken the computer, given in Unix time.

Playing Our Media

After executing the alarm, we'll want to restore the volume, so first we save it. Only afterward do we run our gradual function in the background.

#-----------------------------------------------------------------------
#  Set the volume and play our media
#-----------------------------------------------------------------------

saved_volume=$(ponymix get-volume)

gradual &
mpv --no-terminal $media_url &

We use mpv, a fork of mplayer with simplified command line ergonomics among other improvements, to play our media. Because we don't want any output we use --no-terminal which ensures no terminal output whatsoever.

Activating Fallback Mode

At this point we want to wait for one of two things to occur. Either for mpv to fail, or for user input. If mpv fails, we'll move to our fallback. The question becomes, how can we wait for both occurrences.

This evolved into an involved discussion on stackoverflow, wait for process to finish, or user input.

Basically we loop, and if we receive user input, we kill mpv, and we're done. Otherwise, if we don't receive user input, yet discover mpv is dead, we active our fallback.

#-----------------------------------------------------------------------
#  If mpv fails, use our fallback
#-----------------------------------------------------------------------

got_input=142

while true; do
	if read -t 1; then
		got_input=$?
		pkill --ns $$ mpv > /dev/null
		break
	elif ! pgrep --ns $$ mpv > /dev/null; then
		break
	fi
done

if [ $got_input -ne 0 ]
then
	gradual &
	fallback
fi

Wrapping Up

To finish, we reset our volume and greet the user with an overly chipper message, sure to annoy them into alertness.

ponymix set-volume $saved_volume > /dev/null

echo "Good Morning! ;)"

Enjoy!