Rpi rotary encoder as volume control

I’d like to post this in the how to section, but as a new user i don’t appear to have the needed permission.
This is a guide about how to use a rotary encoder connected to the pi’s gpio pins as a volume control.
this makes use of the rotary-encoder overlay and driver along with a custom driver. It should be noted that this currently only works on the dev branch, with kernel 4.14.30-2 or higher.

firstly we configure the rotary-encoder overlay, in /boot/config.txt add the following line:

dtoverlay=rotary-encoder-overlay:relative_axis=1,linux_axis=9

then we install the headers required to build the driver:

sudo apt-get install rbp2-headers-$(uname -r)

(change to rbp1 if single core pi)

symlink the header files:

sudo ln -s /usr/src/rbp2-headers-$(uname -r) /lib/modules/$(uname -r/build

in a folder create the following
makefile:

obj-m += gpio_volume.o
all:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
install:
	cp gpio_volume.ko /lib/modules/$(shell uname -r)/kernel/drivers/input/keyboard/
	chown root:root /lib/modules/$(shell uname -r)/kernel/drivers/input/keyboard/gpio_volume.ko
	depmod -a

gpio_volume.c:

#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt

#include <linux/slab.h>
#include <linux/module.h>
#include <linux/input.h>
#include <linux/init.h>
#include <linux/device.h>

MODULE_AUTHOR("James Kent");
MODULE_DESCRIPTION("gpio rotary encoder as volume control module");
MODULE_LICENSE("GPL");

static char *devicename = "rotary@4";
module_param(devicename, charp, 0);
MODULE_PARM_DESC(devicename, "name of rotary input device");

static int reltype = REL_MISC; // 0x09
module_param(reltype, int, 0);
MODULE_PARM_DESC(reltype, "type of relative event to listen for");

static int count_per_press = 10;
module_param(count_per_press, int, 0);
MODULE_PARM_DESC(count_per_press, "event count before a press is generated");

static struct input_dev *button_dev;

static void send_key(int key) {
	input_report_key(button_dev, key, 1);
	input_sync(button_dev);
	input_report_key(button_dev, key, 0);
	input_sync(button_dev);
}

int count = 0;

static void rotary_event(struct input_handle *handle, unsigned int type, unsigned int code, int value) {
	printk(KERN_DEBUG pr_fmt("Event. Dev: %s, Type: %d, Code: %d, Value: %d\n"), dev_name(&handle->dev->dev), type, code, value);
	if (type == EV_REL) {
		if (code == reltype) {
			int i;
			int inc = (value > 0) ? 1 : -1;
			if ((inc > 0 && count < 0) || (inc < 0 && count > 0)) { // if change of direction reset count
				count = 0;
			}
			for (i=0; i!=value; i+=inc) {
				count += inc;
				if (abs(count) >= count_per_press) {
					send_key( (inc > 0) ? KEY_VOLUMEUP : KEY_VOLUMEDOWN);
					count = 0;
				}
			}
		}
	}
}

static int rotary_connect(struct input_handler *handler, struct input_dev *dev, const struct input_device_id *id) {
	struct input_handle *handle;
	int error;

	handle = kzalloc(sizeof(struct input_handle), GFP_KERNEL);
	if (!handle)
		return -ENOMEM;

	handle->dev = dev;
	handle->handler = handler;
	handle->name = "gpio_volume";

	error = input_register_handle(handle);
	if (error)
		goto err_free_handle;

	error = input_open_device(handle);
	if (error)
		goto err_unregister_handle;

	printk(KERN_DEBUG pr_fmt("Connected device: %s (%s at %s)\n"),
	       dev_name(&dev->dev),
	       dev->name ?: "unknown",
	       dev->phys ?: "unknown");

	return 0;

 err_unregister_handle:
	input_unregister_handle(handle);
 err_free_handle:
	kfree(handle);
	return error;
}

bool startsWith(const char *pre, const char *str) {
    size_t lenpre = strlen(pre),
           lenstr = strlen(str);
    return lenstr < lenpre ? false : strncmp(pre, str, lenpre) == 0;
}

// keep record of match and remove record is disconnect
bool matched = false;

static bool rotary_match(struct input_handler *handler, struct input_dev *dev) {
	if (matched)
		return false;
	matched = startsWith(devicename, dev->name);
	return matched;
}

static void rotary_disconnect(struct input_handle *handle) {
	printk(KERN_DEBUG pr_fmt("Disconnected device: %s\n"), dev_name(&handle->dev->dev));

	input_close_device(handle);
	input_unregister_handle(handle);
	kfree(handle);
	matched = false;
}

static const struct input_device_id rotary_ids[] = {
	{ .driver_info = 1 },	/* Matches all devices */
	{ },			/* Terminating zero entry */
};

MODULE_DEVICE_TABLE(input, rotary_ids);

static struct input_handler rotary_handler = {
	.event =	rotary_event,
	.match =	rotary_match,
	.connect =	rotary_connect,
	.disconnect =	rotary_disconnect,
	.name =		"gpio_volume",
	.id_table =	rotary_ids,
};

static int __init button_init(void) {
	int error;
	int i;
  
	button_dev = input_allocate_device();
	if (!button_dev) {
		printk(KERN_ERR "gpio_volume: Not enough memory\n");
		error = -ENOMEM;
		return error;
	}

	button_dev->name = "GPIO Rotary Encoder Volume";
	button_dev->evbit[0] = BIT_MASK(EV_KEY);// | BIT_MASK(EV_REP);
	set_bit(KEY_VOLUMEDOWN, button_dev->keybit);
	set_bit(KEY_VOLUMEUP, button_dev->keybit);
	
	for (i=KEY_ESC; i<=KEY_KPDOT; i++) { // add a load of extra keys
		set_bit(i, button_dev->keybit);
	}

	error = input_register_device(button_dev);
	if (error) {
		printk(KERN_ERR "gpio_volume: Failed to register device\n");
		goto err_free_dev;
	}
	return 0;
 err_free_dev:
	input_free_device(button_dev);
	return error;
}

static int __init gpio_volume_init(void) {
	int error = button_init();
	if (count_per_press < 1) // sanitise input
		count_per_press = 1;
	if (error == 0) {
		if (input_register_handler(&rotary_handler)==0) {
			printk(KERN_INFO "gpio_volume loaded.\n");
			return 0;
		} else {
			input_unregister_device(button_dev);
			input_free_device(button_dev);
		}
	}
	return error;
}

static void __exit gpio_volume_exit(void) {
	input_unregister_device(button_dev);
	input_free_device(button_dev);
	input_unregister_handler(&rotary_handler);
}

module_init(gpio_volume_init);
module_exit(gpio_volume_exit);

then run:

make
sudo make install

to auto load the driver on boot edit /etc/modules to add the following on it’s own line (requires sudo):

gpio_volume

in order for this to work in kodi its currently required to edit one of the udev rules:
/etc/udev/rules.d/998-fix-input.rules:

# input
KERNEL=="event*", MODE="0660", GROUP="osmc"
KERNEL=="event*", DRIVERS=="gpio-keys", MODE="0660", GROUP="root" # unrelated, allows gpio-shutdown overlay to work
KERNEL=="event*", DRIVERS=="rotary-encoder", MODE="0660", GROUP="root"
KERNEL=="mouse*|mice", MODE="0660", GROUP="osmc"
KERNEL=="ts[0-9]*|uinput",      MODE="0660", GROUP="osmc"
KERNEL=="js[0-9]*",             MODE="0660", GROUP="osmc"

this changes ownership of devices with the gpio-keys or rotary-encoder devices to the root group preventing kodi grabbing them.

currently kodi treats anything with 20 or less keys as “not a keyboard”, so the gpio-keys driver doesn’t work correctly. by changing ownership to root the power key is handled by systemd allowing it to work. this is also why the above driver reports being able to generate a lot of keys other than the volume up or down keys, so that kodi will recognise it.

This is rather cool!

Thanks for your input. I’m unable to test your code or comment on it more generally but I noticed the above line, which has a few issues:

  • it requires root to write to /etc/modules.
  • The way you have written it would lose any existing entries in /etc/modules.
  • Running sudo echo "gpio_volume" >> /etc/modules would also not work, and using echo “gpio_volume” | sudo tee /etc/modules would also overwrite existing entries.

Therefore the safest method is to edit the file (using sudo).

One other thing, in the makefile:

cp gpio_volume.ko /lib/modules/$(shell uname -r)/kernel/drivers/input/keyboard/

the receiving directory, keyboard, does not exists on my Pi. Does it need to be created first?

Is it possible to create a simple sharedobject to import for addons?
only simple callback commands for:
turn clockwise
turn counterclockwise
button down
button up

i’ve made your suggested changes with editing this file, was a silly mistake on my part, also if this directory doesn’t exists then you probably don’t have the gpio_keys or rotary_encoder drivers present, you could test this with modprobe, does the following work:

sudo modprobe rotary-encoder

if not then the driver isn’t present, you either need to update to the latest dev branch or build this module too

I’ve done a more complete writeup and moved the project to github here: GitHub - JamesGKent/rotary_volume: kernel module to use rotary encoder as a volume control

Note that I changed the name as i thought it was more appropriate, please feel free to raise any issues here or there and i’ll try to address them.

I’m assuming by sharedobject you mean a module that could be imported for making addons, this could be possible using the python-evdev and setting up the rotary encoder the same as above, and avoid this module entirely. this would work for turning the encoder, but would still need to do something for buttons, if i get time i’ll look into this.

Very good – I’ll follow up on this shortly on the GitHub issue as well.

example to use python:

import evdev
desired_device = 'rotary@4'
devices = [evdev.InputDevice(fn) for fn in evdev.list_devices()]
dev = None
for device in devices:
	if device.phys == desired_device:
		dev = device
		break
		
if dev:
	with evdev.UInput() as ui:
		for event in dev.read_loop():
			print(categorize(event))
			if event.type == evdev.ecodes.EV_REL:
				if event.code == evdev.ecodes.REL_X:
					if (event.value > 0):
						ui.write_event(evdev.ecodes.EV_KEY, evdev.ecodes.KEY_VOLUMEDOWN, 1)
						ui.write_event(evdev.ecodes.EV_KEY, evdev.ecodes.KEY_VOLUMEDOWN, 0)
					elif (event.value < 0):
						ui.write_event(evdev.ecodes.EV_KEY, evdev.ecodes.KEY_VOLUMEUP, 1)
						ui.write_event(evdev.ecodes.EV_KEY, evdev.ecodes.KEY_VOLUMEUP, 0)
					ui.syn()

note I haven’t tested this from within kodi.

Thanks for clarifying that point. You’re right, I don’t have the drivers present. So just to be clear, are these drivers only introduced with kernel 4.14.30-2 or higher, or are there other reasons why this kernel is necessary?

The config options required to build them were only enabled from that kernel release, thats the only reason, it should be possible to download the source and add it to your existing installation rather than upgrade, but i haven’t tested this. I have provided a makefile and a link in the github repo to do this however

1 Like

FYI, The Python script works but I think you need to change
ui.write_event(...) to ui.write as per the injecting-input

Good stuff though, I’ll be using this approach.

1 Like

thats what i get for skim reading documentation… either method would work, but ui.write requires less input/code to use correctly…
however it looks like i’ve used the valid arguments for write as the arguments for write_event