Tuesday, August 19, 2014

Infra-red remote controlling LMS on RPi

Acronym soup follows - but as a quick summary ... this is about controlling, via an infra-red remote control, the sounds from a Logitech Media Server.

I wanted to get a Creative Labs Soundblaster X-Fi Surround 5.1 Pro USB Sound Card (part number SB-1095 with remote RM-820) working with one Raspberry Pi that is set-up as a Squeezebox music server (LMS) and player (Squeezelite) and a different RPi working with the IQaudIO DAC and a generic Philips remote control.

Getting the sound working was pretty straight-forward but I also wanted to make use of the infra-red remote controls to at least control the volume and ideally do much more.

Why not simply use the web interface or a smartphone or tablet or a real Squeezebox to control it?
Well, they can be used of course and are needed to select what to play but leaving the simple remote near the player makes it very easy for someone to pause the music when the phone rings, skip a track or pump up the volume.

Getting it to work required pulling a few things together and then doing some custom scripting.

Most of this is not Raspberry Pi specific - it should work on nearly any Linux system. The exception is the installation of the IR receiver for the Philips (and in theory any other regular remote).
Note: there is no need to install an IR receiver for the Soundblaster because there is one built-in to the device and for this particular device this also works with the rotary control that is on top of the device (twist for volume and push for mute/pause).

Here is what it looks like. This is the RPI with the IQaudIO DAC installed and the lid removed from the IQaudIO case. The DAC is attached to the RPi in the top right.
Attached to that is the infra-red receiver - with the "eye" attached (using blue sticky stuff) so that a signal can get through the holes along the top edge of the IQaudIO case (you cannot see them),



So here is how I did it.

I decided to use the "expect" application to drive LMS because it has can send data in "telnet" style, as made available through the LMS CLI, and then take actions using a simple built-in scripting language based on the reply.

Text below that is in monospaced font and/or red shows what appears on the console or needs to be typed.

The full "expect" script is below. Some key points from it are:

  • This assumes that you have already installed Squeezelite and it has access to LMS either locally on same system or somewhere remote that can be accessed through the network (for example by installing SqueezePlug on RPi)
  • "expect" and possibly "telnet" might not be installed on your system. In which case you will have to install them yourself. On Debian-based systems (like Raspbian for Raspberry Pi) you can do this with
    sudo apt-get install expect telnet
  • If you are going to force actions via infra-red (IR) remote control then you will also need to install "lirc"
    sudo apt-get install lirc
  • You might not have the ALSA sound utilities installed. They are used to control some of the sound settings ... to install them you would do
    sudo apt-get install alsa-utils
  • Settings (such as where is the LMS server relative to this controlling client) are included in the file but they can also come from the settings file for Squeezelite ... in which case you probably would not have to edit this file at all.
  • Lines that start with "# " are comments. Some of them have some tracing statements to help if things are not working as expected. Simply remove the "#" from the front
    e.g.
    # puts "settings: host:\"$params(SBSHOST)\" and player MAC:\"$params(SLMAC)\""
    becomes
    puts "settings: host:\"$params(SBSHOST)\" and player MAC:\"$params(SLMAC)\""
    In this case this will output (puts = put string on console) the settings that were found
  • There are some special functions in there - such as treating the OK button as a special case (to restore things to a default configuration with middle volume, mute off, power on, shuffle off, repeat off) but they should be easy to understand simply by reading the script
  • Take care with line wrapping when copying the file below. It is up to you to work out where the blogging software has wrapped lines that should not have been wrapped. If you cannot do that then contact me directly and ask for a copy of the file to be sent by email to you
  • I stored this file as lmscli.exp in /home/pi/lmscli and performed a chmod on it
    i.e. I logged in as "pi" then
    mkdir lmscli
    cd lmscli

    (then copy the file and save as lmscli.exp)

    chmod +x lmscli.exp
    You can test this by hand like this:
    ./lmscli.exp pause
    You should see something like this if it worked (and any music that was playing via your local Squeezelite should have then paused or resumed)
    spawn telnet 127.0.0.1 9090
    Trying 127.0.0.1...

    Connected to 127.0.0.1.

    Escape character is '^]'.

    login user pass 
    b8:27:eb:aa:bb:cc

    pause

    exit

    login user ******

    Connection closed by foreign host.

    Note: the output above is slightly out of sequence but do not worry about it if it worked


Here is the lmscli.exp script (between but not including the separator lines):
------------------------------------------------------------------------------
#!/usr/bin/expect -f

