Welcome to Suricrasia Online!

"Connecting your world, whenever!"

Turning a Keyboard into a Mouse with Libevdev

Cheap USB Keypad

Cheap USB Keypad

This is a short tutorial for using libevdev to capture all input events from a cheap USB keypad, and then using those events to synthesize new events for a virtual mouse device. We will be using C++ with libevdev on linux. On the way we'll be able to do other things such as launching arbitrary processes on keypress, and make it so the process can still capture input when run as a non-root user. This allows you to turn any keyboard into a macro keyboard with code you wrote yourself!

Although I'm using an external keypad you can theoretically use any device. Just make sure not to grab your real keyboard/mouse, since the code we will write is going to capture all of its inputs, which means you won't be able to kill the process if you can't submit a ctrl+c!

Prerequisites

The first thing you'll want to do is make sure libevdev and its headers are installed. You'll also want to make sure pkg-config is installed so we can automatically generate the required g++ arguments to build+link with it. On Debian/Ubuntu this is as simple as:

sudo apt install libevdev2 libevdev-dev pkg-config

This will be different for other, non-apt distributions.

Initialization

In order to initialize libevdev, we need to supply it with a character device. These are files in the directory /dev/input/ and follow the pattern event[0-9]+. To start we'll try opening every one of these in a loop, and use libevdev to get information about it:

#include <libevdev/libevdev.h>
#include <iostream>
#include <string>
#include <unistd.h>
#include <fcntl.h>

int main() {
	struct libevdev *dev = nullptr;

	for (int i = 0;; i++) {
		std::string path = "/dev/input/event" + std::to_string(i);
		int fd = open(path.c_str(), O_RDWR|O_CLOEXEC);
		if (fd == -1) {
			break; // no more character devices
		}
		if (libevdev_new_from_fd(fd, &dev) == 0) {
			std::string phys = libevdev_get_phys(dev);
			std::string name = libevdev_get_name(dev);

			std::cout << path << std::endl;
			std::cout << "- phys: " << phys << std::endl;
			std::cout << "- name: " << name << std::endl;
			std::cout << std::endl;

			libevdev_free(dev);
		}
		close(fd);
	}
	return 0;
}
step-1-enumerating-devices.cpp

You can now build this with the following command:

g++ main.cpp -o main `pkg-config --cflags --libs libevdev`

If you now run sudo ./main you should see it dump out all connected input devices.

/dev/input/event0
- phys: PNP0C0C/button/input0
- name: Power Button

/dev/input/event1
- phys: LNXPWRBN/button/input0
- name: Power Button

/dev/input/event2
- phys: usb-0000:09:00.3-3/input3
- name: Blue Microphones Yeti Stereo Microphone Consumer Control

/dev/input/event3
- phys: usb-0000:02:00.0-9/input0
- name: Logitech USB Optical Mouse
...

Grabbing inputs

The keypad device I am interested in has the somewhat redundant name "Usb KeyBoard Usb KeyBoard". When we spot this name in our loop, we want to be able to save the struct libevdev* pointer without freeing it. We'll wrap the enumeration code into a function called find_device_by_name so we can return the struct libevdev* when we find the requested device.

#include <libevdev/libevdev.h>
#include <iostream>
#include <string>
#include <unistd.h>
#include <fcntl.h>

struct libevdev* find_device_by_name(const std::string& requested_name) {
	struct libevdev *dev = nullptr;

	for (int i = 0;; i++) {
		std::string path = "/dev/input/event" + std::to_string(i);
		int fd = open(path.c_str(), O_RDWR|O_CLOEXEC);
		if (fd == -1) {
			break; // no more character devices
		}
		if (libevdev_new_from_fd(fd, &dev) == 0) {
			std::string name = libevdev_get_name(dev);
			if (name == requested_name) {
				return dev;
			}
			libevdev_free(dev);
			dev = nullptr;
		}
		close(fd);
	}

	return nullptr;
}

int main() {
	struct libevdev *dev = find_device_by_name("Usb KeyBoard Usb KeyBoard");

	if (dev == nullptr) {
		std::cerr << "Couldn't find device!" << std::endl;
		return -1;
	}

	libevdev_free(dev);
	return 0;
}
step-2-find-by-name.cpp

