Related to my recent IoT hacking, what started me down this path is the long term annoyance of my X10 lighting being unreliable. X10 has always been problematic due to it’s use of power line communication, this has gotten worse as we add more and more noisy electronic devices that cause additional feedback onto the house wiring.
With the X10 light switch I had an IR-543 which mapped IR (infra red) and the rest of my home theater gear is all IR controlled, so a single remote could control everything including the lights. Another nice feature of the X10 light switch I had was soft on / soft off – meaning that when you turned the lights off they would dim down to off, and the same for on. At the start of a movie this is pretty nice.
Of course with a wifi enabled light switch, how do I get IR control? This seemed like a good reason to DIY a solution and build an IR controller / repeater based on a Raspberry Pi. I found that it’s relatively easy to control Tasmota devices with curl, so I was able to easily turn the lights on or off using a simple program. I was pleased to discover that the new light switch also had the soft on / soft off behaviour.
To build an IR device on Linux, I first thought of LIRC as I’ve used this in the past. As I dug deeper, it seems the LIRC project is quite dormant and I was fighting with a lot of stale tooling. I was succeeding in getting something working with the various remotes I wanted to use but it felt like it was a lot of work. Then a friend mentioned ir-keytable to me which led me to the more modern IR control in Linux solution.
The short version of the story is that the ir-keytable support is in a similar state as the LIRC work. I believe this boils down to the fact that IR control is still very niche, and there are lots of hardware variables due to many different remote controls. If you want to do something simple: receive IR input to control a linux machine, then ir-keytable is the way to go. More complex situations may require LIRC. Both approaches have their challenges but ir-keytable is the more modern solution.
The rest of this article will be about getting ir-keytable going on Raspberry Pi OS with a TSOP4838 IR receiver. For my application I have a more complex set of requirements so I’ll be continuing with an LIRC based solution, but more on that another time.
For the hardware setup, and more details on LIRC I used this as a reference. For IR receiving we only need to wire up the TSOP4838 and can ignore the resistors and transistor, you can also avoid using a button cell and pull 3.3V directly from the Pi.
Once you’ve got things wired up, then you need to modify /boot/config.txt
to enable the kernel support
1 |
dtoverlay=gpio-ir,gpio_pin=17 |
Of course, if you used a different gpio pin, specify that instead. The easy way to test this is to reboot so the kernel is reconfigured. You can check things are setup right by looking at the loaded modules. There should also be a /dev/lirc0
device now.
1 2 |
$ lsmod | grep gpio gpio_ir_recv 16384 0 |
Assuming you haven’t installed lirc (ie: this is a clean OS image) we can simply install ir-keytable and start playing with things. If you have lirc installed, first remove it.
1 |
$ sudo apt install ir-keytable |
Now just running ir-keytable should tell us some stuff about it’s configuration and state
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
$ ir-keytable Found /sys/class/rc/rc1/ with: Name: vc4 Driver: cec Default keymap: rc-cec Input device: /dev/input/event1 Supported kernel protocols: cec Enabled kernel protocols: cec bus: 30, vendor/product: 0000:0000, version: 0x0001 Repeat delay = 0 ms, repeat period = 125 ms Found /sys/class/rc/rc0/ with: Name: gpio_ir_recv Driver: gpio_ir_recv Default keymap: rc-rc6-mce Input device: /dev/input/event0 LIRC device: /dev/lirc0 Attached BPF protocols: Operation not permitted Supported kernel protocols: lirc rc-5 rc-5-sz jvc sony nec sanyo mce_kbd rc-6 sharp xmp imon Enabled kernel protocols: lirc bus: 25, vendor/product: 0001:0001, version: 0x0100 Repeat delay = 500 ms, repeat period = 125 ms |
The CEC section is due to the HDMI support that the Raspberry Pi has. If I were trying to control a more modern system that is interconnected with HDMI cables. I think this is probably the future of audio/video automation but my equipment stack pre-dates solid CEC support. We care about the rc0
section that shows our new IR receiver.
I have what I call a ‘slim remote’ which is one of these credit card sized remote controls which runs off a button cell. This is what I’m using to test things with. When I tried to test the ir-keytable
install with this remote, nothing happened.
Enabling all of the protocols got me sorted out.
1 2 |
$ sudo ir-keytable -p all $ ir-keytable -t -s rc0 |
Now when I press the buttons on my remote I’m seeing button down / button up events flying by. I see also that repeats happen fairly quickly. I don’t see any keydown events, only scancodes. The enablement of ir-keytable -p all
also does not survive a reboot so we have a few issues to address.
There are two files I need to customize to get things sorted out. I need a NNN.toml
file in /etc/rc_keymaps/
and an entry in /etc/rc_maps.cfg
to point at that file. A great way to get started on that is to figure out which of the existing config files are helping me see events when I enable all protocols.
There are a bunch of keymap files in /lib/udev/rc_keymaps/
– these are used when we enable all protocols. We need to figure out which one we should copy and modify.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
pi@raspberrypi:/lib/udev/rc_keymaps $ ir-keytable -t -s rc0 Testing events. Please, press CTRL-C to abort. 596.500115: lirc protocol(nec): scancode = 0x8012 596.500164: event type EV_MSC(0x04): scancode = 0x8012 596.500164: event type EV_SYN(0x00). 596.560099: lirc protocol(nec): scancode = 0x8012 repeat 596.560160: event type EV_MSC(0x04): scancode = 0x8012 596.560160: event type EV_SYN(0x00). 598.350108: lirc protocol(nec): scancode = 0x8002 598.350158: event type EV_MSC(0x04): scancode = 0x8002 598.350158: event type EV_SYN(0x00). 598.410097: lirc protocol(nec): scancode = 0x8002 repeat 598.410154: event type EV_MSC(0x04): scancode = 0x8002 598.410154: event type EV_SYN(0x00). ^C pi@raspberrypi:/lib/udev/rc_keymaps $ grep 0x8012 * dibusb.toml:0x8012 = "KEY_RIGHT" dtt200u.toml:0x8012 = "KEY_POWER" grep: protocols: Is a directory terratec_slim_2.toml:0x8012 = "KEY_POWER2" roo@iridium:/lib/udev/rc_keymaps $ grep 0x8002 * dibusb.toml:0x8002 = "KEY_HOME" dtt200u.toml:0x8002 = "KEY_CHANNELDOWN" grep: protocols: Is a directory terratec_slim_2.toml:0x8002 = "KEY_VOLUMEDOWN" |
Capture a couple of buttons using the test mode, remember which buttons we pressed and in which order. Then grep for the scancode in /lib/udev/rc_keymaps/
and we should find the right file to copy.
In my case it was terratec_slim_2.toml
– let’s copy that into /etc/rc_keymaps/
and rename it to slim-remote.toml
. Then we will add an entry to /etc/rc_maps.cfg
that looks like:
1 |
* * slim-remote.toml |
This should result in the remote being recognized after a reboot. At this point I discovered something interesting, the power button on my remote causes the Pi to power off. While this is sort of useful, it’s not what I wanted. Also, if you look backĀ at when we first ran just ir-keytable
you’ll notice that there is Default keymap: rc-rc6-mce
– this is causing the system to pull in another remote definition based on the entry in /etc/rc_maps.cfg
, and from what I can tell the file is loaded from the /lib/udev/rc_keymaps/
directory. If you were using one of the popular rc6-mce remotes, this would be magical and useful.
I wasn’t able to understand how to give my new rc_keymap a table name, thus the second * in my rc_maps.cfg entry. Ideally I’d give it a table name, and then I’d be able to modify the kernel overlay to specify my keymap as the default. Oh well, challenges for another day. We can remove the rc-rc6-mce in one of two ways.
- Remove the
rc-rc6-mce
entry from the/etc/rc_maps.cfg
file - Specify a bogus default keymap
For the latter – we change the /boot/config.txt
entry to read
1 |
dtoverlay=gpio-ir,gpio_pin=17,rc-map-name=bogus |
Now we’ll get an empty default keymap. Good enough. Only the keymap we created is being picked up.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
$ ir-keytable -r scancode 0x8001 = KEY_MUTE (0x71) scancode 0x8002 = KEY_VOLUMEDOWN (0x72) scancode 0x8003 = KEY_CHANNELDOWN (0x193) scancode 0x8004 = KEY_NUMERIC_1 (0x201) scancode 0x8005 = KEY_NUMERIC_2 (0x202) scancode 0x8006 = KEY_NUMERIC_3 (0x203) scancode 0x8007 = KEY_NUMERIC_4 (0x204) scancode 0x8008 = KEY_NUMERIC_5 (0x205) scancode 0x8009 = KEY_NUMERIC_6 (0x206) scancode 0x800a = KEY_NUMERIC_7 (0x207) scancode 0x800c = KEY_FULL_SCREEN (0x174) scancode 0x800d = KEY_NUMERIC_0 (0x200) scancode 0x800e = KEY_AGAIN (0x81) scancode 0x8012 = KEY_TOUCHPAD_TOGGLE (0x212) scancode 0x801a = KEY_VOLUMEUP (0x73) scancode 0x801b = KEY_NUMERIC_8 (0x208) scancode 0x801e = KEY_CHANNELUP (0x192) scancode 0x801f = KEY_NUMERIC_9 (0x209) |
With the loss of the default rc6-mce remote, I no longer had KEY_POWER
defined so that fixed my situation of having the machine power off. I wanted to be able to still get the button event, so I added an entry for scancodeĀ 0x8012
. Of course, as soon as I added KEY_POWER
the button was recognized, but the system would power off.
As an aside – all of the scan codes must map to one of the known codes. This list can be found in the linux kernel source. You will also notice that while you might have KEY_ZOOM
in your definition file, it’s an alias for KEY_FULL_SCREEN
which is what we see in the dump above.
I tried mapping scancode 0x8012
to KEY_POWER2
but that also had the same power off behaviour. This seems to be baked into the kernel? I gave up digging and just mapped the button to something that wouldn’t power off an picked KEY_TOUCHPAD_TOGGLE
which allows me to read the button, but has no side effect.
You can support multiple keymaps in /etc/rc_keymaps/
and as long as you have entries that point to them in /etc/rc_maps.cfg
they’ll get picked up. I was able to have two remotes both recognized without any problems. I found a good write up on generating new keymaps, testing, them and even some python code to read the events.
Unfortunately for my needs, it seems the events do not include which keymap defines them. This makes it difficult for any code I’d write to understand which remote was sending the codes. I could rely on unique KEY_CCC
mappings per remote, but since this is constricted by the list of codes in the kernel things start getting very tricky.
I finally decided to take the advice from the LIRC documentation: Why should I use LIRC? In my case I want to build a receive which will take one remotes commands, and map them to many remotes output AND support simply forwarding multiple remotes signals and boosting them. Thus it’s both a universal remote and a signal booster. Using ir-keytable would be possible for the universal remote case, but as a signal booster I need to know which remote is sending codes so I can replicate sending the same codes.
LIRC isn’t dead, it’s just not the best tool for most users who simply want to have basic control of their linux machines over IR.
References I used in creating this post that were not directly linked above
- https://blog.gordonturner.com/2020/05/31/raspberry-pi-ir-receiver/
- https://blog.gordonturner.com/2020/06/10/raspberry-pi-ir-transmitter/
- https://vaughanharper.com/2020/08/12/configuring-an-infrared-remote-control-to-control-runeaudio-archlinux-without-needing-lirc/
- https://raspberrypi.stackexchange.com/questions/99264/read-tv-remote-ir-codes-for-building-a-rpi3-powered-remote
Well – after much beating my head against the problem of getting LIRC to be stable for me, I’ve returned to ir-keytable and it seems like the way to get stable/reliable behaviour.
More on this as I pull the complete solution together.