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