Saturday 12 December 2020

Remote control of mpd on a Raspberry Pi


Remote control of mpd on a Raspberry Pi


I listen to music via a Raspberry Pi 4 running mpd, and which is connected to a Topping E30 DAC.  The DAC has a remote control (RC-15A) with 13 buttons, of which 4 are unused.  I control mpd via an Android app but if I need to quickly pause the music it's a bit of a faff to get the phone, unlock it, load the app and then press pause.  Same for skipping tracks.  I thought it might be convenient to use some of the remote's unused buttons to implement pause, prev/next track and rewinding 5 seconds into the currently playing track.

The approach I took should work with any infrared remote control.  You might even use a button that's in use but doing something non-disruptive to music playback, such as the display brightness. 

I read and experimented with a lot of the information on the internet about this and it wasted a lot of time as it seems most of it was written before a recent change in how IR is handled on the Pi.  Also, a lot of it is based on people knowing what remote they have, which seems to be either one which came with the IR sensor, or one from a major brand (Sony, Samsung etc) where the IR protocol is known.  This was not the case for the Topping remote I was interested in using.

These notes should be helpful no matter which app you want to control on the Pi.

Hardware

I have implemented this solution on the Raspberry Pi 3 and 4.

I used an IR (infrared) sensor with 1838 in the name; it seems there are several variations but they all look like they'll probably work. They're less than £2 each on eBay.  My one has "VS1838B" on the back, and looks like this:






You also need 3 Dupont female to female jumper wires to connect the IR sensor to the GPIO pins on the Pi; I used 30cm long ones without any problems:




 

From left to right the 3 pins on the IR sensor are: Data, Ground, Power, and you want to connect them to GPIO pins 12, 6 and 1 respectively.  On the Pi you can type:

$ pinout

to get a handy view of the pins. You want the red ones:

   3V3  (1) (2)  5V    
 GPIO2  (3) (4)  5V    
 GPIO3  (5) (6)  GND   
 GPIO4  (7) (8)  GPIO14
   GND  (9) (10) GPIO15
GPIO17 (11) (12) GPIO18
GPIO27 (13) (14) GND   
GPIO22 (15) (16) GPIO23
   3V3 (17) (18) GPIO24


This is the remote control:


I use the large round button to toggle pause, the headphone button/line out for previous/next track respectively and the m button to play the currently playing track from the beginning. You can connect the IR sensor before proceeding with the software installation/configuration.


Software

Currently I'm using moOde on the Raspberry Pi 4, but before that I used Raspbian 10 (buster). The instructions on this page are identical for both scenarios; it's mpd which is what's playing music, and it's that which needs to be controlled using the remote. I perform most of the control of mpd using the Android app (M.A.L.P.) directly - moOde doesn't really lend itself to sensible use via a web interface on a phone/tablet. I use mpc to perform the functionality I'm binding to the unused buttons on the remote.   As always with debian/Ubuntu, the official repos contain a really old, buggy version of mpd ("outdated and unsupported by this project") so get the latest one from here if you're not using moOde:


(The official repo version of mpc is fine though.)

You need to install some tools:

$ sudo apt-get install lirc ir-keytable mpc


Configure Remote

You need a .conf file for the Topping remote control. This file is used by lircd; it contains both the protocol (how the low level pulse are interpreted) and which codes the various keys produce when pressed.   I couldn't find this or anything similar on the internet, which meant that I had to use a tool to capture them.   I'd read that this process was a bit flaky and I found this to be the case; it required a fair bit of trial and error.  Eventually I produced the file though (I describe the process I used below); it needs to go here:


/etc/lirc/lircd.conf.d/topping.lircd.conf 


begin remote


  name topping


  bits       32

  flags      SPACE_ENC|CONST_LENGTH

  eps        30

  aeps       100

  header     8970  4566

  one        501  1748

  zero       501   628

  ptrail     507

  repeat     8974  2311

  gap        108261

  frequency  38000


  begin codes

     power        0x11EE18E7

     mute         0x11EE609F

     up           0x11EE629D

     down         0x11EE6897

     left         0x11EEE21D

     right        0x11EEA857

     pausetoggle  0x11EEAA55

     headphones   0x11EE20DF

     lineout      0x11EE02FD

     fir          0x11EE2AD5

     m            0x11EE0AF5

     auto         0x11EE08F7

     brightness   0x11EE28D7

   end codes


end remote


Delete the other files in that folder.


Configure Hardware

