Roku TV remote button presses register as duplicate events

I just got into coreelec and I wanted to get a roku IR remote working because it’s my personal favorite. It’s kind of an odd case though, because instead of having one command code per button, it actually has two. When you tap a button (as in press and immediately release) let’s say it sends 0x01. That’s all well and good. But when you hold a button, even for just a fraction of a second, it sends out 0x01 first…and then instead of constantly sending 0x01, it does the same but for 0x81, the same command but with the MSB set high. If pressing a button sent one command, and holding a button sent a different command, things would be fine, since you can just map multiple commands to the same event. But after doing that, since it’s always gonna send out 0x01 before the stream of 0x81, it registers as you pressing the button once, then immediately releasing before pressing and holding again. Really nasty. Take a look.

677.925248: event type EV_MSC(0x04): scancode = 0xeac72a
677.925248: event type EV_KEY(0x01) key_down: KEY_ENTER(0x001c)
677.925248: event type EV_SYN(0x00).
678.033119: event type EV_KEY(0x01) key_up: KEY_ENTER(0x001c)
678.033119: event type EV_MSC(0x04): scancode = 0xeac7aa
678.033119: event type EV_KEY(0x01) key_down: KEY_ENTER(0x001c)
678.033119: event type EV_SYN(0x00).
678.141031: event type EV_MSC(0x04): scancode = 0xeac7aa
678.141031: event type EV_SYN(0x00).
678.248924: event type EV_MSC(0x04): scancode = 0xeac7aa
678.248924: event type EV_SYN(0x00).
678.356861: event type EV_MSC(0x04): scancode = 0xeac7aa
678.356861: event type EV_SYN(0x00).
678.552037: event type EV_MSC(0x04): scancode = 0xeac7aa
678.552037: event type EV_SYN(0x00).
678.812025: event type EV_KEY(0x01) key_up: KEY_ENTER(0x001c)
678.812025: event type EV_SYN(0x00).

This is holding down the OK (enter) button for a second or so. I know about the remote wakeup mask. I don’t know what it does. But if anyone knows how to apply a mask to every scancode and exclude the MSB(s), that should solve the problem completely. Oh, and here’s my toml if you’re curious.

[[protocols]]
name = "Roku TV Remote"
protocol = "nec"
variant = "necx"
[protocols.scancodes]
0xeac717 = "KEY_POWER"
0xeac766 = "KEY_BACK"
0xeac703 = "KEY_HOME"
0xeac719 = "KEY_UP"
0xeac733 = "KEY_DOWN"
0xeac71e = "KEY_LEFT"
0xeac72d = "KEY_RIGHT"
0xeac72a = "KEY_ENTER"
0xeac778 = "KEY_RESTART"
0xeac761 = "KEY_CONTEXT_MENU"
0xeac734 = "KEY_REWIND"
0xeac74c = "KEY_PLAYPAUSE"
0xeac755 = "KEY_FASTFORWARD"
0xeac70f = "KEY_VOLUMEUP"
0xeac710 = "KEY_VOLUMEDOWN"
0xeac720 = "KEY_MUTE"
0xeac752 = "KEY_PROG1"
0xeac706 = "KEY_PROG2"
0xeac74d = "KEY_PROG3"
0xeac76c = "KEY_PROG4"

0xeac797 = "KEY_POWER"
0xeac7e6 = "KEY_BACK"
0xeac783 = "KEY_HOME"
0xeac799 = "KEY_UP"
0xeac7b3 = "KEY_DOWN"
0xeac79e = "KEY_LEFT"
0xeac7ad = "KEY_RIGHT"
0xeac7aa = "KEY_ENTER"
0xeac7f8 = "KEY_RESTART"
0xeac7e1 = "KEY_CONTEXT_MENU"
0xeac7b4 = "KEY_REWIND"
0xeac7cc = "KEY_PLAYPAUSE"
0xeac7d5 = "KEY_FASTFORWARD"
0xeac78f = "KEY_VOLUMEUP"
0xeac790 = "KEY_VOLUMEDOWN"
0xeac7a0 = "KEY_MUTE"
0xeac7d2 = "KEY_PROG1"
0xeac786 = "KEY_PROG2"
0xeac7cd = "KEY_PROG3"
0xeac7dc = "KEY_PROG4"