# lmscli "Expect" script
# Author:       Paul Webster
# Version:      0.1
# Date: 29-Mar-2014
##
# Simple script (with no error checking) to send Logitech Media Server (SqueezeCenter) CLI commands to control a player
# Attempts to read parameters from Squeezelite defaults file - where syntax is
# keyword="value"
#
# "Expect" and telnet are required
# sudo apt-get install expect telnet
# Also - if this is to be driven via infrared remote (expected use) then also install lirc
# sudo apt-get install lirc
#

set settingsfile /etc/default/squeezelite

# Defaults
# Values here are overridden by values found in /etc/default/squeezelite (if present)
# SBSHOST = ip address of the LMS/Squeezebox Server
# SBSPORT = port number on LMS that cli runs on (rarely changed)
# SLMAC = the MAC address claimed by the player to be controlled - not always the real MAC address and is specified to Squeezelite
# SBSUSER / SBSPASS = username and password to access LMS. Usually not configured - in which case leave as is
# - if used then make sure that the user/pass fields are URL-encoded (e.g. %20 for space)
array set params {
    SBSHOST     127.0.0.1
    SBSPORT     9090
    SLMAC       00:11:22:33:44:55:66
    SBSUSER     user
    SBSPASS     pass
}

# Override the defaults that are in this script with the ones from the Squeezelite settings
if {[file exists "$settingsfile"]} {
    set fp [open "$settingsfile" r]
    set file_data [read $fp]
    close $fp

    # get lines
    set data [split $file_data "\n"]
    foreach line $data {
        #parse lines for config data
        if {[regexp {^(\w+)\s*=\s*[\"|](.*)[\"|]} $line -> name value]} {
            # puts "found name of \"$name\" with value \"$value\""
            set params($name) $value
        }
    }
}

# puts "settings: host:\"$params(SBSHOST)\" and player MAC:\"$params(SLMAC)\""
# The settings are now in place

set arg1 [lindex $argv 0]
set arg2 [lindex $argv 1]

# puts "recieved arguments of \"$arg1\" and \"$arg2\""
spawn telnet $params(SBSHOST) $params(SBSPORT)
expect "Escape character is *"
send "login $params(SBSUSER) $params(SBSPASS)\n"
expect "login user"


switch $arg1 {

pause   {
                send "$params(SLMAC) pause\n"
                expect "* pause"
        }

bb      {
                send "$params(SLMAC) time -10\n"
                expect "* time"
        }

ff      {
                send "$params(SLMAC) time +10\n"
                expect "* time"
        }

muteoff {
                send "$params(SLMAC) mixer muting 0\n"
                expect "* mixer muting"
        }

muteon  {
                send "$params(SLMAC) mixer muting 1\n"
                expect "* mixer muting"
        }

next    {
                send "$params(SLMAC) playlist index +1\n"
                expect "* playlist index"
        }

prev    {
                send "$params(SLMAC) playlist index -1\n"
                expect "* playlist index"
        }

power   {
                send "$params(SLMAC) power\n"
                expect "* power"
        }

voldown {
                send "$params(SLMAC) mixer volume -2.5\n"
                expect "* mixer volume"
        }

volup   {
                send "$params(SLMAC) mixer volume +2.5\n"
                expect "* mixer volume"
        }

shuffle {
                send "$params(SLMAC) playlist shuffle\n"
                expect "* playlist shuffle"
        }

repeat  {
                send "$params(SLMAC) playlist repeat\n"
                expect "* playlist repeat"
        }

stop    {
                send "$params(SLMAC) stop\n"
                expect "* stop"
        }

ok      {       #Restore to base values
                send "$params(SLMAC) playlist repeat 0 1\n"
                expect "* playlist repeat"
                send "$params(SLMAC) playlist shuffle 0\n"
                expect "* playlist shuffle"
                send "$params(SLMAC) mixer volume 50\n"
                expect "* mixer volume"
                send "$params(SLMAC) mixer muting 0\n"
                expect "* mixer muting"
                send "$params(SLMAC) power 1\n"
                expect "* power"
}

default {puts "Unknown command issued to lmscli"}

}
# End of switch (do not put on same line as the closing brace)

send "exit\n"
expect eof
------------------------------------------------------------------------------

Next step is to get the commands that are sent by infra-red to get sent to lmscli.
I have not covered all of the steps required to get lirc working (there are plenty of posts and FAQs about that elsewhere) - so all I am showing here are the configuration files to link lirc to the "expect" script.

This first file should be saved as "root" in /etc/lirc as lircrc
To be safe, make a copy of the file that is already there (if there is one).
This particular file is set-up to handle 2 different remote controls, "RM-820" and "philipsdvd".
Therefore you will see that many of the functions are repeated. By doing it this way it meant that I could have the same config file on two different RPi systems, making life easier for me.
The full file is below - some key points from it are:

  • the "remote =" line define which remote control this is referring to. It has to match the name used in other lirc config files (see more about that much further below)
  • the text after the "button =" has to exactly match what you have mapped the button presses to for the particular remote control - via the lirc config files (see more about that much further below)
  • I was still getting occasional lock-ups resulting in no sound coming out or sometimes a few digital burps and in worst case the Ethernet connection stops responding. So while trying to work out why that was happening (and I had tried what was the most recent official firmware and the work-in-progress FIQ handler at the time) I mapped one of the keys to a "reboot" command. I chose the long-back command on the RM-820 or the "scan" button on the Philips remote rather than Power for that as it very unlikely to be used accidentally and the Power key is mapped to the player power function on LMS because sometimes toggling the soft power button is enough to get things working again since Squeezelite releases something when it is told to power down. However, if you face similar problems and you do not have a button/function that you can readily map then you could edit the "#Power" section below to comment out the current "config =" line (by putting a "#" in front of it) and then put in "config = reboot". Applications/scripts that are invoked by lirc are run as "root" so you do not need to put "sudo" in front. This makes it very powerful and dangerous so take care
  • the volume is controlled through ALSA (sound libraries) directly. Therefore I set the command to change the volume via ALSA and also repeat the command to LMS so that it shows the volume change. However, these are not really matched up so it would be easy to have LMS show a very different volume to what is really set in ALSA. To help get around that potential issue I configured things to use the "OK" button to try to return this setting (and some others) to defaults. There is similar trickery in place for the "mute" function for the same reason. The ALSA settings are stored so that they can be restored when needed. The hint for some of this came from http://alsa.opensrc.org/Usb-audio and possibly other places that I found while hunting for a solution but have since forgotten.


------------------------------------------------------------------------------
# Power
begin
    prog = irexec
    remote = RM-820
    button = power
    config = /home/pi/lmscli/lmscli.exp power
    # config = reboot
end

begin
    prog = irexec
    remote = philipsdvd
    button = KEY_POWER
    repeat = 0
    config = /home/pi/lmscli/lmscli.exp power
end

# Hold the menu button down for a long time ... to try to force a reboot
begin
    prog = irexec
    remote = RM-820
    button = menu/back-long
    config = reboot
end

# On philipsdvd remote - there does not seem to be a long-hold ... so use the "scan" button - saved as SYSRQ
begin
    prog = irexec
    remote = philipsdvd
    button = KEY_SYSRQ
    repeat = 0
    config = reboot
end

# S51 Volume Knob
begin
    prog = irexec
    remote = RM-820
    button = knobvoldn
    repeat = 1
    config = amixer sset Master 1- ; /home/pi/lmscli/lmscli.exp voldown
end

begin
    prog = irexec
    remote = RM-820
    button = voldn
    repeat = 1
    config = amixer sset Master 1- ; /home/pi/lmscli/lmscli.exp voldown
end

begin
    prog = irexec
    remote = philipsdvd
    button = KEY_DOWN
    repeat = 1
    config = amixer sset Master 1- ; /home/pi/lmscli/lmscli.exp voldown
end

begin
    prog = irexec
    remote = RM-820
    button = knobMute
    repeat = 1
    config = if [ `amixer sget Master|grep "Front Left:"|awk '{print $3}'` -gt 0 ]; then alsactl store -f ~/.asound.state; amixer sset Master 0; amixer sset 'Power LED' off; /home/pi/lmscli/lmscli.exp muteon; else alsactl restore -f ~/.asound.state; amixer sset 'Power LED' on ; /home/pi/lmscli/lmscli.exp muteoff; fi
end

begin
    prog = irexec
    remote = RM-820
    button = mute
    repeat = 1
    config = if [ `amixer sget Master|grep "Front Left:"|awk '{print $3}'` -gt 0 ]; then alsactl store -f ~/.asound.state; amixer sset Master 0; amixer sset 'Power LED' off; /home/pi/lmscli/lmscli.exp muteon; else alsactl restore -f ~/.asound.state; amixer sset 'Power LED' on ; /home/pi/lmscli/lmscli.exp muteoff; fi
end

begin
    prog = irexec
    remote = philipsdvd
    button = KEY_AUDIO
    repeat = 1
    config = if [ `amixer sget Master|grep "Front Left:"|awk '{print $3}'` -gt 0 ]; then alsactl store -f ~/.asound.state; amixer sset Master 0;  /home/pi/lmscli/lmscli.exp muteon; else alsactl restore -f ~/.asound.state; /home/pi/lmscli/lmscli.exp muteoff; fi
end

begin
    prog = irexec
    remote = RM-820
    button = play-pause
    repeat = 1
    config = if [ `amixer sget Master|grep "Front Left:"|awk '{print $3}'` -gt 0 ]; then alsactl store -f ~/.asound.state; amixer sset Master 0; amixer sset 'Power LED' off; /home/pi/lmscli/lmscli.exp pause; else alsactl restore -f ~/.asound.state; amixer sset 'Power LED' on ; /home/pi/lmscli/lmscli.exp pause; fi
end

begin
    prog = irexec
    remote = philipsdvd
    button = KEY_PAUSE
    repeat = 0
    config = if [ `amixer sget Master|grep "Front Left:"|awk '{print $3}'` -gt 0 ]; then alsactl store -f ~/.asound.state; amixer sset Master 0; /home/pi/lmscli/lmscli.exp pause; else alsactl restore -f ~/.asound.state; /home/pi/lmscli/lmscli.exp pause; fi
end

begin
    prog = irexec
    remote = philipsdvd
    button = KEY_PLAY
    repeat = 0
    config = if [ `amixer sget Master|grep "Front Left:"|awk '{print $3}'` -gt 0 ]; then alsactl store -f ~/.asound.state; amixer sset Master 0; /home/pi/lmscli/lmscli.exp pause; else alsactl restore -f ~/.asound.state; /home/pi/lmscli/lmscli.exp pause; fi
end

begin
    prog = irexec
    remote = RM-820
    button = knobvolup
    repeat = 1
    config = amixer sset Master 1+ ; /home/pi/lmscli/lmscli.exp volup
end

begin
    prog = irexec
    remote = RM-820
    button = volup
    repeat = 1
    config = amixer sset Master 1+ ; /home/pi/lmscli/lmscli.exp volup
end

begin
    prog = irexec
    remote = philipsdvd
    button = KEY_UP
    repeat = 1
    config = amixer sset Master 1+ ; /home/pi/lmscli/lmscli.exp volup
end

begin
    prog = irexec
    remote = RM-820
    button = bb
    repeat = 1
    config = /home/pi/lmscli/lmscli.exp prev
end

begin
    prog = irexec
    remote = philipsdvd
    button = KEY_REWIND
    repeat = 0
    config = /home/pi/lmscli/lmscli.exp prev
end

begin
    prog = irexec
    remote = RM-820
    button = ff
    repeat = 1
    config = /home/pi/lmscli/lmscli.exp next
end

begin
    prog = irexec
    remote = philipsdvd
    button = KEY_FASTFORWARD
    repeat = 0
    config = /home/pi/lmscli/lmscli.exp next
end

begin
    prog = irexec
    remote = RM-820
    button = shuffle
    config = /home/pi/lmscli/lmscli.exp shuffle
end

begin
    prog = irexec
    remote = philipsdvd
    button = KEY_SHUFFLE
    repeat = 0
    config = /home/pi/lmscli/lmscli.exp shuffle
end

begin
    prog = irexec
    remote = RM-820
    button = repeat
    config = /home/pi/lmscli/lmscli.exp repeat
end

begin
    prog = irexec
    remote = philipsdvd
    button = KEY_MEDIA_REPEAT
    repeat = 0
    config = /home/pi/lmscli/lmscli.exp repeat
end

begin
    prog = irexec
    remote = philipsdvd
    button = KEY_MEDIA
    repeat = 0
    config = /home/pi/lmscli/lmscli.exp repeat
end

begin
    prog = irexec
    remote = RM-820
    button = right
    repeat = 1
    config = /home/pi/lmscli/lmscli.exp ff
end

begin
    prog = irexec
    remote = philipsdvd
    button = KEY_RIGHT
    repeat = 1
    config = /home/pi/lmscli/lmscli.exp ff
end

begin
    prog = irexec
    remote = RM-820
    button = left
    repeat = 1
    config = /home/pi/lmscli/lmscli.exp bb
end

begin
    prog = irexec
    remote = philipsdvd
    button = KEY_LEFT
    repeat = 1
    config = /home/pi/lmscli/lmscli.exp bb
end

begin
    prog = irexec
    remote = philipsdvd
    button = KEY_STOP
    repeat = 1
    config = /home/pi/lmscli/lmscli.exp stop
end

begin
    prog = irexec
    remote = RM-820
    button = ok
    config = /home/pi/lmscli/lmscli.exp ok
end

begin
    prog = irexec
    remote = philipsdvd
    button = KEY_OK
    repeat = 0
    config = /home/pi/lmscli/lmscli.exp ok
end

------------------------------------------------------------------------------

Next is the set-up of lirc to know what hardware is being used.
The file shown below is my config file from the RPi that has the Philips Remote control - which is getting the data via a simple IR receiver attached to GPIO pins on the RPi.
However, I have left the RM-820 set-up in there as well - but commented out. So if you are trying to get the Creative device to work then uncomment those lines and comment out the Philips ones.
This file is stored as "root" as /etc/lirc/hardware.conf
------------------------------------------------------------------------------
# /etc/lirc/hardware.conf
#
# Arguments which will be used when launching lircd
#LIRCD_ARGS=""
LIRCD_ARGS=""

#Don't start lircmd even if there seems to be a good config file
#START_LIRCMD=false

#Don't start irexec, even if a good config file seems to exist.
#START_IREXEC=false

#Try to load appropriate kernel modules
LOAD_MODULES=true

# Run "lircd --driver=help" for a list of supported drivers.
# Uncomment the line below (and comment out the one following it) for Creative Labs Soundblaster X-Fi USB DAC
#DRIVER="alsa_usb"
DRIVER="default"
# usually /dev/lirc0 is the correct setting for systems using udev
# Uncomment the line below (and comment out the one following it) for Creative Labs Soundblaster X-Fi USB DAC
#DEVICE="hw:Pro"
DEVICE="/dev/lirc0"
# Uncomment the line below (and comment out the one following it) for Creative Labs Soundblaster X-Fi USB DAC
#MODULES=""
MODULES="lirc_rpi"

# Default configuration files for your hardware if any
# Uncomment the line below (and comment out the one following it) for Creative Labs Soundblaster X-Fi USB DAC
#LIRCD_CONF="creative/lircd.conf.alsa_usb"
LIRCD_CONF="/home/pi/lircd-philipsdvd.conf"
LIRCMD_CONF=""

------------------------------------------------------------------------------

In this case you can see that I put the configuration file for the Philips remote into the /home/pi directory.
Here is the contents of that file.

------------------------------------------------------------------------------
# Please make this file available to others
# by sending it to
#
# this config file was automatically generated
# using lirc-0.9.0-pre1(default) on Wed Apr 16 18:37:15 2014
#
# contributed by
#
# brand:                       lircd-philipsdvd.conf
# model no. of remote control:
# devices being controlled by this remote:
#

begin remote

  name  philipsdvd
  bits            8
  flags RC6|CONST_LENGTH
  eps            30
  aeps          100

  header       2690   871
  one           462   422
  zero          462   422
  pre_data_bits   13
  pre_data       0xEFB
  gap          106190
  toggle_bit_mask 0x10000
  rc6_mask    0x10000

      begin codes
          KEY_POWER                0xF3
          KEY_1                    0xFE
          KEY_2                    0xFD
          KEY_3                    0xFC
          KEY_4                    0xFB
          KEY_5                    0xFA
          KEY_6                    0xF9
          KEY_7                    0xF8
          KEY_8                    0xF7
          KEY_9                    0xF6
          KEY_0                    0xFF
          KEY_BACK                 0x7C
          KEY_DISPLAYTOGGLE        0x10
          KEY_MENU                 0xAB
          KEY_CONTEXT_MENU         0x7D
          KEY_UP                   0xA7
          KEY_DOWN                 0xA6
          KEY_LEFT                 0xA5
          KEY_RIGHT                0xA4
          KEY_OK                   0xA3
          KEY_REWIND               0xDE
          KEY_FASTFORWARD          0xDF
          KEY_STOP                 0xCE
          KEY_PLAY                 0xD3
          KEY_PAUSE                0xCF
          KEY_SUBTITLE             0xB4
          KEY_ANGLE                0x7A
          KEY_ZOOM                 0x08
          KEY_AUDIO                0xB1
          KEY_MEDIA_REPEAT         0xE2
          KEY_MEDIA                0xC4
          KEY_SHUFFLE              0xE3
          KEY_SYSRQ                0xD5
      end codes

end remote
------------------------------------------------------------------------------


For the Creative device I used the default that came with lirc (as you can see from the commented out set-up in the hardware.conf).

For the particular IR module that I used I connected it to GPIO pin 23 - which is one of the pins that the IQaudIO device exports to its connectors on top of the card. I forced this pin to be used by adding the following lines to /etc/modules
lirc_dev
lirc_rpi gpio_in_pin=23
This might not be right for you - so you will need to check the documentation for the IR module that you are using to see how to do it for your particular device. Note - this is not needed for the Creative device because it has a built-in IR receiver and it passes the commands in through the USB connection not GPIO.


As always, if you have questions then ask it in the comments section below and I'll try to answer it.