Building a Command Line Alarm Clock for Linux with Bash

Author
Michael
Published
2015, Jun 30, 05:07 am
Category
TechLinux
Tags
Bash, CLI, Linux, Shell

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 other day I finally sat down and coded an alarm shell script for myself. I figured others might find it useful or instructive, so here it is.


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.0"

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

human_time="7 tomorrow"
media_url="http://178.33.191.197: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://178.33.191.197:6060 is the stream URL for the Internet radio station toker.fm

Gradual Volume

Here we have the 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  ----------

This function is dead simple. We set the volume to 10%, loop while the volume is less than 100%, increasing the volume 1% every half a second. Easy as stubbing your toe.

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 mplayer
    -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.

#-----------------------------------------------------------------------
#  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

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

saved_volume=$(ponymix get-volume)

gradual &
mplayer -noconsolecontrols -really-quiet -msglevel all=-1 -nolirc "$media_url" > /dev/null &

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.

By default, mplayer produces a ton of output. Therefore we use the options -really-quiet -msglevel all=-1 -nolirc to mute as much output as possible and shunt the rest to the garbage with > /dev/null. We also need the -noconsolecontrols option to "prevent MPlayer from reading key events from standard input."

Activating Fallback Mode

At this point we want to wait for one of two things to occur. Either for mplayer to fail, or for user input. If mplayer 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.

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

got_input=142

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

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

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

Wrapping Up

ponymix set-volume $saved_volume > /dev/null

echo "Good Morning! ;)"

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

Enjoy!



Comments


Add a comment