You need to tell the Pi you're going to use the relevant GPIO pin mentioned above.  I used GPIO18 (pin 12).  This is done in:

/boot/config.txt

in this line:


dtoverlay=gpio-ir,gpio_in_pin=18,gpio_in_pull=up

You need to reboot after changing config.txt.

Tell lirc to use the correct device/driver with the following lines (commenting out the previous values) in:

/etc/lirc/lirc_options.conf


#driver   = devinput

#device   = auto

driver    = default

device    = /dev/lirc0


Type:


$ sudo systemctl restart lircd


whenever you change that .conf file.


Map Buttons to Actions

When you press a button on the remote you typically want something to happen. The system used for this mapping is irexec. You have good control of what happens; you can asynchronously (only!) launch any code.  The mapping of a button on the remote to the command to execute is configured here:

/etc/lirc/irexec.lircrc 


begin

prog = irexec

button = pausetoggle  

config = mpc toggle

end

begin

prog = irexec

button = headphones  

config = mpc prev

end

begin

prog = irexec

button = lineout  

config = mpc next

end

begin prog = irexec button = m

config = mpc seek -00:00:05 

end


There'll be an existing systemd service called irexec.service, here:

/lib/systemd/system/irexec.service 

Mine looks like this (without comments):

[Unit]
Documentation=man:irexec(1)
Documentation=http://lirc.org/html/configure.html
Documentation=http://lirc.org/html/configure.html#lircrc_format
Description=Handle events from IR remotes decoded by lircd(8)

[Service]
Type=simple
ExecStart=/usr/bin/irexec  /etc/lirc/irexec.lircrc

[Install]
WantedBy=multi-user.target

