[input] Implement hot-plug support for bindings

Hot-plug support is done via "connections" (not sure I'm happy with the
name) that provide a user specifiable name to input devices.  The
connections record the device name (eg, "6d spacemouse") and id (usually
usb path for evdev devices, but may be the device unique id if
available) and whether automatic reconnection should match just the
device name or both device name and id (prevents problems with changing
the device connected to the one usb port).
This commit is contained in:
Bill Currie 2021-11-10 13:22:44 +09:00
parent b95c749438
commit b231b63413
3 changed files with 267 additions and 109 deletions

View file

@ -92,13 +92,17 @@ typedef struct in_device_s {
\a name is used to specify the device when creating bindings, and \a name is used to specify the device when creating bindings, and
identifying the device for hints etc (eg, "press joy1 1 to ..."). identifying the device for hints etc (eg, "press joy1 1 to ...").
\a id is the device name (eg, "6d spacemouse") or (preferred) the device \a devname is the device name (eg, "6d spacemouse")
id (eg "usb-0000:00:1d.1-2/input0"). The device name is useful for ease of
user identification and allowing the device to be plugged into any USB \a id is the device id (eg "usb-0000:00:1d.1-2/input0").
socket. However, it doesn't allow multiple devices with the same name
(eg, using twin joysticks of the same model). Thus the device id is The device name is useful for ease of user identification and allowing
preferred, but does require the device to be plugged into the same uSB the device to be plugged into any USB socket. However, it doesn't allow
path (ie, same socket on the same hub connected to the same port on the PC) multiple devices with the same name (eg, using twin joysticks of the same
model). Thus if \a match_id is true, both the device name and the device
id used to auto-connect the device, but it does require the device to be
plugged into the same uSB path (ie, same socket on the same hub connected
to the same port on the PC)
\a devid is the actual device associated with the bindings. If -1, the \a devid is the actual device associated with the bindings. If -1, the
device is not currently connected. device is not currently connected.
@ -115,8 +119,11 @@ typedef struct in_device_s {
is the base index into the imt button bindings array. is the base index into the imt button bindings array.
*/ */
typedef struct in_devbindings_s { typedef struct in_devbindings_s {
const char *name; ///< name used when binding inputs struct in_devbindings_s *next;
const char *id; ///< physical device name or id (preferred) char *name; ///< name used when binding inputs
char *devname; ///< physical device name
char *id; ///< physical device id
int match_id; ///< if true, both devname and id must match
int devid; ///< id of device associated with these bindings int devid; ///< id of device associated with these bindings
int num_axes; int num_axes;
int num_buttons; int num_buttons;

View file

@ -40,7 +40,9 @@
#include "QF/cmd.h" #include "QF/cmd.h"
#include "QF/hash.h" #include "QF/hash.h"
#include "QF/heapsort.h"
#include "QF/input.h" #include "QF/input.h"
#include "QF/progs.h" // for PR_RESMAP
#include "QF/sys.h" #include "QF/sys.h"
#include "QF/input/imt.h" #include "QF/input/imt.h"
@ -49,67 +51,111 @@
#include "QF/input/event.h" #include "QF/input/event.h"
#include "QF/input/imt.h" #include "QF/input/imt.h"
typedef struct DARRAY_TYPE (in_devbindings_t) in_devbindingset_t; typedef struct DARRAY_TYPE (int) in_knowndevset_t;
static in_devbindingset_t devbindings = DARRAY_STATIC_INIT (8); static in_knowndevset_t known_devices = DARRAY_STATIC_INIT (8);
static int in_binding_handler; static int in_binding_handler;
static int in_keyhelp_handler; static int in_keyhelp_handler;
static int in_keyhelp_saved_handler; static int in_keyhelp_saved_handler;
static PR_RESMAP (in_devbindings_t) devbindings;
static in_devbindings_t *devbindings_list;
static int
devid_cmp (const void *a, const void *b)
{
return *(const int *)a - *(const int *)b;
}
static int *
in_find_devid (int devid)
{
return bsearch (&devid, known_devices.a, known_devices.size,
sizeof (int), devid_cmp);
}
static in_devbindings_t *
in_binding_find_connection (const char *devname, const char *id)
{
in_devbindings_t *db;
//FIXME slow
for (db = devbindings_list; db; db = db->next) {
if (strcmp (devname, db->devname) != 0) {
continue;
}
if (db->match_id && strcmp (id, db->id) != 0) {
continue;
}
return db;
}
return 0;
}
static void static void
in_binding_add_device (const IE_event_t *ie_event) in_binding_add_device (const IE_event_t *ie_event)
{ {
size_t devid = ie_event->device.devid; size_t devid = ie_event->device.devid;
if (devid >= devbindings.size) {
DARRAY_RESIZE (&devbindings, devid + 1); if (in_find_devid (devid)) {
memset (&devbindings.a[devid], 0, sizeof (in_devbindings_t)); // the device is already known. this is likely the result of a
} // broadcast of connected devices
in_devbindings_t *db = &devbindings.a[devid];
if (db->id) {
if (db->id != IN_GetDeviceId (devid)) {
Sys_Error ("in_binding_add_device: devid conflict: %zd", devid);
}
Sys_Printf ("in_binding_add_device: readd %s\n", db->id);
return; return;
} }
db->id = IN_GetDeviceId (devid); DARRAY_APPEND (&known_devices, devid);
db->devid = devid; // keep the known devices sorted by id
IN_AxisInfo (devid, 0, &db->num_axes); heapsort (known_devices.a, known_devices.size, sizeof (int), devid_cmp);
IN_ButtonInfo (devid, 0, &db->num_buttons);
db->axis_info = malloc (db->num_axes * sizeof (in_axisinfo_t) const char *devname = IN_GetDeviceName (devid);
+ db->num_buttons * sizeof (in_buttoninfo_t)); const char *id = IN_GetDeviceId (devid);
db->button_info = (in_buttoninfo_t *) &db->axis_info[db->num_axes]; in_devbindings_t *db = in_binding_find_connection (devname, id);
IN_AxisInfo (devid, db->axis_info, &db->num_axes);
IN_ButtonInfo (devid, db->button_info, &db->num_buttons); if (db) {
Sys_Printf ("in_binding_add_device: %s %d %d\n", db->id, db->num_axes, if (db->match_id) {
db->num_buttons); Sys_Printf ("Reconnected %s to %s %s\n", db->name, devname, id);
} else {
Sys_Printf ("Reconnected %s to %s\n", db->name, devname);
}
db->devid = devid;
IN_SetDeviceEventData (devid, db);
} else {
Sys_Printf ("Added device %s %s\n", devname, id);
}
} }
static void static void
in_binding_remove_device (const IE_event_t *ie_event) in_binding_remove_device (const IE_event_t *ie_event)
{ {
size_t devid = ie_event->device.devid; size_t devid = ie_event->device.devid;
in_devbindings_t *db = &devbindings.a[devid]; in_devbindings_t *db = IN_GetDeviceEventData (devid);
int *kd;
if (devid >= devbindings.size) { if (!(kd = in_find_devid (devid))) {
Sys_Error ("in_binding_remove_device: invalid devid: %zd", devid); Sys_Error ("in_binding_remove_device: invalid devid: %zd", devid);
} }
if (!db->id) { DARRAY_REMOVE_AT (&known_devices, kd - known_devices.a);
return;
const char *devname = IN_GetDeviceName (devid);
const char *id = IN_GetDeviceId (devid);
if (db) {
db->devid = -1;
if (db->match_id) {
Sys_Printf ("Disconnected %s from %s %s\n", db->name, devname, id);
} else {
Sys_Printf ("Disconnected %s from %s\n", db->name, devname);
}
} }
free (db->axis_info); // axis and button info in same block Sys_Printf ("Removed device %s %s\n", devname, id);
memset (db, 0, sizeof (*db));
} }
static void static void
in_binding_axis (const IE_event_t *ie_event) in_binding_axis (const IE_event_t *ie_event)
{ {
size_t devid = ie_event->axis.devid;
int axis = ie_event->axis.axis; int axis = ie_event->axis.axis;
int value = ie_event->axis.value; int value = ie_event->axis.value;
in_devbindings_t *db = &devbindings.a[devid]; in_devbindings_t *db = ie_event->axis.data;;
if (devid < devbindings.size && axis < db->num_axes) { if (db && axis < db->num_axes) {
db->axis_info[axis].value = value; db->axis_info[axis].value = value;
if (db->axis_imt_id) { if (db->axis_imt_id) {
IMT_ProcessAxis (db->axis_imt_id + axis, value); IMT_ProcessAxis (db->axis_imt_id + axis, value);
@ -120,12 +166,11 @@ in_binding_axis (const IE_event_t *ie_event)
static void static void
in_binding_button (const IE_event_t *ie_event) in_binding_button (const IE_event_t *ie_event)
{ {
size_t devid = ie_event->button.devid;
int button = ie_event->button.button; int button = ie_event->button.button;
int state = ie_event->button.state; int state = ie_event->button.state;
in_devbindings_t *db = &devbindings.a[devid]; in_devbindings_t *db = ie_event->button.data;
if (devid < devbindings.size && button < db->num_buttons) { if (db && button < db->num_buttons) {
db->button_info[button].state = state; db->button_info[button].state = state;
if (db->button_imt_id) { if (db->button_imt_id) {
IMT_ProcessButton (db->button_imt_id + button, state); IMT_ProcessButton (db->button_imt_id + button, state);
@ -150,6 +195,8 @@ in_binding_event_handler (const IE_event_t *ie_event, void *unused)
return 1; return 1;
} }
static int keyhelp_axis_threshold;
static int static int
in_keyhelp_event_handler (const IE_event_t *ie_event, void *unused) in_keyhelp_event_handler (const IE_event_t *ie_event, void *unused)
{ {
@ -158,25 +205,35 @@ in_keyhelp_event_handler (const IE_event_t *ie_event, void *unused)
} }
size_t devid = ie_event->button.devid; size_t devid = ie_event->button.devid;
in_devbindings_t *db = &devbindings.a[devid]; in_devbindings_t *db = ie_event->button.data;
const char *name = db->name; const char *name = db ? db->name : 0;
const char *type = 0; const char *type = 0;
int num = -1; int num = -1;
const char *devname = IN_GetDeviceName (devid);
if (!name) { const char *id = IN_GetDeviceId (devid);
name = db->id;
}
if (ie_event->type == ie_axis) { if (ie_event->type == ie_axis) {
int axis = ie_event->axis.axis; int axis = ie_event->axis.axis;
int value = ie_event->axis.value; int value = ie_event->axis.value;
in_axisinfo_t *ai = &db->axis_info[axis]; in_axisinfo_t *ai;
if (db) {
ai = &db->axis_info[axis];
} else {
//FIXME set single axis info entry
int num_axes;
in_axisinfo_t *axis_info;
IN_AxisInfo (devid, 0, &num_axes);
axis_info = alloca (num_axes * sizeof (in_axisinfo_t));
IN_AxisInfo (devid, axis_info, &num_axes);
ai = &axis_info[axis];
}
if (!ai->min && !ai->max) { if (!ai->min && !ai->max) {
if (value > 2) { if (abs (value) > keyhelp_axis_threshold) {
num = axis; num = axis;
type = "axis"; type = "axis";
} }
} else { } else {
//FIXME does not work if device has not been connected (db is null)
int diff = abs (value - ai->value); int diff = abs (value - ai->value);
if (diff * 5 >= ai->max - ai->min) { if (diff * 5 >= ai->max - ai->min) {
num = axis; num = axis;
@ -193,18 +250,18 @@ in_keyhelp_event_handler (const IE_event_t *ie_event, void *unused)
return 0; return 0;
} }
IE_Set_Focus (in_keyhelp_saved_handler); IE_Set_Focus (in_keyhelp_saved_handler);
Sys_Printf ("%s %s %d\n", name, type, num); Sys_Printf ("%s (%s %s) %s %d\n", name, devname, id, type, num);
return 1; return 1;
} }
static in_devbindings_t * static in_devbindings_t *
in_binding_find_device (const char *name) in_binding_find_device (const char *name)
{ {
for (size_t i = 0; i < devbindings.size; i++) { in_devbindings_t *db;
in_devbindings_t *dev = &devbindings.a[i];
if (strcmp (name, dev->id) == 0 for (db = devbindings_list; db; db = db->next) {
|| (dev->name && strcmp (name, dev->name) == 0)) { if (strcmp (name, db->name) == 0) {
return dev; break;
} }
} }
return 0; return 0;
@ -261,12 +318,18 @@ in_bind_f (void)
Sys_Printf ("invalid axis number: %s\n", number); Sys_Printf ("invalid axis number: %s\n", number);
return; return;
} }
if (dev->axis_imt_id == -1) {
dev->axis_imt_id = IMT_GetAxisBlock (dev->num_axes);
}
IMT_BindAxis (imt, dev->axis_imt_id + num, binding); IMT_BindAxis (imt, dev->axis_imt_id + num, binding);
} else { } else {
if (*end || num < 0 || num >= dev->num_buttons) { if (*end || num < 0 || num >= dev->num_buttons) {
Sys_Printf ("invalid button number: %s\n", number); Sys_Printf ("invalid button number: %s\n", number);
return; return;
} }
if (dev->button_imt_id == -1) {
dev->button_imt_id = IMT_GetAxisBlock (dev->num_buttons);
}
IMT_BindButton (imt, dev->button_imt_id + num, binding); IMT_BindButton (imt, dev->button_imt_id + num, binding);
} }
} }
@ -350,67 +413,150 @@ in_clear_f (void)
static void static void
in_devices_f (void) in_devices_f (void)
{ {
for (size_t i = 0; i < devbindings.size; i++) { for (size_t i = 0; i < known_devices.size; i++) {
in_devbindings_t *dev = &devbindings.a[i]; int devid = known_devices.a[i];
Sys_Printf ("%s %s %s\n", dev->name, dev->id, in_devbindings_t *db = IN_GetDeviceEventData (devid);
dev->devid >= 0 ? "connected" : "disconnected"); const char *name = IN_GetDeviceName (devid);
Sys_Printf (" axes: %d\n", dev->num_axes); const char *id = IN_GetDeviceId (devid);
Sys_Printf (" buttons: %d\n", dev->num_buttons); int num_axes, num_buttons;
IN_AxisInfo (devid, 0, &num_axes);
IN_ButtonInfo (devid, 0, &num_buttons);
Sys_Printf ("devid %d:\n", devid);
if (db) {
Sys_Printf (" bind name: %s\n", db->name);
} else {
Sys_Printf (" no bind name\n");
}
Sys_Printf (" name: %s\n", name);
Sys_Printf (" id: %s\n", id);
Sys_Printf (" axes: %d\n", num_axes);
Sys_Printf (" buttons: %d\n", num_buttons);
} }
} }
static void static void
in_devname_f (void) in_connect_f (void)
{ {
switch (Cmd_Argc ()) { int argc = Cmd_Argc ();
case 2: { const char *fullid = 0;
const char *name = Cmd_Argv (1);
in_devbindings_t *dev = in_binding_find_device (name); if (argc == 4) {
if (dev) { fullid = Cmd_Argv (3);
if (strcmp (name, dev->id) == 0) { }
Sys_Printf ("nickname for %s is %s\n", dev->id, dev->name); if (argc < 3 || argc > 4 || (fullid && strcmp (fullid, "fullid"))) {
} else { goto in_connect_usage;
Sys_Printf ("device_id for %s is %s\n", dev->name, dev->id); }
const char *bindname = Cmd_Argv (1);
const char *device_id = Cmd_Argv (2);
int devid = -1;
for (in_devbindings_t *db = devbindings_list; db; db = db->next) {
if (strcmp (bindname, db->name) == 0) {
Sys_Printf ("%s already exists\n", bindname);
return;
}
}
if (device_id[0] == '#') {
char *end;
devid = strtol (device_id + 1, &end, 0);
if (*end || !in_find_devid (devid)) {
Sys_Printf ("Not a valid device number: %s", device_id);
return;
}
} else {
int len = strlen (device_id);
for (size_t i = 0; i < known_devices.size; i++) {
if (strcmp (device_id, IN_GetDeviceId (known_devices.a[i])) == 0) {
devid = known_devices.a[i];
break;
}
if (strncasecmp (device_id,
IN_GetDeviceName (known_devices.a[i]),
len) == 0) {
if (devid > -1) {
Sys_Printf ("'%s' is ambiguous\n", device_id);
return;
} }
} else { devid = known_devices.a[i];
Sys_Printf ("No device identified by %s\n", name);
} }
break;
} }
case 3: { }
const char *device_id = Cmd_Argv (1); if (devid == -1) {
const char *nickname = Cmd_Argv (2); Sys_Printf ("No such device: %s\n", device_id);
in_devbindings_t *dev = in_binding_find_device (nickname); return;
if (dev) { }
Sys_Printf ("%s already exists: %s %s\n", if (IN_GetDeviceEventData (devid)) {
nickname, dev->name, dev->id); Sys_Printf ("%s already connected\n", device_id);
return; return;
} }
dev = in_binding_find_device (device_id);
if (!dev) { in_devbindings_t *db = PR_RESNEW (devbindings);
Sys_Printf ("%s does not exist\n", device_id); db->next = devbindings_list;
return; devbindings_list = db;
}
if (dev->name) { db->name = strdup (bindname);
free ((char *) dev->name); db->devname = strdup (IN_GetDeviceName (devid));
} db->id = strdup (IN_GetDeviceId (devid));
dev->name = strdup (nickname); db->match_id = !!fullid;
break; db->devid = devid;
IN_AxisInfo (devid, 0, &db->num_axes);
IN_ButtonInfo (devid, 0, &db->num_buttons);
db->axis_info = malloc (db->num_axes * sizeof (in_axisinfo_t)
+ db->num_buttons * sizeof (in_buttoninfo_t));
db->button_info = (in_buttoninfo_t *) &db->axis_info[db->num_axes];
IN_AxisInfo (devid, db->axis_info, &db->num_axes);
IN_ButtonInfo (devid, db->button_info, &db->num_buttons);
db->axis_imt_id = -1;
db->button_imt_id = -1;
IN_SetDeviceEventData (devid, db);
return;
in_connect_usage:
Sys_Printf ("in_connect bindname device_id [fullid]\n");
Sys_Printf (" Create a new device binding connection.\n");
Sys_Printf (" bindname: Connection name used for binding inputs\n.");
Sys_Printf (" device_id: Specify the device to be connected.\n");
Sys_Printf (" May be the numeric device number (#N), the device\n");
Sys_Printf (" name or device id as shown by in_devices.\n");
Sys_Printf (" fullid: if present, both device name and device id\n");
Sys_Printf (" will be used when automatically reconnecting the\n");
Sys_Printf (" device.\n");
}
static void
in_connections_f (void)
{
for (in_devbindings_t *db = devbindings_list; db; db = db->next) {
if (db->match_id) {
Sys_Printf ("%s: %s %s\n", db->name, db->devname, db->id);
} else {
Sys_Printf ("%s: %s\n", db->name, db->devname);
}
if (db->devid > -1) {
Sys_Printf (" connected\n");
} else {
Sys_Printf (" disconnected\n");
} }
default:
Sys_Printf ("in_devname device_id nickname\n");
Sys_Printf (" Name a deviced identified by device_id as nickname\n");
Sys_Printf ("in_devname device_id\n");
Sys_Printf (" Show the nickname given to the device identified by device_id\n");
Sys_Printf ("in_devname nickname\n");
Sys_Printf (" Show the device_id of the device named by nickname\n");
break;
} }
} }
static void static void
keyhelp_f (void) keyhelp_f (void)
{ {
keyhelp_axis_threshold = 3;
if (Cmd_Argc () > 1) {
char *end;
int threshold = strtol (Cmd_Argv (1), &end, 0);
if (!*end && threshold > 0) {
keyhelp_axis_threshold = threshold;
}
}
in_keyhelp_saved_handler = IE_Get_Focus (); in_keyhelp_saved_handler = IE_Get_Focus ();
IE_Set_Focus (in_keyhelp_handler); IE_Set_Focus (in_keyhelp_handler);
Sys_Printf ("Press button or move axis to identify\n"); Sys_Printf ("Press button or move axis to identify\n");
@ -437,8 +583,13 @@ static bindcmd_t in_binding_commands[] = {
{ "in_devices", in_devices_f, { "in_devices", in_devices_f,
"List the known devices and their status." "List the known devices and their status."
}, },
{ "in_devname", in_devname_f, { "in_connect", in_connect_f,
"Give a device a nickname." "Create a device binding connection. Supports hot-plug in that the "
"device will be automatically reconnected when plugged in or"
PACKAGE_NAME " is restarted."
},
{ "in_connections", in_connections_f,
"List device bindings and statuses."
}, },
{ "keyhelp", keyhelp_f, { "keyhelp", keyhelp_f,
"Identify the next active input axis or button.\n" "Identify the next active input axis or button.\n"

View file

@ -150,13 +150,13 @@ device_add (device_t *dev)
} }
devmap_list = dm; devmap_list = dm;
dm->device = dev;
dm->devid = IN_AddDevice (evdev_driver_handle, dev, name, id);
dev->data = dm; dev->data = dm;
dev->axis_event = in_evdev_axis_event; dev->axis_event = in_evdev_axis_event;
dev->button_event = in_evdev_button_event; dev->button_event = in_evdev_button_event;
dm->device = dev;
dm->devid = IN_AddDevice (evdev_driver_handle, dev, name, id);
#if 0 #if 0
Sys_Printf ("in_evdev: add %s\n", dev->path); Sys_Printf ("in_evdev: add %s\n", dev->path);
Sys_Printf (" %s\n", dev->name); Sys_Printf (" %s\n", dev->name);