if this functionality doesn’t exist at all I think I’ll have to learn what a BPF is

Have you tried with kodi key mapper app to see what happens (it makes an gem.xml file that can overide programmed presses. )

Ive used it a few times when I cant make a key work from original function.

Use AmRemote format, it include more parameter like:

repeat_enable   = 1
release_delay	= 150
1 Like

yeah, it’s still gonna generate multiple events per single press so there isn’t really anything you can do

1 Like

Maybe this will help. It mentions that toml supports decoding raw IR pulses after kernel 5.3, so maybe it’s working in CE-NO (5.15)? With raw pulses you could define a button as a combinations of two codes, or however you like.

The command mode2 will show you the raw pulse times for any button press to help you out. IrScrutinizer may be a useful tool in the process. The raw pulses can be pasted in that app to show you a graphical representation as well as providing many other IR tools.

I haven’t tried anything yet, but I got a much newer roku tv remote and for some reason it sends scancode 0xcf30c0e1 whenever you release the OK button or one of the four shortcut buttons. Nothing else

This topic was automatically closed 91 days after the last reply. New replies are no longer allowed.

Solved. (mostly)
I booted from microsd into CE 22 (up from 21) on that box, the GT King, and there the roku remote works like a million times better (more snappy). No idea why, I had played with the timings a bunch before without any results so it must be something more obscure. Anyway, I was still curious about writing a bpf decoder to utilize the changes in scancode to allow for more rapid presses. I am ashamed to admit this but I had Claude Code take care of it. I’m supposed to hate AI but he wrote it up the logic like 5 minutes, and got the bpf-specific intricacies in order afterward. I couldn’t seem to get it to work on the CE 21 so I booted LibreELEC off of a microsd and was able to load the bpf + toml from there. And it is ridiculous how well it works. It can keep up with sequential button presses about as fast as I can comfortably mash the buttons with one hand. It’s able to exploit the press-bit behavior of the roku remote to emulate the remote protocols that flip a certain bit for each button press for more accuracy. And pressing and holding works too! So then I tried CE 22, expecting it to work great, but it looks like the required kernel config options to facilitate this are not set? Claude says you gotta set CONFIG_RC_CORE=y and CONFIG_BPF_LIRC_MODE2=y. I don’t know about contributing to the CE kernels so I hope a developer can see this and take care of it :crossed_fingers:. I will probably still have to use libreelec instead, since they have mainline with the composite/cvbs video output, which seems to be missing on CE 22, or at least nothing I tried could get anything more than the Beelink splash screen displayed.

Here’s the code you need, keeping in mind I haven’t touched this.

// SPDX-License-Identifier: CC0-1.0
/*
 * roku_nec.bpf.c — BPF IR decoder for Roku TV remotes (NECx protocol)
 *
 * Roku remotes use NEC-extended encoding but set bit 7 of the command byte
 * on hold-repeat frames instead of sending standard NEC repeat codes.
 * This causes rc-core to see alternating scancodes (e.g. 0xeac72a vs
 * 0xeac7aa) which produces spurious key-up/key-down pairs mid-hold.
 *
 * This decoder masks out the toggle bit and uses rc-core's toggle mechanism
 * to cleanly distinguish new presses from held buttons:
 *   - bit 7 clear (0x2a) → new press  → flip toggle → key_down
 *   - bit 7 set   (0xaa) → hold       → same toggle → repeat
 *
 * Non-Roku NEC remotes are passed through unmodified.
 *
 * Compile:
 *   clang --target=bpf -O2 -c roku_nec.bpf.c -o roku_nec.bpf.o
 *
 * Load (disables built-in decoders, loads this + keymap):
 *   ir-keytable -c -p roku_nec.bpf.o -w roku.toml -s rc0
 */

typedef unsigned int __u32;
typedef unsigned long long __u64;

/* BPF infrastructure */
#define SEC(name) __attribute__((section(name), used))

/* BPF helper functions (by stable ID) */
static void *(*bpf_map_lookup_elem)(void *map, const void *key) = (void *)1;
static long (*bpf_rc_repeat)(void *ctx)                          = (void *)77;
static long (*bpf_rc_keydown)(void *ctx, __u32 protocol,
                              __u64 scancode, __u32 toggle)      = (void *)78;

