diff options
-rw-r--r-- | README.md | 76 | ||||
-rw-r--r-- | configs/uinput-midi.cfg | 24 | ||||
-rw-r--r-- | evdev.c | 379 | ||||
-rw-r--r-- | evdev.h | 24 | ||||
-rw-r--r-- | makefile | 5 | ||||
-rw-r--r-- | monster.cfg | 28 | ||||
-rw-r--r-- | plugin.c | 2 |
7 files changed, 518 insertions, 20 deletions
@@ -143,6 +143,81 @@ NRPNs are not yet fully implemented, though rudimentary support is in the codeba The channel specification syntax is currently a bit clunky. +### The `evdev` backend + +This backend allows using Linux `evdev` devices such as mouses, keyboards, gamepads and joysticks +as input and output devices. All buttons and axes available to the Linux system are mappable. +Output is provided by the `uinput` kernel module, which allows creation of virtual input devices. +This functionality may require elevated privileges (such as special group membership or root access). + +#### Global configuration + +This backend does not take any global configuration. + +#### Instance configuration + +| Option | Example value | Default value | Description | +|---------------|-----------------------|---------------|-----------------------------------------------| +| `input` | `/dev/input/event1` | none | `evdev` device to use as input device | +| `exclusive` | `1` | `0` | Prevent other processes from using the device | +| `name` | `My Input Device` | none | Output device presentation name. Setting this option enables the instance for output | +| `id` | `0x1 0x2 0x3` | none | Set output device bus identification (Vendor, Product and Version), optional | +| `axis.AXISNAME`| `34300 0 65536 255 4095` | none | Specify absolute axis details (see below) for output. This is required for any absolute axis to be output. + +The absolute axis details configuration is required for any absolute axis on output-enabled instances. +The configuration value contains, space-separated, the following values: + +* `value`: The value to assume for the axis until an event is received +* `minimum`: The axis minimum value +* `maximum`: The axis maximum value +* `fuzz`: A value used for filtering the input stream +* `flat`: An offset, below which all deviations will be ignored +* `resolution`: Axis resolution in units per millimeter (or units per radian for rotational axes) + +For real devices, all of these parameters for every axis can be found by running `evtest` on the device. + +#### Channel specification + +A channel is specified by its event type and event code, separated by `.`. For a complete list of event types and codes +see the kernel sources. The most interesting event types are + +* `EV_KEY` for keys and buttons +* `EV_ABS` for absolute axes (such as Joysticks) +* `EV_REL` for relative axes (such as Mouses) + +The `evtest` tool is useful to gather information on devices active on the local system, including types, codes +and configuration supported by these devices. + +Example mapping: +``` +ev1.EV_KEY.KEY_A > ev1.EV_ABS.ABS_X +``` + +Note that to map an absolute axis on an output-enabled instance, additional information such as the axis minimum +and maximum are required. These must be specified in the instance configuration. When only mapping the instance +as a channel input, this is not required. + +#### Known bugs/problems + +Creating an `evdev` output device requires elevated privileges, namely, write access to the system's +`/dev/uinput`. Usually, this is granted for users in the `input` group and the `root` user. + +Input devices may synchronize logically connected event types (for example, X and Y axes) via `EV_SYN`-type +events. The MIDIMonster also generates these events after processing channel events, but may not keep the original +event grouping. + +Relative axes (`EV_REL`-type events), such as generated by mouses, are currently handled in a very basic fashion, +generating only the normalized channel values of `0`, `0.5` and `1` for any input less than, equal to and greater +than `0`, respectively. As for output, only the values `-1`, `0` and `1` are generated for the same interval. + +`EV_KEY` key-down events are sent for normalized channel values over `0.9`. + +Extended event type values such as `EV_LED`, `EV_SND`, etc are recognized in the MIDIMonster configuration file +but may or may not work with the internal channel mapping and normalization code. + +Input devices can currently only be specified by device node directly. There may be a facility to open input +devices by presentation name in the future. + ### The `loopback` backend This backend allows the user to create logical mapping channels, for example to exchange triggering @@ -265,6 +340,7 @@ In order to build the MIDIMonster, you'll need some libraries that provide support for the protocols to translate. * libasound2-dev (for the MIDI backend) +* libevdev-dev (for the evdev backend) * A C compiler * GNUmake diff --git a/configs/uinput-midi.cfg b/configs/uinput-midi.cfg new file mode 100644 index 0000000..d33e4df --- /dev/null +++ b/configs/uinput-midi.cfg @@ -0,0 +1,24 @@ +; Note that this configuration file was originally written with +; an older syntax and thus only contains right-to-left mappings + +[backend midi] +name = MIDIMonster + +[uinput mouse] +device = /dev/input/by-id/usb-Logitech_USB-PS_2_Optical_Mouse-event-mouse + +; MIDI input devices +[midi lc1] +read = Launch +write = Launch + +[map] +; LC Button +mouse.1.272 > lc1.note0.0 +mouse.1.273 > lc1.note0.1 +;out.note0.50 > lc1.note0.2 +;out.note0.51 > lc1.note0.3 +;out.note0.52 > lc1.note0.4 +;out.note0.53 > lc1.note0.5 +;out.note0.54 > lc1.note0.6 +;out.note0.55 > lc1.note0.7 @@ -0,0 +1,379 @@ +#include <fcntl.h> +#include <stdio.h> +#include <errno.h> +#include <string.h> +#include <unistd.h> +#include <sys/ioctl.h> +#include <libevdev/libevdev.h> +#include <libevdev/libevdev-uinput.h> + +#include "midimonster.h" +#include "evdev.h" + +#define BACKEND_NAME "evdev" + +typedef union { + struct { + uint32_t pad; + uint16_t type; + uint16_t code; + } fields; + uint64_t label; +} evdev_channel_ident; + +int init(){ + backend evdev = { + .name = BACKEND_NAME, + .conf = evdev_configure, + .create = evdev_instance, + .conf_instance = evdev_configure_instance, + .channel = evdev_channel, + .handle = evdev_set, + .process = evdev_handle, + .start = evdev_start, + .shutdown = evdev_shutdown + }; + + if(mm_backend_register(evdev)){ + fprintf(stderr, "Failed to register evdev backend\n"); + return 1; + } + + return 0; +} + +static int evdev_configure(char* option, char* value) { + fprintf(stderr, "The evdev backend does not take any global configuration\n"); + return 1; +} + +static instance* evdev_instance(){ + instance* inst = mm_instance(); + if(!inst){ + return NULL; + } + + evdev_instance_data* data = calloc(1, sizeof(evdev_instance_data)); + if(!data){ + fprintf(stderr, "Failed to allocate memory\n"); + return NULL; + } + + data->input_fd = -1; + data->output_proto = libevdev_new(); + if(!data->output_proto){ + fprintf(stderr, "Failed to initialize libevdev output prototype device\n"); + free(data); + return NULL; + } + + inst->impl = data; + return inst; +} + +static int evdev_configure_instance(instance* inst, char* option, char* value) { + evdev_instance_data* data = (evdev_instance_data*) inst->impl; + char* next_token = NULL; + struct input_absinfo abs_info = { + 0 + }; + + if(!strcmp(option, "input")){ + if(data->input_fd >= 0){ + fprintf(stderr, "Instance %s already was assigned an input device\n", inst->name); + return 1; + } + + data->input_fd = open(value, O_RDONLY | O_NONBLOCK); + if(data->input_fd < 0){ + fprintf(stderr, "Failed to open evdev input device node %s: %s\n", value, strerror(errno)); + return 1; + } + + if(libevdev_new_from_fd(data->input_fd, &data->input_ev)){ + fprintf(stderr, "Failed to initialize libevdev for %s\n", value); + close(data->input_fd); + data->input_fd = -1; + return 1; + } + + if(data->exclusive && libevdev_grab(data->input_ev, LIBEVDEV_GRAB)){ + fprintf(stderr, "Failed to obtain exclusive device access on %s\n", value); + } + } + else if(!strcmp(option, "exclusive")){ + if(data->input_fd >= 0 && libevdev_grab(data->input_ev, LIBEVDEV_GRAB)){ + fprintf(stderr, "Failed to obtain exclusive device access on %s\n", inst->name); + } + data->exclusive = 1; + } + else if(!strcmp(option, "name")){ + data->output_enabled = 1; + libevdev_set_name(data->output_proto, value); + } + else if(!strcmp(option, "id")){ + next_token = value; + libevdev_set_id_vendor(data->output_proto, strtol(next_token, &next_token, 0)); + libevdev_set_id_product(data->output_proto, strtol(next_token, &next_token, 0)); + libevdev_set_id_version(data->output_proto, strtol(next_token, &next_token, 0)); + } + else if(!strncmp(option, "axis.", 5)){ + //value minimum maximum fuzz flat resolution + next_token = value; + abs_info.value = strtol(next_token, &next_token, 0); + abs_info.minimum = strtol(next_token, &next_token, 0); + abs_info.maximum = strtol(next_token, &next_token, 0); + abs_info.fuzz = strtol(next_token, &next_token, 0); + abs_info.flat = strtol(next_token, &next_token, 0); + abs_info.resolution = strtol(next_token, &next_token, 0); + if(libevdev_enable_event_code(data->output_proto, EV_ABS, libevdev_event_code_from_name(EV_ABS, option + 5), &abs_info)){ + fprintf(stderr, "Failed to enable absolute axis %s for output\n", option + 5); + return 1; + } + } + else{ + fprintf(stderr, "Unknown configuration parameter %s for evdev backend\n", option); + return 1; + } + return 0; +} + +static channel* evdev_channel(instance* inst, char* spec){ + evdev_instance_data* data = (evdev_instance_data*) inst->impl; + char* separator = strchr(spec, '.'); + evdev_channel_ident ident = { + .label = 0 + }; + + if(!separator){ + fprintf(stderr, "Invalid evdev channel specification %s\n", spec); + return NULL; + } + + *(separator++) = 0; + + if(libevdev_event_type_from_name(spec) < 0){ + fprintf(stderr, "Invalid evdev type specification: %s", spec); + return NULL; + } + ident.fields.type = libevdev_event_type_from_name(spec); + + if(libevdev_event_code_from_name(ident.fields.type, separator) >= 0){ + ident.fields.code = libevdev_event_code_from_name(ident.fields.type, separator); + } + else{ + fprintf(stderr, "evdev Code name not recognized, using as number: %s\n", separator); + ident.fields.code = strtoul(separator, NULL, 10); + } + + if(data->output_enabled){ + if(!libevdev_has_event_code(data->output_proto, ident.fields.type, ident.fields.code)){ + //enable the event on the device + //the previous check is necessary to not fail while enabling axes, which require additional information + if(libevdev_enable_event_code(data->output_proto, ident.fields.type, ident.fields.code, NULL)){ + fprintf(stderr, "Failed to enable output event %s.%s%s\n", + libevdev_event_type_get_name(ident.fields.type), + libevdev_event_code_get_name(ident.fields.type, ident.fields.code), + (ident.fields.type == EV_ABS) ? ": To output absolute axes, specify their details in the configuration":""); + return NULL; + } + } + } + + return mm_channel(inst, ident.label, 1); +} + +static int evdev_push_event(instance* inst, evdev_instance_data* data, struct input_event event){ + uint64_t range = 0; + channel_value val; + evdev_channel_ident ident = { + .fields.type = event.type, + .fields.code = event.code + }; + channel* chan = mm_channel(inst, ident.label, 0); + + if(chan){ + val.raw.u64 = event.value; + switch(event.type){ + case EV_REL: + val.normalised = 0.5 + ((event.value < 0) ? 0.5 : -0.5); + break; + case EV_ABS: + range = libevdev_get_abs_maximum(data->input_ev, event.code) - libevdev_get_abs_minimum(data->input_ev, event.code); + val.normalised = (event.value - libevdev_get_abs_minimum(data->input_ev, event.code)) / (double) range; + break; + case EV_KEY: + case EV_SW: + default: + val.normalised = 1.0 * event.value; + break; + } + + if(mm_channel_event(chan, val)){ + fprintf(stderr, "Failed to push evdev channel event to core\n"); + return 1; + } + } + + return 0; +} + +static int evdev_handle(size_t num, managed_fd* fds){ + instance* inst = NULL; + evdev_instance_data* data = NULL; + size_t fd; + unsigned int read_flags = LIBEVDEV_READ_FLAG_NORMAL; + int read_status; + struct input_event ev; + + if(!num){ + return 0; + } + + for(fd = 0; fd < num; fd++){ + inst = (instance*) fds[fd].impl; + if(!inst){ + fprintf(stderr, "evdev backend signaled for unknown fd\n"); + continue; + } + + data = (evdev_instance_data*) inst->impl; + + for(read_status = libevdev_next_event(data->input_ev, read_flags, &ev); read_status >= 0; read_status = libevdev_next_event(data->input_ev, read_flags, &ev)){ + read_flags = LIBEVDEV_READ_FLAG_NORMAL; + if(read_status == LIBEVDEV_READ_STATUS_SYNC){ + read_flags = LIBEVDEV_READ_FLAG_SYNC; + } + + //handle event + if(evdev_push_event(inst, data, ev)){ + return 1; + } + } + } + + return 0; +} + +static int evdev_start(){ + size_t n, u, fds = 0; + instance** inst = NULL; + evdev_instance_data* data = NULL; + + if(mm_backend_instances(BACKEND_NAME, &n, &inst)){ + fprintf(stderr, "Failed to fetch instance list\n"); + return 1; + } + + if(!n){ + free(inst); + return 0; + } + + for(u = 0; u < n; u++){ + data = (evdev_instance_data*) inst[u]->impl; + + if(data->output_enabled){ + if(libevdev_uinput_create_from_device(data->output_proto, LIBEVDEV_UINPUT_OPEN_MANAGED, &data->output_ev)){ + fprintf(stderr, "Failed to create evdev output device: %s\n", strerror(errno)); + return 1; + } + fprintf(stderr, "Created device node %s for instance %s\n", libevdev_uinput_get_devnode(data->output_ev), inst[u]->name); + } + + inst[u]->ident = data->input_fd; + if(data->input_fd >= 0){ + if(mm_manage_fd(data->input_fd, BACKEND_NAME, 1, inst[u])){ + fprintf(stderr, "Failed to register event input descriptor for instance %s\n", inst[u]->name); + free(inst); + return 1; + } + fds++; + } + + } + + fprintf(stderr, "evdev backend registered %zu descriptors to core\n", fds); + free(inst); + return 0; +} + +static int evdev_set(instance* inst, size_t num, channel** c, channel_value* v) { + size_t evt = 0; + evdev_instance_data* data = (evdev_instance_data*) inst->impl; + evdev_channel_ident ident = { + .label = 0 + }; + int32_t value = 0; + uint64_t range = 0; + + if(!num){ + return 0; + } + + if(!data->output_enabled){ + fprintf(stderr, "Instance %s not enabled for output\n", inst->name); + return 0; + } + + for(evt = 0; evt < num; evt++){ + ident.label = c[evt]->ident; + + switch(ident.fields.type){ + case EV_REL: + value = (v[evt].normalised < 0.5) ? -1 : ((v[evt].normalised > 0.5) ? 1 : 0); + break; + case EV_ABS: + range = libevdev_get_abs_maximum(data->output_proto, ident.fields.code) - libevdev_get_abs_minimum(data->output_proto, ident.fields.code); + value = (range * v[evt].normalised) + libevdev_get_abs_minimum(data->output_proto, ident.fields.code); + break; + case EV_KEY: + case EV_SW: + default: + value = (v[evt].normalised > 0.9) ? 1 : 0; + break; + } + + if(libevdev_uinput_write_event(data->output_ev, ident.fields.type, ident.fields.code, value)){ + fprintf(stderr, "Failed to output event on instance %s\n", inst->name); + return 1; + } + } + + //send syn event to publish all events + if(libevdev_uinput_write_event(data->output_ev, EV_SYN, SYN_REPORT, 0)){ + fprintf(stderr, "Failed to output sync event on instance %s\n", inst->name); + return 1; + } + + return 0; +} + +static int evdev_shutdown(){ + evdev_instance_data* data = NULL; + instance** instances = NULL; + size_t n, u; + + if(mm_backend_instances(BACKEND_NAME, &n, &instances)){ + fprintf(stderr, "Failed to fetch instance list\n"); + return 1; + } + + for(u = 0; u < n; u++){ + data = (evdev_instance_data*) instances[u]->impl; + + if(data->input_fd >= 0){ + libevdev_free(data->input_ev); + close(data->input_fd); + } + + if(data->output_enabled){ + libevdev_uinput_destroy(data->output_ev); + } + + libevdev_free(data->output_proto); + } + + free(instances); + return 0; +} @@ -0,0 +1,24 @@ +#include <sys/types.h> +#include <linux/input.h> + +#include "midimonster.h" + +int init(); +static int evdev_configure(char* option, char* value); +static int evdev_configure_instance(instance* instance, char* option, char* value); +static instance* evdev_instance(); +static channel* evdev_channel(instance* instance, char* spec); +static int evdev_set(instance* inst, size_t num, channel** c, channel_value* v); +static int evdev_handle(size_t num, managed_fd* fds); +static int evdev_start(); +static int evdev_shutdown(); + +typedef struct /*_evdev_instance_model*/ { + int input_fd; + struct libevdev* input_ev; + int exclusive; + + int output_enabled; + struct libevdev* output_proto; + struct libevdev_uinput* output_ev; +} evdev_instance_data; @@ -1,5 +1,5 @@ .PHONY: clean -BACKENDS = artnet.so midi.so osc.so loopback.so +BACKENDS = artnet.so midi.so osc.so loopback.so evdev.so OBJS = config.o backend.o plugin.o PLUGINDIR = "\"./\"" @@ -11,6 +11,9 @@ CFLAGS ?= -g -Wall midimonster: LDLIBS = -ldl midimonster: CFLAGS += -rdynamic -DPLUGINS=$(PLUGINDIR) midi.so: LDLIBS = -lasound +evdev.so: CFLAGS += $(shell pkg-config --cflags libevdev) +evdev.so: LDLIBS = $(shell pkg-config --libs libevdev) + %.so :: %.c %.h $(CC) $(CFLAGS) $(LDLIBS) $< -o $@ $(LDFLAGS) diff --git a/monster.cfg b/monster.cfg index 558478e..33bedeb 100644 --- a/monster.cfg +++ b/monster.cfg @@ -1,25 +1,17 @@ [backend midi] name = MIDIMonster -[backend artnet] -bind = * -net = 0 +[evdev test] +input = /dev/input/event14 +name = barfoo +axis.ABS_X = 34300 0 65535 255 4095 +axis.ABS_RZ = 34300 0 65535 255 4095 +axis.ABS_Y = 34300 0 65535 255 4095 -[artnet net1] -iface = 0 -uni = 0 -dest = 255.255.255.255 +[midi foo] -[osc osc1] -bind = * 8000 -dest = learn@8001 -/1/fader1 = f 0.0 1.0 -/1/fader2 = f 0.0 1.0 -/1/fader3 = f 0.0 1.0 -/1/fader4 = f 0.0 1.0 -/1/xy = ff 0.0 1.0 0.0 1.0 [map] -osc1./1/xy:0 <> net1.1+2 -osc1./1/xy:1 <> net1.3+4 -osc1./1/fader1 <> net1.20+21 +test.EV_KEY.BTN_SOUTH > test.EV_KEY.KEY_A +test.EV_ABS.ABS_RZ > test.EV_ABS.ABS_Y +test.EV_ABS.ABS_X > test.EV_KEY.KEY_B @@ -42,12 +42,12 @@ static int plugin_attach(char* path, char* file){ else{ return 0; } + free(lib); plugin_handle = realloc(plugin_handle, (plugins + 1) * sizeof(void*)); if(!plugin_handle){ fprintf(stderr, "Failed to allocate memory\n"); dlclose(handle); - free(lib); return 1; } |