359 lines
11 KiB
C
359 lines
11 KiB
C
// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
|
|
// See LICENSE.md for license information.
|
|
// SPDX-License-Identifier: MIT
|
|
//
|
|
// virtualPad.c - two virtual gamepads for the joystick gates of the ports
|
|
// that read the host's gamepads (SDL3, Win32 under Wine, the web port in
|
|
// Chromium). Linux only: they are uinput devices.
|
|
//
|
|
// virtualPad [--generic] [NAME] NAME prefixes the devices' names
|
|
// (default "JoeyLib gate")
|
|
//
|
|
// Makes two uinput devices laid out as Xbox 360 pads (the xpad driver's
|
|
// buttons and axes, Microsoft's USB ids), which SDL's, Wine's, and
|
|
// Chromium's gamepad mappings all know: stick A is the first pad ("NAME pad
|
|
// A"), stick B the second. With --generic they are plain joysticks instead
|
|
// -- an X and a Y axis, a trigger and a thumb button, and pid.codes' test
|
|
// ids -- which no gamepad mapping knows, so SDL offers them only as raw
|
|
// joysticks. Then reads lines on stdin and holds or lets go of the controls
|
|
// named:
|
|
//
|
|
// press CONTROL... CONTROL as joyGate.sh names them: A-left A-right
|
|
// release CONTROL... A-up A-down A-button0 A-button1, and the same for B
|
|
// quit remove the pads (as does end of input)
|
|
//
|
|
// The left stick moves to the end of its travel (left/up -32767,
|
|
// right/down +32767; holding both of an axis's directions is the positive
|
|
// one), button 0 is the south button (A) and button 1 the east (B) -- on a
|
|
// generic stick, the trigger and the thumb button. Prints "ready" once both
|
|
// devices exist, "ok" after each line it has applied, and "error: ..." for
|
|
// a word it does not know (a line with an unknown command gets no "ok").
|
|
//
|
|
// Needs write access to /dev/uinput (a desktop session's user usually has
|
|
// it through udev's uaccess rule); exit 1 without it. The pads are the
|
|
// whole machine's while they exist, not only the gate's.
|
|
|
|
#define _DEFAULT_SOURCE
|
|
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/ioctl.h>
|
|
#include <time.h>
|
|
#include <unistd.h>
|
|
|
|
#include <linux/input.h>
|
|
#include <linux/uinput.h>
|
|
|
|
#include "gateNet.h"
|
|
#include "toolFile.h"
|
|
|
|
#define TOOL_NAME "virtualPad"
|
|
#define UINPUT_PATH "/dev/uinput"
|
|
#define GENERIC_OPTION "--generic"
|
|
#define DEFAULT_PREFIX "JoeyLib gate"
|
|
#define PAD_COUNT 2u
|
|
#define NS_PER_US 1000L
|
|
// How long the kernel and the readers get to see a new device.
|
|
#define SETTLE_SECONDS 1.0
|
|
#define WORD_SPACE " \t\r\n\v\f"
|
|
#define STICK_SEPARATOR '-'
|
|
|
|
#define XBOX360_VENDOR 0x045Eu
|
|
#define XBOX360_PRODUCT 0x028Eu
|
|
#define XBOX360_VERSION 0x0110u
|
|
// pid.codes' vendor id and its test product, in no gamepad database.
|
|
#define GENERIC_VENDOR 0x1209u
|
|
#define GENERIC_PRODUCT 0x0001u
|
|
#define GENERIC_VERSION 0x0100u
|
|
|
|
#define STICK_MAX 32767
|
|
#define STICK_MIN (-32768)
|
|
#define STICK_FUZZ 16
|
|
#define STICK_FLAT 128
|
|
#define TRIGGER_MAX 255
|
|
#define HAT_MIN (-1)
|
|
#define HAT_MAX 1
|
|
#define FULL STICK_MAX
|
|
|
|
// The lines a stick has, as the commands name them.
|
|
typedef enum LineE {
|
|
LINE_LEFT = 0,
|
|
LINE_RIGHT,
|
|
LINE_UP,
|
|
LINE_DOWN,
|
|
LINE_BUTTON0,
|
|
LINE_BUTTON1,
|
|
LINE_COUNT
|
|
} LineE;
|
|
|
|
// One axis: its code and range.
|
|
typedef struct AxisT {
|
|
uint16_t code;
|
|
int32_t min;
|
|
int32_t max;
|
|
int32_t fuzz;
|
|
int32_t flat;
|
|
} AxisT;
|
|
|
|
// What a pad is: its ids, its buttons and axes, and the buttons that are
|
|
// button 0 and button 1.
|
|
typedef struct LayoutT {
|
|
uint16_t vendor;
|
|
uint16_t product;
|
|
uint16_t version;
|
|
const uint16_t *buttons;
|
|
size_t buttonCount;
|
|
const AxisT *axes;
|
|
size_t axisCount;
|
|
uint16_t button0;
|
|
uint16_t button1;
|
|
} LayoutT;
|
|
|
|
// A pad made: its uinput device, its layout, and the lines held.
|
|
typedef struct PadT {
|
|
int fd;
|
|
const LayoutT *layout;
|
|
bool held[LINE_COUNT];
|
|
} PadT;
|
|
|
|
static void padApply(const PadT *pad);
|
|
static void padClose(PadT *pad);
|
|
static void padEmit(const PadT *pad, uint16_t type, uint16_t code, int32_t value);
|
|
static void padIoctl(int fd, unsigned long request, unsigned long value);
|
|
static void padOpen(PadT *pad, const char *prefix, char stick, const LayoutT *layout);
|
|
static bool padsCommand(PadT *pads, char *line);
|
|
static void uinputFail(void) __attribute__((noreturn));
|
|
|
|
static const char *const kLineNames[LINE_COUNT] = {
|
|
"left",
|
|
"right",
|
|
"up",
|
|
"down",
|
|
"button0",
|
|
"button1"
|
|
};
|
|
|
|
static const uint16_t kXbox360Buttons[] = {
|
|
BTN_SOUTH, BTN_EAST, BTN_NORTH, BTN_WEST, BTN_TL, BTN_TR,
|
|
BTN_SELECT, BTN_START, BTN_MODE, BTN_THUMBL, BTN_THUMBR
|
|
};
|
|
static const uint16_t kGenericButtons[] = {
|
|
BTN_TRIGGER, BTN_THUMB
|
|
};
|
|
|
|
// The stick's axes first: a generic pad has those alone.
|
|
static const AxisT kXbox360Axes[] = {
|
|
{ABS_X, STICK_MIN, STICK_MAX, STICK_FUZZ, STICK_FLAT},
|
|
{ABS_Y, STICK_MIN, STICK_MAX, STICK_FUZZ, STICK_FLAT},
|
|
{ABS_RX, STICK_MIN, STICK_MAX, STICK_FUZZ, STICK_FLAT},
|
|
{ABS_RY, STICK_MIN, STICK_MAX, STICK_FUZZ, STICK_FLAT},
|
|
{ABS_Z, 0, TRIGGER_MAX, 0, 0},
|
|
{ABS_RZ, 0, TRIGGER_MAX, 0, 0},
|
|
{ABS_HAT0X, HAT_MIN, HAT_MAX, 0, 0},
|
|
{ABS_HAT0Y, HAT_MIN, HAT_MAX, 0, 0}
|
|
};
|
|
#define STICK_AXES 2u
|
|
|
|
static const LayoutT kXbox360 = {
|
|
XBOX360_VENDOR, XBOX360_PRODUCT, XBOX360_VERSION,
|
|
kXbox360Buttons, sizeof(kXbox360Buttons) / sizeof(kXbox360Buttons[0]),
|
|
kXbox360Axes, sizeof(kXbox360Axes) / sizeof(kXbox360Axes[0]),
|
|
BTN_SOUTH, BTN_EAST
|
|
};
|
|
static const LayoutT kGeneric = {
|
|
GENERIC_VENDOR, GENERIC_PRODUCT, GENERIC_VERSION,
|
|
kGenericButtons, sizeof(kGenericButtons) / sizeof(kGenericButtons[0]),
|
|
kXbox360Axes, STICK_AXES,
|
|
BTN_TRIGGER, BTN_THUMB
|
|
};
|
|
|
|
static const char kStickNames[PAD_COUNT] = {'A', 'B'};
|
|
|
|
|
|
// Send the axes and buttons the held lines make, then a report.
|
|
static void padApply(const PadT *pad) {
|
|
int32_t x;
|
|
int32_t y;
|
|
|
|
x = pad->held[LINE_RIGHT] ? FULL : (pad->held[LINE_LEFT] ? -FULL : 0);
|
|
y = pad->held[LINE_DOWN] ? FULL : (pad->held[LINE_UP] ? -FULL : 0);
|
|
padEmit(pad, EV_ABS, ABS_X, x);
|
|
padEmit(pad, EV_ABS, ABS_Y, y);
|
|
padEmit(pad, EV_KEY, pad->layout->button0, pad->held[LINE_BUTTON0] ? 1 : 0);
|
|
padEmit(pad, EV_KEY, pad->layout->button1, pad->held[LINE_BUTTON1] ? 1 : 0);
|
|
padEmit(pad, EV_SYN, SYN_REPORT, 0);
|
|
}
|
|
|
|
|
|
static void padClose(PadT *pad) {
|
|
padIoctl(pad->fd, UI_DEV_DESTROY, 0u);
|
|
close(pad->fd);
|
|
}
|
|
|
|
|
|
static void padEmit(const PadT *pad, uint16_t type, uint16_t code, int32_t value) {
|
|
struct input_event event;
|
|
struct timespec now;
|
|
|
|
memset(&event, 0, sizeof(event));
|
|
clock_gettime(CLOCK_REALTIME, &now);
|
|
event.input_event_sec = now.tv_sec;
|
|
event.input_event_usec = now.tv_nsec / NS_PER_US;
|
|
event.type = type;
|
|
event.code = code;
|
|
event.value = value;
|
|
if (write(pad->fd, &event, sizeof(event)) != (ssize_t)sizeof(event)) {
|
|
uinputFail();
|
|
}
|
|
}
|
|
|
|
|
|
// One uinput ioctl taking a number; a failure ends the tool.
|
|
static void padIoctl(int fd, unsigned long request, unsigned long value) {
|
|
if (ioctl(fd, request, value) != 0) {
|
|
uinputFail();
|
|
}
|
|
}
|
|
|
|
|
|
// The pad for stick, named "<prefix> pad <stick>" (cut to what uinput holds).
|
|
static void padOpen(PadT *pad, const char *prefix, char stick, const LayoutT *layout) {
|
|
struct uinput_abs_setup abs;
|
|
struct uinput_setup setup;
|
|
size_t i;
|
|
|
|
memset(pad, 0, sizeof(*pad));
|
|
pad->layout = layout;
|
|
pad->fd = open(UINPUT_PATH, O_WRONLY | O_NONBLOCK);
|
|
if (pad->fd < 0) {
|
|
uinputFail();
|
|
}
|
|
padIoctl(pad->fd, UI_SET_EVBIT, EV_KEY);
|
|
for (i = 0; i < layout->buttonCount; i++) {
|
|
padIoctl(pad->fd, UI_SET_KEYBIT, layout->buttons[i]);
|
|
}
|
|
padIoctl(pad->fd, UI_SET_EVBIT, EV_ABS);
|
|
for (i = 0; i < layout->axisCount; i++) {
|
|
memset(&abs, 0, sizeof(abs));
|
|
abs.code = layout->axes[i].code;
|
|
abs.absinfo.minimum = layout->axes[i].min;
|
|
abs.absinfo.maximum = layout->axes[i].max;
|
|
abs.absinfo.fuzz = layout->axes[i].fuzz;
|
|
abs.absinfo.flat = layout->axes[i].flat;
|
|
padIoctl(pad->fd, UI_SET_ABSBIT, layout->axes[i].code);
|
|
if (ioctl(pad->fd, UI_ABS_SETUP, &abs) != 0) {
|
|
uinputFail();
|
|
}
|
|
}
|
|
memset(&setup, 0, sizeof(setup));
|
|
setup.id.bustype = BUS_USB;
|
|
setup.id.vendor = layout->vendor;
|
|
setup.id.product = layout->product;
|
|
setup.id.version = layout->version;
|
|
snprintf(setup.name, sizeof(setup.name), "%s pad %c", prefix, stick);
|
|
if (ioctl(pad->fd, UI_DEV_SETUP, &setup) != 0) {
|
|
uinputFail();
|
|
}
|
|
padIoctl(pad->fd, UI_DEV_CREATE, 0u);
|
|
}
|
|
|
|
|
|
// One line of commands: press or release (each control held or let go, and
|
|
// the pads touched sent again), or quit (false).
|
|
static bool padsCommand(PadT *pads, char *line) {
|
|
const char *verb;
|
|
char *control;
|
|
char *separator;
|
|
bool press;
|
|
bool touched[PAD_COUNT];
|
|
uint32_t p;
|
|
uint32_t l;
|
|
|
|
verb = strtok(line, WORD_SPACE);
|
|
if (verb == NULL) {
|
|
return true;
|
|
}
|
|
if (strcmp(verb, "quit") == 0) {
|
|
return false;
|
|
}
|
|
press = strcmp(verb, "press") == 0;
|
|
if (!press && strcmp(verb, "release") != 0) {
|
|
printf("error: unknown command %s\n", verb);
|
|
return true;
|
|
}
|
|
memset(touched, 0, sizeof(touched));
|
|
while ((control = strtok(NULL, WORD_SPACE)) != NULL) {
|
|
separator = strchr(control, STICK_SEPARATOR);
|
|
p = PAD_COUNT;
|
|
l = LINE_COUNT;
|
|
if (separator == control + 1) {
|
|
for (p = 0; p < PAD_COUNT && kStickNames[p] != control[0]; p++) {
|
|
// Up to the stick named.
|
|
}
|
|
for (l = 0; l < LINE_COUNT && strcmp(separator + 1, kLineNames[l]) != 0; l++) {
|
|
// Up to the line named.
|
|
}
|
|
}
|
|
if (p == PAD_COUNT || l == LINE_COUNT) {
|
|
printf("error: unknown control %s\n", control);
|
|
continue;
|
|
}
|
|
pads[p].held[l] = press;
|
|
touched[p] = true;
|
|
}
|
|
for (p = 0; p < PAD_COUNT; p++) {
|
|
if (touched[p]) {
|
|
padApply(&pads[p]);
|
|
}
|
|
}
|
|
printf("ok\n");
|
|
return true;
|
|
}
|
|
|
|
|
|
// What uinput refused, as the tool's last word (the kernel removes any
|
|
// device made so far when its descriptor closes at exit).
|
|
static void uinputFail(void) {
|
|
toolFail("%s: %s", UINPUT_PATH, strerror(errno));
|
|
}
|
|
|
|
|
|
int main(int argc, char **argv) {
|
|
PadT pads[PAD_COUNT];
|
|
const LayoutT *layout;
|
|
const char *prefix;
|
|
char *line;
|
|
size_t cap;
|
|
uint32_t p;
|
|
int first;
|
|
|
|
gateNetStart(TOOL_NAME);
|
|
layout = &kXbox360;
|
|
first = 1;
|
|
if (argc > first && strcmp(argv[first], GENERIC_OPTION) == 0) {
|
|
layout = &kGeneric;
|
|
first++;
|
|
}
|
|
prefix = argc > first ? argv[first] : DEFAULT_PREFIX;
|
|
for (p = 0; p < PAD_COUNT; p++) {
|
|
padOpen(&pads[p], prefix, kStickNames[p], layout);
|
|
}
|
|
gateNetSleep(SETTLE_SECONDS);
|
|
for (p = 0; p < PAD_COUNT; p++) {
|
|
padApply(&pads[p]);
|
|
}
|
|
printf("ready\n");
|
|
line = NULL;
|
|
cap = 0u;
|
|
while (getline(&line, &cap, stdin) >= 0 && padsCommand(pads, line)) {
|
|
// Each line applied.
|
|
}
|
|
free(line);
|
|
for (p = 0; p < PAD_COUNT; p++) {
|
|
padClose(&pads[p]);
|
|
}
|
|
return 0;
|
|
}
|