/* RC protocol IDs (enum rc_proto) */
enum {
	RC_PROTO_NEC  = 9,
	RC_PROTO_NECX = 10,
};

/* LIRC mode2 packet encoding */
#define LIRC_PULSE    0x01000000
#define LIRC_SPACE    0x00000000
#define LIRC_TIMEOUT  0x03000000
#define LIRC_OVERFLOW 0x04000000
#define LIRC_TYPE(v)  ((v) & 0xFF000000)
#define LIRC_DUR(v)   ((v) & 0x00FFFFFF)

/* NEC timing in microseconds (with generous tolerance) */
#define HDR_PULSE_MIN  8000
#define HDR_PULSE_MAX 10000
#define HDR_SPACE_MIN  3500
#define HDR_SPACE_MAX  5500
#define RPT_SPACE_MIN  1500
#define RPT_SPACE_MAX  3000
#define BIT_PULSE_MIN   300
#define BIT_PULSE_MAX   900
#define BIT0_SPACE_MIN  300
#define BIT0_SPACE_MAX  900
#define BIT1_SPACE_MIN 1200
#define BIT1_SPACE_MAX 2200

/* Roku remote address — change this if your remote has a different address */
#define ROKU_ADDR 0xEAC7

/* State machine phases */
enum {
	ST_INACTIVE,
	ST_HEADER_SPACE,
	ST_BIT_PULSE,
	ST_BIT_SPACE,
	ST_TRAILER,
};

struct decoder_state {
	__u32 state;
	__u32 bits;
	__u32 data;
	__u32 toggle;  /* flips on each new press, stays same on hold */
};

/* Legacy map definition — ir-keytable has no BTF/.maps support */
struct bpf_map_def {
	__u32 type;
	__u32 key_size;
	__u32 value_size;
	__u32 max_entries;
	__u32 flags;
	__u32 id;
	__u32 pinning;
};

struct bpf_map_def SEC("lirc_mode2/maps") decoder_state_map = {
	.type        = 2,  /* BPF_MAP_TYPE_ARRAY */
	.key_size    = sizeof(__u32),
	.value_size  = sizeof(struct decoder_state),
	.max_entries = 1,
};

