joeylib2/tests/gate/virtualPad.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;
}