(Note that although the docs say "By default, no logging is done" this isn't correct and it'll spam /var/log/syslog and /var/log/daemon.log with 3 lines for each button invoked.  Setting logging to error won't change this, either.)

To enable it, type:

$ sudo systemctl enable irexec.service


Whenever you change a systemd .service file you have to type:


$ sudo systemctl daemon-reload

$ sudo systemctl restart irexec.service


Make sure the following services are running:

$ sudo systemctl --state=running | grep lirc

irexec.service      loaded active running Handle events from IR remotes decoded by lircd(8)  
lircd.service       loaded active running Flexible IR remote input/output application support
lircd.socket        loaded active running lircd.socket                                       

You should now have a working remote!

Note that you can create .conf files for more than one remote.  Make sure they are all in:

/etc/lirc/lircd.conf.d/

with unique names for the buttons, and that your lircrc file - which if you remember is stored here:

/etc/lirc/irexec.lircrc 

has mappings for the button to the functions you want performed.


Capturing the protocol used by the remote

I recorded the remote with:


$ sudo irrecord -n -d /dev/lirc0 ~/lircd.conf


I had to do this about 20 times. Sometimes it didn’t like the button pressing process and gave up. Sometimes it produced a file with zeros for the relevant values. When it finally produced sensible values I found, through trial and error and looking at valid files, that it had produced a second number for each key which needed to be removed. That said, I'm not sure how you'd produce a .conf file for lirc for a random remote without this.



Diagnosing problems

To test if the the IR sensor is working, connected correctly, that config.txt is configured correctly and that lirc is running, you can type:

$ cat /dev/lirc0  | hd


Then press buttons on the remote. You should see a bunch of random looking numbers for each press.


A similar test is:

$ sudo mode2 --device /dev/lirc0


which gives you data like this:


pulse 882

space 96092

pulse 9333

space 1928

pulse 793

space 96174



Testing whether the .conf is configuring button presses prior to irexec handling them is done via:

$ irw
0000000011ee629d 00 up topping
0000000011ee629d 01 up topping
0000000011ee629d 02 up topping
0000000011ee629d 00 up topping
0000000011ee629d 01 up topping
0000000011ee6897 00 down topping
0000000011eee21d 00 left topping


Another tool I used in this process:

$ sudo ir-keytable -v -t -p nec
Found device /sys/class/rc/rc0/
Parsing uevent /sys/class/rc/rc0/lirc0/ueven
/sys/class/rc/rc0/lirc0/uevent uevent MAJOR=251
/sys/class/rc/rc0/lirc0/uevent uevent MINOR=0
/sys/class/rc/rc0/lirc0/uevent uevent DEVNAME=lirc0
Input sysfs node is /sys/class/rc/rc0/input0/
Event sysfs node is /sys/class/rc/rc0/input0/event0/
Parsing uevent /sys/class/rc/rc0/input0/event0/uevent
/sys/class/rc/rc0/input0/event0/uevent uevent MAJOR=13
/sys/class/rc/rc0/input0/event0/uevent uevent MINOR=64
/sys/class/rc/rc0/input0/event0/uevent uevent DEVNAME=input/event0
Parsing uevent /sys/class/rc/rc0/uevent
/sys/class/rc/rc0/uevent uevent NAME=rc-rc6-mce
/sys/class/rc/rc0/uevent uevent DRV_NAME=gpio_ir_recv
/sys/class/rc/rc0/uevent uevent DEV_NAME=gpio_ir_recv
input device is /dev/input/event0
/sys/class/rc/rc0/protocols protocol rc-5 (disabled)
/sys/class/rc/rc0/protocols protocol nec (disabled)
/sys/class/rc/rc0/protocols protocol rc-6 (disabled)
/sys/class/rc/rc0/protocols protocol jvc (disabled)
/sys/class/rc/rc0/protocols protocol sony (disabled)
/sys/class/rc/rc0/protocols protocol rc-5-sz (disabled)
/sys/class/rc/rc0/protocols protocol sanyo (disabled)
/sys/class/rc/rc0/protocols protocol sharp (disabled)
/sys/class/rc/rc0/protocols protocol mce_kbd (disabled)
/sys/class/rc/rc0/protocols protocol xmp (disabled)
/sys/class/rc/rc0/protocols protocol imon (disabled)
/sys/class/rc/rc0/protocols protocol rc-mm (disabled)
/sys/class/rc/rc0/protocols protocol lirc (enabled)
Opening /dev/input/event0
Input Protocol version: 0x00010001
BPF protocols removed
Protocols changed to nec 
Testing events. Please, press CTRL-C to abort.
6439.150112: lirc protocol(nec): scancode = 0x8855
6439.150136: event type EV_MSC(0x04): scancode = 0x8855
6439.150136: event type EV_SYN(0x00).
6440.510074: lirc protocol(nec): scancode = 0x8855
6440.510100: event type EV_MSC(0x04): scancode = 0x8855
6440.510100: event type EV_SYN(0x00).
6440.560189: lirc protocol(nec): scancode = 0x8855 repeat
6440.560213: event type EV_MSC(0x04): scancode = 0x8855
6440.560213: event type EV_SYN(0x00).
6442.200109: lirc protocol(nec): scancode = 0x8840
6442.200148: event type EV_MSC(0x04): scancode = 0x8840
6442.200148: event type EV_SYN(0x00).
6445.970063: lirc protocol(nec): scancode = 0x8855
6445.970093: event type EV_MSC(0x04): scancode = 0x8855
6445.970093: event type EV_SYN(0x00).
6446.020078: lirc protocol(nec): scancode = 0x8855 repeat
6446.020113: event type EV_MSC(0x04): scancode = 0x8855
6446.020113: event type EV_SYN(0x00).

Shows keypresses in a slightly less raw format. But after a while this gets laggy, shows the output from previous button presses as if it’s not flushing the output quickly enough.  But it’s maybe useful for showing the protocols supported by the kernel, and which are enabled (nec in my case).  If you were going to use a different remote perhaps you could specify them instead of nec with the -p parameter.




Notes

  • Don't be tempted to remove the metal over the front of the sensor - it's a Faraday cage as the sensor is susceptible to RFI.  
  • During work on this I occasionally thought there was a problem with the sensor. It turns out that pointing the remote directly at the sensor when very close to it is actually trickier than pointing at it from further away, or even at the wall opposite.  There are two reasons for this: the field of view of the sensor is rather narrow, and the remote control transmitter signal points very slightly upwards, meaning you have to point a bit lower than directly at it.  At normal distances this isn't really a problem. 
  • Hypothetically one could short the IR leads against the metal case of some equipment, instantly rebooting the Pi, if one wasn't careful.
  • I never used modprobe at any point.
  • One thing I would have liked to do would be to have the headphone/line out buttons work as prev/next when tapped, and rewind/forward when held. Maybe that's possible from an irexec config point of view, if one was proficient enough with repeat, delay and mode instructions. Unfortunately, just rewind/forward (using mpc seek -/+00:00:05, for example) runs into problems, presumably related to how mpd is buffering the track being played. You can skip 30 seconds or so into the future, but then the audio stops, I guess because it's not getting the music data quickly enough (I'm listening to flac files on an USB HD). And you can't hear any music when rewinding. I tried making the mpd buffer larger but that didn't work either.



Remote control of mpd on a Raspberry Pi