For this usecase, it is sufficient to match on the "name" field of the device. However, if you want to capture multiple keyboards of the same make that all have the same name (e.g. if you're trying to make an emoji keyboard) then you can use the "phys" field to get a more unique value, as it seems to include the USB address.

To capture the events coming from the device, it's as simple as using the libevdev_grab function and calling libevdev_next_event in a loop.

void process_events(struct libevdev *dev) {

	struct input_event ev = {};
	int status = 0;
	auto is_error = [](int v) { return v < 0 && v != -EAGAIN; };
	auto has_next_event = [](int v) { return v >= 0; };
	const auto flags = LIBEVDEV_READ_FLAG_NORMAL | LIBEVDEV_READ_FLAG_BLOCKING;

	while (status = libevdev_next_event(dev, flags, &ev), !is_error(status)) {
		if (!has_next_event(status)) continue;

		std::cout << "Got input_event";
		std::cout << " type=" << ev.type;
		std::cout << " code=" << ev.code;
		std::cout << " value=" << ev.value << std::endl;
	}
}

int main() {
	struct libevdev *dev = find_device_by_name("Usb KeyBoard Usb KeyBoard");

	if (dev == nullptr) {
		std::cerr << "Couldn't find device!" << std::endl;
		return -1;
	}

	libevdev_grab(dev, LIBEVDEV_GRAB);

	process_events(dev);

	libevdev_free(dev);
	return 0;
}
step-3-grab-and-dump.cpp

Those is_error and has_next_event lambdas are an attempt at making the return value handling of libevdev_next_event more readable/concise. The return value is non-negative if an event was captured, otherwise the return value is an error code. However not all error codes are equal—the return value could be -EAGAIN which means you just need to retry. Therefore the while loop's condition checks if the return value is non-negative or -EAGAIN, and the body is skipped if we got EAGAIN.

Now if we run this code and I press on the keys of the keypad, I get output like this:

Got input_event type=4 code=4 value=458849
Got input_event type=1 code=73 value=1
Got input_event type=0 code=0 value=0
Got input_event type=4 code=4 value=458849
Got input_event type=1 code=73 value=0

From here, it's a matter of detective work to determine what the events mean. By pressing on the buttons and observing the outputs, it's possible to infer the meanings of the fields of the input_event structure. For this keyboard, I figured out that when ev.type == 1, then the code field contains the scancode and the value field represents whether it's a key up, key down, or autorepeat event.

Since all other events can be ignored, I'll modify the process_events function to call a secondary function, called process_key, with the scancode and key state. I don't care about autorepeat events so I will filter those out.

void process_key(int code, bool is_down) {
	std::cout << "scancode= " << code << " is_down=" << is_down << std::endl;
}

void process_events(struct libevdev *dev) {
	struct input_event ev = {};
	int status = 0;
	auto is_error = [](int v) { return v < 0 && v != -EAGAIN; };
	auto has_next_event = [](int v) { return v >= 0; };
	const auto flags = LIBEVDEV_READ_FLAG_NORMAL | LIBEVDEV_READ_FLAG_BLOCKING;

	while (status = libevdev_next_event(dev, flags, &ev), !is_error(status)) {
		if (!has_next_event(status)) continue;

		if (ev.type == 1) {
			bool is_up = ev.value == 0;
			bool is_down = ev.value == 1;
			if (is_down || is_up) { // excludes autorepeat
				process_key(ev.code, is_down);
			}
		}
	}
}
step-4-process-key.cpp

Running external programs

Now that we have a function that receives scancodes, we could launch arbitrary commands with the system function. Unfortunately, since we have to run our code as root to capture the keypad input, these commands will also run as root. This is especially a problem if you want to use this to launch X11 apps, since they won't run as your user and therefore not use your config files.

To get around this, we'll use capabilities to give our program the ability to change its group ID to input. Now we won't need sudo to run our program!

#include <grp.h>
...
int main() {
	auto grp = getgrnam("input");
	if (grp == nullptr) {
		std::cerr << "getgrnam(\"input\") failed" << std::endl;
		return -1;
	}
	int oldgid = getgid();
	if (setgid(grp->gr_gid) < 0) {
		std::cerr << "couldn't change group to input!" << std::endl;
		return -1;
	}

	struct libevdev *dev = find_device_by_name("Usb KeyBoard Usb KeyBoard");

	if (dev == nullptr) {
		std::cerr << "Couldn't find device!" << std::endl;
		return -1;
	}

	//drop back into old permissions
	if (setgid(oldgid) < 0) {
		std::cerr << "couldn't switch back to old group!" << std::endl;
		return -1;
	}

	libevdev_grab(dev, LIBEVDEV_GRAB);

	process_events(dev);

	libevdev_free(dev);
	return 0;
}
step-5-group-capabilities.cpp
sudo setcap "cap_setgid=eip" ./main
./main #works!

Unfortunately, adding the capability to a process still requires sudo, and the capabilities will be stripped away every time you rebuild. However now you can run arbitrary commands on particular keypresses and they will run as your user. Here's an example:

void process_key(int code, bool is_down) {
	std::cout << "scancode= " << code << " is_down=" << is_down << std::endl;

	if (is_down && code == 69) {
		system("xterm &");
	}
}
step-5-group-capabilities.cpp

For me this launches xterm when I press the num lock key on the keypad.

Creating a virtual mouse

In addition to providing a way to capture a device, libevdev also allows you to create virtual devices and events. Doing this is fairly straightforward, you create a device with libevdev_new and set its name and properties with the corresponding functions. For this project I will create a virtual mouse and encapsulate it in a class:

#include <libevdev/libevdev-uinput.h>
#include <mutex>

class VirtualMouse {
public:
	struct libevdev_uinput* m_uinput = nullptr;
	std::mutex m_mouseMutex;
public:
	VirtualMouse() {}
	~VirtualMouse() {
		libevdev_uinput_destroy(m_uinput);
	}

	int Init() {
		struct libevdev* dev = libevdev_new();
		libevdev_set_name(dev, "Virtual Mouse");

		libevdev_enable_property(dev, INPUT_PROP_POINTER);

		libevdev_enable_event_type(dev, EV_REL);
		libevdev_enable_event_code(dev, EV_REL, REL_X, nullptr);
		libevdev_enable_event_code(dev, EV_REL, REL_Y, nullptr);
		libevdev_enable_event_code(dev, EV_REL, REL_WHEEL, nullptr);

		libevdev_enable_event_type(dev, EV_KEY);
		libevdev_enable_event_code(dev, EV_KEY, BTN_LEFT, nullptr);
		libevdev_enable_event_code(dev, EV_KEY, BTN_RIGHT, nullptr);
		libevdev_enable_event_code(dev, EV_KEY, BTN_MIDDLE, nullptr);

		int r = libevdev_uinput_create_from_device(dev, LIBEVDEV_UINPUT_OPEN_MANAGED, &m_uinput);
		libevdev_free(dev);
		return r;
	}

	void Move(int rx, int ry) {
		std::lock_guard<std::mutex> guard(m_mouseMutex);
		libevdev_uinput_write_event(m_uinput, EV_REL, REL_X, rx);
		libevdev_uinput_write_event(m_uinput, EV_REL, REL_Y, ry);
		libevdev_uinput_write_event(m_uinput, EV_SYN, SYN_REPORT, 0);
	}

	void Scroll(int rs) {
		std::lock_guard<std::mutex> guard(m_mouseMutex);
		libevdev_uinput_write_event(m_uinput, EV_REL, REL_WHEEL, rs);
		libevdev_uinput_write_event(m_uinput, EV_SYN, SYN_REPORT, 0);
	}

	void Click(int btn, bool isDown) {
		std::lock_guard<std::mutex> guard(m_mouseMutex);
		libevdev_uinput_write_event(m_uinput, EV_KEY, btn, isDown);
		libevdev_uinput_write_event(m_uinput, EV_SYN, SYN_REPORT, 0);
	}
};

VirtualMouse g_mouse;
...
int main() {
...
	// must init mouse before we drop permissions
	if (g_mouse.Init() != 0) {
		std::cerr << "couldn't init mouse!" << std::endl;
		return -1;
	}

	//drop back into old permissions
	if (setgid(oldgid) < 0) {
		std::cerr << "couldn't switch back to old group!" << std::endl;
		return -1;
	}
...
}
step-6-virtual-mouse-clicks.cpp

As you can see, creating a device is a simple—if verbose—process of enabling all the right properties, event types, and event codes. If you wanted to make a keyboard, you need to manually enable every individual scancode you intend to generate an event for.

Now that we have our mouse, we can click its buttons when certain keys of the keypad are pressed:

void process_key(int code, bool is_down) {
	if (code == 82) g_mouse.Click(BTN_LEFT, is_down);
	if (code == 96) g_mouse.Click(BTN_RIGHT, is_down);
	if (code == 83) g_mouse.Click(BTN_MIDDLE, is_down);
}
step-6-virtual-mouse-clicks.cpp

Neat!

Moving the virtual mouse

What I'd like to do now is move the cursor around when the keypad buttons are used like arrow keys. This is more complicated than simply calling methods on the g_mouse object in process_key, since I want to have acceleration.

What we'll need is a thread to periodically synthesize and send mouse motion events, as well as a way for that thread to query what keys are currently pressed. To solve the second problem, we'll create a global std::set<int> variable to store all pressed keys, as well as a mutex to protect it.

#include <set>
...
std::set<int> g_pressedKeys;
std::mutex g_pressed_keys_mutex;

void process_key(int code, bool is_down) {
	std::lock_guard<std::mutex> guard(g_pressed_keys_mutex);
	if (is_down) {
		g_pressedKeys.insert(code);
	} else {
		g_pressedKeys.erase(code);
	}

	if (code == 82) g_mouse.Click(BTN_LEFT, is_down);
	if (code == 96) g_mouse.Click(BTN_RIGHT, is_down);
	if (code == 83) g_mouse.Click(BTN_MIDDLE, is_down);
}
step-7-virtual-mouse-movement.cpp

Now we can create a thread that periodically checks for pressed keys, and generates an appropriate mouse movement event.

#include <thread>
#include <chrono>
#include <atomic>
#include <math.h>
...
std::atomic_bool g_run_mouse_thread;
void mouse_thread_fn(void*) {
	float rx = 0;
	float ry = 0;
	const float friction = 0.85;
	const float accel = 1.2/friction;
	while (g_run_mouse_thread) {
		float dx = 0;
		float dy = 0;

		float rs = 0;

		{
			std::lock_guard<std::mutex> guard(g_pressed_keys_mutex);
			if (g_pressedKeys.count(77) > 0) rx += accel;
			if (g_pressedKeys.count(75) > 0) rx -= accel;
			if (g_pressedKeys.count(76) > 0) ry += accel;
			if (g_pressedKeys.count(72) > 0) ry -= accel;

			if (g_pressedKeys.count(78) > 0) rs += 1;
			if (g_pressedKeys.count(14) > 0) rs -= 1;
		}

		// resize movement vector to be length 1
		if (fabs(dx)+fabs(dy) > accel) {
			dx *= .7;
			dy *= .7;
		}

		rx += dx;
		ry += dy;
		rx *= friction;
		ry *= friction;

		if (fabs(rx)+fabs(ry) > 0) {
			g_mouse.Move(rx, ry);
		}
		if (fabs(rs) > 0) {
			g_mouse.Scroll(rs);
		}
		std::this_thread::sleep_for(std::chrono::milliseconds(10));
	}
}
...
int main() {
...
	g_run_mouse_thread = true;
	std::thread mouse_thread(mouse_thread_fn, nullptr);

	libevdev_grab(dev, LIBEVDEV_GRAB);

	process_events(dev);

	libevdev_free(dev);

	g_run_mouse_thread = false;
	mouse_thread.join();

	return 0;
}
step-7-virtual-mouse-movement.cpp

Since we are now using threads, make sure to build with the -pthread argument.

And with that, our quest is a success! Here's a video of it in action:

Admittedly it's very hard to control and it makes my wrists hurt. This could be a problem with the friction/acceleration settings I have, or it could just be this method of input isn't for me.

Once you have your input grabbing code exactly as you like it, you could install the program to your /usr/bin folder and add it as a startup app in your desktop environment. That way it will run on login and have access to your DISPLAY/WAYLAND_DISPLAY variable, and run any app you desire correctly.

You can find more information about libevdev on its API documentation page. You can also find all the code for this blog post on its github repo.


← Back