SEC("lirc_mode2/roku_nec")
int roku_nec_decode(unsigned int *sample)
{
	__u32 val  = *sample;
	__u32 type = LIRC_TYPE(val);
	__u32 dur  = LIRC_DUR(val);
	__u32 key  = 0;

	struct decoder_state *s = bpf_map_lookup_elem(&decoder_state_map, &key);
	if (!s)
		return 0;

	/* Reset on timeout or overflow */
	if (type == LIRC_TIMEOUT || type == LIRC_OVERFLOW) {
		s->state = ST_INACTIVE;
		return 0;
	}

	if (s->state == ST_INACTIVE) {
		/* Waiting for 9ms header pulse */
		if (type == LIRC_PULSE &&
		    dur >= HDR_PULSE_MIN && dur <= HDR_PULSE_MAX) {
			s->state = ST_HEADER_SPACE;
			s->bits  = 0;
			s->data  = 0;
		}

	} else if (s->state == ST_HEADER_SPACE) {
		if (type != LIRC_SPACE) {
			s->state = ST_INACTIVE;
		} else if (dur >= HDR_SPACE_MIN && dur <= HDR_SPACE_MAX) {
			/* 4.5ms — normal header, data follows */
			s->state = ST_BIT_PULSE;
		} else if (dur >= RPT_SPACE_MIN && dur <= RPT_SPACE_MAX) {
			/* 2.25ms — standard NEC repeat code */
			bpf_rc_repeat(sample);
			s->state = ST_INACTIVE;
		} else {
			s->state = ST_INACTIVE;
		}

	} else if (s->state == ST_BIT_PULSE) {
		if (type == LIRC_PULSE &&
		    dur >= BIT_PULSE_MIN && dur <= BIT_PULSE_MAX) {
			s->state = ST_BIT_SPACE;
		} else {
			s->state = ST_INACTIVE;
		}

	} else if (s->state == ST_BIT_SPACE) {
		if (type != LIRC_SPACE) {
			s->state = ST_INACTIVE;
		} else if (dur >= BIT1_SPACE_MIN && dur <= BIT1_SPACE_MAX) {
			/* Bit = 1 */
			s->data |= 1u << s->bits;
			s->bits++;
			s->state = (s->bits == 32) ? ST_TRAILER : ST_BIT_PULSE;
		} else if (dur >= BIT0_SPACE_MIN && dur <= BIT0_SPACE_MAX) {
			/* Bit = 0 */
			s->bits++;
			s->state = (s->bits == 32) ? ST_TRAILER : ST_BIT_PULSE;
		} else {
			s->state = ST_INACTIVE;
		}

	} else if (s->state == ST_TRAILER) {
		s->state = ST_INACTIVE;

		/* Expect a final 562µs trailer pulse */
		if (type != LIRC_PULSE ||
		    dur < BIT_PULSE_MIN || dur > BIT_PULSE_MAX)
			return 0;

		/*
		 * NEC bit order is LSB-first, accumulated at position 0 upward:
		 *   bits[ 7: 0] = address low byte
		 *   bits[15: 8] = address high byte  (or ~addr_lo for standard NEC)
		 *   bits[23:16] = command
		 *   bits[31:24] = ~command
		 */
		/* Swap address bytes to match kernel NEC scancode convention */
		__u32 raw_addr = s->data & 0xFFFF;
		__u32 address  = ((raw_addr & 0xFF) << 8) | ((raw_addr >> 8) & 0xFF);
		__u32 command  = (s->data >> 16) & 0xFF;
		__u32 cmd_inv  = (s->data >> 24) & 0xFF;

		/* Validate command/complement pair */
		if ((command ^ cmd_inv) != 0xFF)
			return 0;

		if (address == ROKU_ADDR) {
			/*
			 * Roku remote: bit 7 of command = hold indicator.
			 * Mask it out so press and hold produce the same scancode.
			 * Use rc-core toggle to distinguish new press from hold:
			 *   - bit 7 clear → new press → flip toggle
			 *   - bit 7 set   → holding   → keep toggle (= repeat)
			 */
			__u32 is_hold    = (command >> 7) & 1;
			__u32 masked_cmd = command & 0x7F;
			__u64 scancode   = (__u64)((address << 8) | masked_cmd);

			if (!is_hold)
				s->toggle ^= 1;

			bpf_rc_keydown(sample, RC_PROTO_NECX,
				       scancode, s->toggle);
		} else {
			/* Non-Roku NEC remote — standard passthrough */
			__u32 addr_lo = address & 0xFF;
			__u32 addr_hi = (address >> 8) & 0xFF;

			if ((addr_lo ^ addr_hi) == 0xFF) {
				/* Standard NEC (8-bit address) */
				__u64 sc = (__u64)((addr_lo << 8) | command);
				bpf_rc_keydown(sample, RC_PROTO_NEC, sc, 0);
			} else {
				/* Extended NEC (16-bit address) */
				__u64 sc = (__u64)((address << 8) | command);
				bpf_rc_keydown(sample, RC_PROTO_NECX, sc, 0);
			}
		}
	} else {
		s->state = ST_INACTIVE;
	}

	return 0;
}

char _license[] SEC("license") = "GPL";  /* required by BPF verifier */

For the button mapping you just use the toml I posted above, except you only need the top half of the scancodes list since bpf is handling the rest now. Also attached is the compiled bpf.o for convenience.
roku_nec.bpf.o (2.0 KB)

I also ran into problems with Roku remotes and the necx protocol:- Help with necx protocol

My final solution was to use a flirc usb ir dongle, works perfectly.

does it let you push and hold (key repeat)? if so they must have accounted for the roku stuff

No, it only records the single press before the repeat starts.

However what you can do is record a single press. Then record the same keybinding but hold the remote button before executing the flirc_util command. This records both the single press and the repeat press on to the same button.

For example to record the up direction button.

  1. Type the command flirc_util record up then press enter to run the command
  2. Press the UP button on the remote.
  3. Type the same command flirc_util record up but don’t press enter.
  4. Hold the UP button on the remote for a couple of seconds to ensure the remote is sending the repeat command.
  5. Now press enter to record the press while still holding the remote button.

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.