Files
buffybox/shared/indev.c
Vladimir Stoiakin 8b1ca6e4cf Update LVGL to 9.2.2
2025-04-15 16:17:52 +00:00

556 lines
16 KiB
C

/**
* Copyright 2021 Johannes Marbach
* SPDX-License-Identifier: GPL-3.0-or-later
*/
#include "indev.h"
#include "cursor/cursor.h"
#include "log.h"
#include <libinput.h>
#include <libudev.h>
#include <limits.h>
#include <stdio.h>
#include <string.h>
#include <linux/input.h>
#include <sys/select.h>
/**
* Defines
*/
#define INPUT_DEVICE_NODE_PREFIX "/dev/input/event"
#define MAX_KEYBOARD_DEVS 4
#define MAX_POINTER_DEVS 4
#define MAX_TOUCHSCREEN_DEVS 1
/**
* Static variables
*/
static lv_libinput_capability allowed_capability = LV_LIBINPUT_CAPABILITY_NONE;
static struct udev *context = NULL;
static struct udev_monitor *monitor = NULL;
static int monitor_fd = -1;
struct input_device {
char *node;
lv_libinput_capability capability;
lv_indev_t *indev;
};
static struct input_device **devices = NULL;
static int num_devices = 0;
static int num_connected_devices = 0;
lv_group_t *keyboard_input_group = NULL;
lv_obj_t *cursor_obj = NULL;
/**
* Static prototypes
*/
/**
* Test whether a device can act as a keyboard device.
*
* @param device the device to test
* @return true if the device has the keyboard capability
*/
static bool is_keyboard_device(struct input_device *device);
/**
* Test whether a device can act as a pointer device.
*
* @param device the device to test
* @return true if the device has the pointer capability
*/
static bool is_pointer_device(struct input_device *device);
/**
* Test whether a device can act as a touch device.
*
* @param device the device to test
* @return true if the device has the touch capability
*/
static bool is_touch_device(struct input_device *device);
/**
* Convert a device capability into a descriptive string.
*
* @param capability input device capability
* @return textual description
*/
static char *capability_to_str(lv_libinput_capability capability);
/**
* Connect a specific input device using its udev device.
*
* @param device udev device
*/
static void connect_udev_device(struct udev_device *device);
/**
* Connect a specific input device using its device node.
*
* @param node device node path
*/
static void connect_devnode(const char *node);
/**
* Disconnect a specific input device using its device node.
*
* @param node device node path
*/
static void disconnect_devnode(const char *node);
/**
* Disconnect a specific input device using its index in the devices array.
*
* @param idx index in devices array
*/
static void disconnect_idx(int idx);
/**
* Set up the input group for a keyboard device.
*
* @param device the input device
*/
static void set_keyboard_input_group(struct input_device *device);
/**
* Set up the mouse cursor image for a pointer device.
*
* @param device the input device
*/
static void set_mouse_cursor(struct input_device *device);
/**
* Static functions
*/
static bool is_keyboard_device(struct input_device *device) {
return (device->capability & LV_LIBINPUT_CAPABILITY_KEYBOARD) != LV_LIBINPUT_CAPABILITY_NONE;
}
static bool is_pointer_device(struct input_device *device) {
return (device->capability & LV_LIBINPUT_CAPABILITY_POINTER) != LV_LIBINPUT_CAPABILITY_NONE;
}
static bool is_touch_device(struct input_device *device) {
return (device->capability & LV_LIBINPUT_CAPABILITY_TOUCH) != LV_LIBINPUT_CAPABILITY_NONE;
}
static char *capability_to_str(lv_libinput_capability capability) {
if (capability == LV_LIBINPUT_CAPABILITY_KEYBOARD) {
return "keyboard";
}
if (capability == LV_LIBINPUT_CAPABILITY_POINTER) {
return "pointer";
}
if (capability == LV_LIBINPUT_CAPABILITY_TOUCH) {
return "touch";
}
return "none";
}
static void connect_udev_device(struct udev_device *device) {
/* Obtain and verify device node */
const char *node = udev_device_get_devnode(device);
if (!node || strncmp(node, INPUT_DEVICE_NODE_PREFIX, strlen(INPUT_DEVICE_NODE_PREFIX)) != 0) {
bbx_log(BBX_LOG_LEVEL_VERBOSE, "Ignoring unsupported input device %s", udev_device_get_syspath(device));
return;
}
/* Connect device using its node */
connect_devnode(node);
}
static void connect_devnode(const char *node) {
/* Check if the device is already connected */
for (int i = 0; i < num_connected_devices; ++i) {
if (strcmp(devices[i]->node, node) == 0) {
bbx_log(BBX_LOG_LEVEL_WARNING, "Ignoring already connected input device %s", node);
return;
}
}
/* Double array size every time it's filled */
if (num_connected_devices == num_devices) {
/* Re-allocate array */
struct input_device **tmp = realloc(devices, (2 * num_devices + 1) * sizeof(struct input_device *));
if (!tmp) {
bbx_log(BBX_LOG_LEVEL_ERROR, "Could not reallocate memory for input device array");
return;
}
devices = tmp;
/* Update size counter */
num_devices = 2 * num_devices + 1;
/* Fill empty space with zeros */
lv_memzero(devices + num_connected_devices, (num_devices - num_connected_devices) * sizeof(struct input_device *));
}
/* Allocate memory for new input device and insert it */
struct input_device *device = malloc(sizeof(struct input_device));
lv_memzero(device, sizeof(struct input_device));
devices[num_connected_devices] = device;
/* Copy the node path so that it can be used beyond the caller's scope */
device->node = strdup(node);
/* Initialise the indev and obtain the libinput device */
device->indev = lv_libinput_create(LV_INDEV_TYPE_NONE, device->node);
if (!device->indev) {
bbx_log(BBX_LOG_LEVEL_WARNING, "Aborting connection of input device %s because libinput failed to connect it", node);
disconnect_idx(num_connected_devices);
return;
}
lv_libinput_t *dsc = lv_indev_get_driver_data(device->indev);
struct libinput_device *device_libinput = dsc->libinput_device;
if (!device_libinput) {
bbx_log(BBX_LOG_LEVEL_WARNING, "Aborting connection of input device %s because libinput failed to connect it", node);
disconnect_idx(num_connected_devices);
return;
}
/* Obtain device capabilities */
device->capability = lv_libinput_query_capability(device_libinput);
/* If the device doesn't have any supported capabilities, exit */
if ((device->capability & allowed_capability) == LV_LIBINPUT_CAPABILITY_NONE) {
bbx_log(BBX_LOG_LEVEL_WARNING, "Aborting connection of input device %s because it has no allowed capabilities", node);
disconnect_idx(num_connected_devices);
return;
}
/*
* Set up indev type and related properties
*
* FIXME: Some libinput devices (notably, certain hid-i2c keyboards)
* report both keyboard and pointer capability. Since lvgl expects
* every input device to have only one type, we register those devices
* as keyboards, considering this capability more useful. However
* we must also assume that keyboard+touch devices are touch.
*/
if (is_touch_device(device)) {
device->indev->type = LV_INDEV_TYPE_POINTER;
device->indev->long_press_repeat_time = USHRT_MAX;
} else if (is_keyboard_device(device)) {
device->indev->type = LV_INDEV_TYPE_KEYPAD;
} else if (is_pointer_device(device)) {
device->indev->type = LV_INDEV_TYPE_POINTER;
device->indev->long_press_repeat_time = USHRT_MAX;
}
/* Set the input group for keyboard devices */
if (is_keyboard_device(device)) {
set_keyboard_input_group(device);
}
/* Set the mouse cursor for pointer devices */
if (is_pointer_device(device)) {
set_mouse_cursor(device);
}
/* Increment connected device count */
num_connected_devices++;
bbx_log(BBX_LOG_LEVEL_VERBOSE, "Connected input device %s (%s)", node, capability_to_str(device->capability));
}
static void disconnect_udev_device(struct udev_device *device) {
/* Obtain and verify device node */
const char *node = udev_device_get_devnode(device);
if (!node || strncmp(node, INPUT_DEVICE_NODE_PREFIX, strlen(INPUT_DEVICE_NODE_PREFIX)) != 0) {
bbx_log(BBX_LOG_LEVEL_VERBOSE, "Ignoring unsupported input device %s", udev_device_get_syspath(device));
return;
}
/* Disconnect device using its node */
disconnect_devnode(node);
}
static void disconnect_devnode(const char *node) {
/* Find connected device matching the specified node */
int idx = -1;
for (int i = 0; i < num_connected_devices; ++i) {
if (strcmp(devices[i]->node, node) == 0) {
idx = i;
break;
}
}
/* If no matching device was found, exit */
if (idx < 0) {
bbx_log(BBX_LOG_LEVEL_WARNING, "Ignoring already disconnected input device %s", node);
return;
}
/* Disconnect device using its index */
disconnect_idx(idx);
/* Shift subsequent devices forward */
for (int i = idx + 1; i < num_connected_devices; ++i) {
devices[i - 1] = devices[i];
/* Zero out the last element after shifting it forward */
if (i == num_connected_devices - 1) {
lv_memzero(devices + i, sizeof(struct input_device *));
}
}
/* Decrement connected device count */
--num_connected_devices;
bbx_log(BBX_LOG_LEVEL_VERBOSE, "Disconnected input device %s", node);
}
static void disconnect_idx(int idx) {
/* Delete LVGL indev */
if (devices[idx]->indev) {
lv_libinput_delete(devices[idx]->indev);
}
/* Free previously copied node path */
if (devices[idx]->node) {
free(devices[idx]->node);
}
/* Deallocate memory and zero out freed array element */
free(devices[idx]);
lv_memzero(devices + idx, sizeof(struct input_device *));
}
static void set_keyboard_input_group(struct input_device *device) {
/* Ignore non-keyboard devices */
if (!is_keyboard_device(device)) {
return;
}
/* Apply the input group */
lv_indev_set_group(device->indev, keyboard_input_group);
}
static void set_mouse_cursor(struct input_device *device) {
/* Ignore non-pointer devices */
if (!is_pointer_device(device)) {
return;
}
/* Initialise cursor image if needed */
if (!cursor_obj) {
cursor_obj = lv_image_create(lv_screen_active());
lv_image_set_src(cursor_obj, &cursor);
}
/* Apply the cursor image */
lv_indev_set_cursor(device->indev, cursor_obj);
}
static void query_device_monitor(lv_timer_t *timer) {
LV_UNUSED(timer);
bbx_indev_query_monitor();
}
/**
* Public functions
*/
void bbx_indev_set_allowed_device_capability(bool keyboard, bool pointer, bool touchscreen) {
allowed_capability = LV_LIBINPUT_CAPABILITY_NONE;
if (keyboard) {
allowed_capability |= LV_LIBINPUT_CAPABILITY_KEYBOARD;
}
if (pointer) {
allowed_capability |= LV_LIBINPUT_CAPABILITY_POINTER;
}
if (touchscreen) {
allowed_capability |= LV_LIBINPUT_CAPABILITY_TOUCH;
}
}
void bbx_indev_set_keyboard_input_group(lv_group_t *group) {
/* Store the group */
keyboard_input_group = group;
/* Apply the group on all connected keyboard devices */
for (int i = 0; i < num_connected_devices; ++i) {
if (is_keyboard_device(devices[i])) {
set_keyboard_input_group(devices[i]);
}
}
}
void bbx_indev_start_monitor_and_autoconnect(bool keyboard, bool pointer, bool touchscreen) {
bbx_indev_set_allowed_device_capability(keyboard, pointer, touchscreen);
bbx_indev_start_monitor();
lv_timer_create(query_device_monitor, 1000, NULL);
bbx_indev_auto_connect();
}
void bbx_indev_auto_connect() {
bbx_log(BBX_LOG_LEVEL_VERBOSE, "Auto-connecting supported input devices");
/* Make sure udev context is initialised */
if (!context) {
context = udev_new();
if (!context) {
bbx_log(BBX_LOG_LEVEL_WARNING, "Could not create udev context");
return;
}
}
/* Scan for available input devices */
struct udev_enumerate *enumerate = udev_enumerate_new(context);
udev_enumerate_add_match_subsystem(enumerate, "input");
udev_enumerate_scan_devices(enumerate);
/* Prepare for enumerating found devices */
struct udev_list_entry *first_entry = udev_enumerate_get_list_entry(enumerate);
struct udev_list_entry *entry;
udev_list_entry_foreach(entry, first_entry) {
/* Obtain system path */
const char *path = udev_list_entry_get_name(entry);
/* Create udev device */
struct udev_device *device = udev_device_new_from_syspath(context, path);
if (!device) {
bbx_log(BBX_LOG_LEVEL_WARNING, "Could not create udev device for %s", path);
continue;
}
/* Connect device */
connect_udev_device(device);
/* Unreference udev device */
udev_device_unref(device);
}
/* Unreference enumeration */
udev_enumerate_unref(enumerate);
}
void bbx_indev_start_monitor() {
/* Make sure udev context is initialised */
if (!context) {
context = udev_new();
if (!context) {
bbx_log(BBX_LOG_LEVEL_WARNING, "Could not create udev context");
return;
}
}
/* Check if monitor is already running */
if (monitor) {
bbx_log(BBX_LOG_LEVEL_WARNING, "Not starting udev monitor because it is already running");
return;
}
/* Create new monitor */
monitor = udev_monitor_new_from_netlink(context, "udev");
if (!monitor) {
bbx_log(BBX_LOG_LEVEL_WARNING, "Could not create udev monitor");
bbx_indev_stop_monitor();
return;
}
/* Apply input subsystem filter */
if (udev_monitor_filter_add_match_subsystem_devtype(monitor, "input", NULL) < 0) {
bbx_log(BBX_LOG_LEVEL_WARNING, "Could not add input subsystem filter for udev monitor");
}
/* Start monitor */
if (udev_monitor_enable_receiving(monitor) < 0) {
bbx_log(BBX_LOG_LEVEL_WARNING, "Could not enable udev monitor");
bbx_indev_stop_monitor();
return;
}
/* Obtain monitor file descriptor */
if ((monitor_fd = udev_monitor_get_fd(monitor)) < 0) {
bbx_log(BBX_LOG_LEVEL_WARNING, "Could not acquire file descriptor for udev monitor");
bbx_indev_stop_monitor();
return;
}
}
void bbx_indev_stop_monitor() {
/* Unreference monitor */
if (monitor) {
udev_monitor_unref(monitor);
monitor = NULL;
}
/* Reset monitor file descriptor */
if (monitor_fd >= 0) {
monitor_fd = -1;
}
/* Unreference udev context */
if (context) {
udev_unref(context);
context = NULL;
}
}
void bbx_indev_query_monitor() {
/* Make sure the monitor is running */
if (!monitor) {
bbx_log(BBX_LOG_LEVEL_ERROR, "Cannot query udev monitor because it is not running");
return;
}
/* Prepare file descriptor set for reading */
fd_set fds;
FD_ZERO(&fds);
FD_SET(monitor_fd, &fds);
/* Set up timeval to not block when no updates are available */
struct timeval tv = { .tv_sec = 0, .tv_usec = 0 };
/* Read and process all updates */
while (select(monitor_fd + 1, &fds, NULL, NULL, &tv) > 0 && FD_ISSET(monitor_fd, &fds)) {
/* Obtain udev device */
struct udev_device *device = udev_monitor_receive_device(monitor);
if (!device) {
continue;
}
/* Obtain action */
const char *action = udev_device_get_action(device);
/* Connect or disconnect the device */
if (strcmp(action, "add") == 0) {
connect_udev_device(device);
} else if (strcmp(action, "remove") == 0) {
disconnect_udev_device(device);
}
/* Unreference udev device */
udev_device_unref(device);
}
}
bool bbx_indev_is_keyboard_connected() {
for (int i = 0; i < num_connected_devices; ++i) {
if (is_keyboard_device(devices[i])) {
return true;
}
}
return false;
}