diff --git a/.devcontainer/features/desktop-selkies/src/start-selkies.sh b/.devcontainer/features/desktop-selkies/src/start-selkies.sh index 2104a036..7fb1d1ff 100755 --- a/.devcontainer/features/desktop-selkies/src/start-selkies.sh +++ b/.devcontainer/features/desktop-selkies/src/start-selkies.sh @@ -44,7 +44,6 @@ sudo chmod 777 /dev/input/js* if [ -e "/usr/lib/x86_64-linux-gnu/selkies_joystick_interposer.so" ]; then export SELKIES_INTERPOSER='/usr/$LIB/selkies_joystick_interposer.so' export LD_PRELOAD="${SELKIES_INTERPOSER}${LD_PRELOAD:+:${LD_PRELOAD}}" - export SDL_JOYSTICK_DEVICE=/dev/input/js0 fi # Start desktop environment diff --git a/.github/workflows/build_and_publish_all_images.yaml b/.github/workflows/build_and_publish_all_images.yaml index e50dfbb2..5babf547 100644 --- a/.github/workflows/build_and_publish_all_images.yaml +++ b/.github/workflows/build_and_publish_all_images.yaml @@ -46,11 +46,11 @@ jobs: strategy: matrix: include: - - name: conda - build_args: | - PACKAGE_VERSION=0.0.0.dev0 - PKG_VERSION=0.0.0 - context: addons/conda + # - name: conda + # build_args: | + # PACKAGE_VERSION=0.0.0.dev0 + # PKG_VERSION=0.0.0 + # context: addons/conda - name: coturn context: addons/coturn @@ -208,9 +208,9 @@ jobs: strategy: matrix: include: - - name: conda - source: /opt/selkies-gstreamer-conda.tar.gz - target: selkies-gstreamer-portable.tar.gz + # - name: conda + # source: /opt/selkies-gstreamer-conda.tar.gz + # target: selkies-gstreamer-portable.tar.gz - name: gst-web source: /opt/gst-web.tar.gz diff --git a/.github/workflows/build_and_publish_changed_images.yaml b/.github/workflows/build_and_publish_changed_images.yaml index 431f68d7..39126696 100644 --- a/.github/workflows/build_and_publish_changed_images.yaml +++ b/.github/workflows/build_and_publish_changed_images.yaml @@ -57,11 +57,11 @@ jobs: strategy: matrix: include: - - name: conda - build_args: | - PACKAGE_VERSION=0.0.0.dev0 - PKG_VERSION=0.0.0 - context: addons/conda + # - name: conda + # build_args: | + # PACKAGE_VERSION=0.0.0.dev0 + # PKG_VERSION=0.0.0 + # context: addons/conda - name: coturn context: addons/coturn @@ -237,9 +237,9 @@ jobs: strategy: matrix: include: - - name: conda - source: /opt/selkies-gstreamer-conda.tar.gz - target: selkies-gstreamer-portable.tar.gz + # - name: conda + # source: /opt/selkies-gstreamer-conda.tar.gz + # target: selkies-gstreamer-portable.tar.gz - name: gst-web source: /opt/gst-web.tar.gz diff --git a/addons/example/entrypoint.sh b/addons/example/entrypoint.sh index d44c815b..c71b357b 100755 --- a/addons/example/entrypoint.sh +++ b/addons/example/entrypoint.sh @@ -12,10 +12,10 @@ until [ -d "${XDG_RUNTIME_DIR}" ]; do sleep 0.5; done # Configure joystick interposer export SELKIES_INTERPOSER='/usr/$LIB/selkies_joystick_interposer.so' export LD_PRELOAD="${SELKIES_INTERPOSER}${LD_PRELOAD:+:${LD_PRELOAD}}" -export SDL_JOYSTICK_DEVICE=/dev/input/js0 mkdir -pm1777 /dev/input || sudo-root mkdir -pm1777 /dev/input || echo 'Failed to create joystick interposer directory' touch /dev/input/js0 /dev/input/js1 /dev/input/js2 /dev/input/js3 || sudo-root touch /dev/input/js0 /dev/input/js1 /dev/input/js2 /dev/input/js3 || echo 'Failed to create joystick interposer devices' -chmod 777 /dev/input/js* || sudo-root chmod 777 /dev/input/js* || echo 'Failed to change permission for joystick interposer devices' +touch /dev/input/event1000 /dev/input/event1001 /dev/input/event1002 /dev/input/event1003 || sudo-root touch /dev/input/event1000 /dev/input/event1001 /dev/input/event1002 /dev/input/event1003 || echo 'Failed to create joystick interposer devices' +chmod 777 /dev/input/js* /dev/input/event* || sudo-root chmod 777 /dev/input/js* /dev/input/event* || echo 'Failed to change permission for joystick interposer devices' # Set default display export DISPLAY="${DISPLAY:-:20}" diff --git a/addons/example/selkies-gstreamer-entrypoint.sh b/addons/example/selkies-gstreamer-entrypoint.sh index 5130507b..dcce3060 100755 --- a/addons/example/selkies-gstreamer-entrypoint.sh +++ b/addons/example/selkies-gstreamer-entrypoint.sh @@ -12,7 +12,6 @@ until [ -d "${XDG_RUNTIME_DIR}" ]; do sleep 0.5; done # Configure joystick interposer export SELKIES_INTERPOSER='/usr/$LIB/selkies_joystick_interposer.so' export LD_PRELOAD="${SELKIES_INTERPOSER}${LD_PRELOAD:+:${LD_PRELOAD}}" -export SDL_JOYSTICK_DEVICE=/dev/input/js0 # Set default display export DISPLAY="${DISPLAY:-:20}" diff --git a/addons/js-interposer/.gitignore b/addons/js-interposer/.gitignore index f1fe8d1e..f55bf1d6 100644 --- a/addons/js-interposer/.gitignore +++ b/addons/js-interposer/.gitignore @@ -1 +1,2 @@ -*.so \ No newline at end of file +*.so +sdl_joystick_reader \ No newline at end of file diff --git a/addons/js-interposer/Makefile b/addons/js-interposer/Makefile index f10de941..47f8645f 100644 --- a/addons/js-interposer/Makefile +++ b/addons/js-interposer/Makefile @@ -1,10 +1,46 @@ PREFIX ?= /usr -all: - gcc -shared -fPIC -o selkies_joystick_interposer.so joystick_interposer.c -ldl +all: build build32 -install: all +deps: + @sudo apt-get update + @sudo apt-get install -y build-essential evtest strace joystick libsdl2-dev gcc-multilib libevdev-dev + +build: + gcc -shared -fPIC -o selkies_joystick_interposer.so joystick_interposer.c -ldl -Wall + +build32: + gcc -m32 -shared -fPIC -o selkies_joystick_interposer_i386.so joystick_interposer.c -ldl -Wall + +install: build mkdir -p $(PREFIX)/lib/$(gcc -print-multiarch | sed -e 's/i.*86/i386/') cp *.so $(PREFIX)/lib/$(gcc -print-multiarch | sed -e 's/i.*86/i386/')/ clean: - rm -f *.so \ No newline at end of file + rm -f *.so + sudo rm -f /dev/input/js0 /dev/input/event1000 + +fake-devices: + @sudo touch /dev/input/js0 && sudo chmod 777 /dev/input/js0 + @sudo touch /dev/input/event1000 && sudo chmod 777 /dev/input/event1000 + +py-js-test: + python3 js-interposer-test.py /tmp/selkies_js0.sock + +py-ev-test: + python3 js-interposer-test.py /tmp/selkies_event1000.sock + +jstest: build + LD_PRELOAD=$(PWD)/selkies_joystick_interposer.so jstest /dev/input/js0 + +evtest: build fake-devices + LD_PRELOAD=$(PWD)/selkies_joystick_interposer.so evtest /dev/input/event1000 + +sdltest: build fake-devices + gcc -o sdl_joystick_reader sdl-js-test.c -lSDL2 + LD_PRELOAD=$(PWD)/selkies_joystick_interposer.so ./sdl_joystick_reader + +winetest: build build32 fake-devices + LD_PRELOAD=$(PWD)/selkies_joystick_interposer.so wine control + +xvfbtest: build + LD_PRELOAD=$(PWD)/selkies_joystick_interposer.so Xvfb \ No newline at end of file diff --git a/addons/js-interposer/README.md b/addons/js-interposer/README.md index 621d9fa2..5859c2c0 100644 --- a/addons/js-interposer/README.md +++ b/addons/js-interposer/README.md @@ -23,7 +23,8 @@ If using Wine with `x86_64`, both `/usr/lib/x86_64-linux-gnu/selkies_joystick_in ```bash sudo mkdir -pm1777 /dev/input sudo touch /dev/input/js0 /dev/input/js1 /dev/input/js2 /dev/input/js3 -sudo chmod 777 /dev/input/js* +sudo touch /dev/input/event1000 /dev/input/event1001 /dev/input/event1002 /dev/input/event1003 +sudo chmod 777 /dev/input/js* /dev/input/event* ``` 3. Use the below command before running your target application as well as Selkies-GStreamer for the interposer library to intercept joystick/gamepad events (the single quotes are required in the first line). @@ -31,14 +32,12 @@ sudo chmod 777 /dev/input/js* ```bash export SELKIES_INTERPOSER='/usr/$LIB/selkies_joystick_interposer.so' export LD_PRELOAD="${SELKIES_INTERPOSER}${LD_PRELOAD:+:${LD_PRELOAD}}" -export SDL_JOYSTICK_DEVICE=/dev/input/js0 ``` Otherwise, if you only need one architecture, the below is an equivalent command. ```bash export LD_PRELOAD="/usr/lib/x86_64-linux-gnu/selkies_joystick_interposer.so${LD_PRELOAD:+:${LD_PRELOAD}}" -export SDL_JOYSTICK_DEVICE=/dev/input/js0 ``` You can replace `/usr/$LIB/selkies_joystick_interposer.so` with any non-root path of your choice if using the `.tar.gz` tarball. Make sure the correct `selkies_joystick_interposer.so` is installed in that path. diff --git a/addons/js-interposer/joystick_interposer.c b/addons/js-interposer/joystick_interposer.c index 0533c282..3cc71833 100644 --- a/addons/js-interposer/joystick_interposer.c +++ b/addons/js-interposer/joystick_interposer.c @@ -14,13 +14,10 @@ file, You can obtain one at https://mozilla.org/MPL/2.0/. The ioctl() SYSCALL is interposed to fake the behavior of a input event character device. These ioctl requests were mostly reverse engineered from the joystick.h source and using the jstest command to test. - Note that some applications list the /dev/input/* directory to discover JS devices, to solve for this, create empty files at the following paths: + Note that some applications list the /dev/input/ directory to discover JS devices, to solve for this, create empty files at the following paths: sudo mkdir -pm1777 /dev/input - sudo touch /dev/input/{js0,js1,js2,js3} - sudo chmod 777 /dev/input/js* - - For SDL2 support, only 1 interposed joystick device is supported at a time and the following env var must be set: - export SDL_JOYSTICK_DEVICE=/dev/input/js0 + sudo touch /dev/input/{js0,js1,js2,js3,event1000,event1001,event1002,event1003} + sudo chmod 777 /dev/input/js* /dev/input/event* */ #define _GNU_SOURCE // Required for RTLD_NEXT @@ -35,6 +32,7 @@ file, You can obtain one at https://mozilla.org/MPL/2.0/. #include #include #include +#include #include #include #include @@ -46,6 +44,7 @@ file, You can obtain one at https://mozilla.org/MPL/2.0/. // Timeout to wait for unix domain socket to exist and connect. #define SOCKET_CONNECT_TIMEOUT_MS 250 +// Raw joystick interposer constants. #define JS0_DEVICE_PATH "/dev/input/js0" #define JS0_SOCKET_PATH "/tmp/selkies_js0.sock" #define JS1_DEVICE_PATH "/dev/input/js1" @@ -56,15 +55,73 @@ file, You can obtain one at https://mozilla.org/MPL/2.0/. #define JS3_SOCKET_PATH "/tmp/selkies_js3.sock" #define NUM_JS_INTERPOSERS 4 -// Define the function signature for the original open and ioctl syscalls -typedef int (*open_func_t)(const char *pathname, int flags, ...); -typedef int (*ioctl_func_t)(int fd, unsigned long request, ...); +// Event type joystick interposer constant. +#define EV0_DEVICE_PATH "/dev/input/event1000" +#define EV0_SOCKET_PATH "/tmp/selkies_event1000.sock" +#define EV1_DEVICE_PATH "/dev/input/event1001" +#define EV1_SOCKET_PATH "/tmp/selkies_event1001.sock" +#define EV2_DEVICE_PATH "/dev/input/event1002" +#define EV2_SOCKET_PATH "/tmp/selkies_event1002.sock" +#define EV3_DEVICE_PATH "/dev/input/event1003" +#define EV3_SOCKET_PATH "/tmp/selkies_event1003.sock" +#define NUM_EV_INTERPOSERS 4 + +// Macros for working with interposer count and indexing. +#define NUM_INTERPOSERS() NUM_JS_INTERPOSERS + NUM_EV_INTERPOSERS + +static FILE *log_file_fd = NULL; +void init_log_file() +{ + if (log_file_fd != NULL) + return; + log_file_fd = fopen(LOG_FILE, "a"); +} + +// Log messages from the interposer go to stderr +#define LOG_INFO "[INFO]" +#define LOG_WARN "[WARN]" +#define LOG_ERROR "[ERROR]" +static void interposer_log(const char *level, const char *msg, ...) +{ + init_log_file(); + va_list argp; + va_start(argp, msg); + fprintf(log_file_fd, "[%lu][Selkies Joystick Interposer]%s ", (unsigned long)time(NULL), level); + vfprintf(log_file_fd, msg, argp); + fprintf(log_file_fd, "\n"); + fflush(log_file_fd); + va_end(argp); +} + +// Function that takes the address of a function pointer and uses dlsym to load the system function into it +static int load_real_func(void (**target)(void), const char *name) +{ + if (*target != NULL) return 0; + *target = dlsym(RTLD_NEXT, name); + if (target == NULL) + { + interposer_log(LOG_ERROR, "Error getting original '%s' function: %s", name, dlerror()); + return -1; + } + return 0; +} + +// Function pointers to original calls +static int (*real_open)(const char *pathname, int flags, ...) = NULL; +static int (*real_open64)(const char *pathname, int flags, ...) = NULL; +static int (*real_ioctl)(int fd, unsigned long request, ...) = NULL; +static int (*real_epoll_ctl)(int epfd, int op, int fd, struct epoll_event *event) = NULL; -// Function pointers to the original open and ioctl syscalls -static open_func_t real_open = NULL; -static ioctl_func_t real_ioctl = NULL; +// Initialization function to load the real functions +__attribute__((constructor)) void init_interposer() +{ + load_real_func((void *)&real_open, "open"); + load_real_func((void *)&real_open64, "open64"); + load_real_func((void *)&real_ioctl, "ioctl"); + load_real_func((void *)&real_epoll_ctl, "epoll_ctl"); +} -// type definition for correction struct +// Type definition for correction struct typedef struct js_corr js_corr_t; typedef struct @@ -79,6 +136,7 @@ typedef struct // Struct for storing information about each interposed joystick device. typedef struct { + uint8_t type; char open_dev_name[255]; char socket_path[255]; int sockfd; @@ -86,8 +144,18 @@ typedef struct js_config_t js_config; } js_interposer_t; -static js_interposer_t interposers[NUM_JS_INTERPOSERS] = { +#define DEV_TYPE_JS 0 +#define DEV_TYPE_EV 1 + +// Min/max values for ABS axes +#define ABS_AXIS_MIN -32767 +#define ABS_AXIS_MAX 32767 +#define ABS_HAT_MIN -1 +#define ABS_HAT_MAX 1 + +static js_interposer_t interposers[NUM_INTERPOSERS()] = { { + type : DEV_TYPE_JS, open_dev_name : JS0_DEVICE_PATH, socket_path : JS0_SOCKET_PATH, sockfd : -1, @@ -95,6 +163,7 @@ static js_interposer_t interposers[NUM_JS_INTERPOSERS] = { js_config : {}, }, { + type : DEV_TYPE_JS, open_dev_name : JS1_DEVICE_PATH, socket_path : JS1_SOCKET_PATH, sockfd : -1, @@ -102,6 +171,7 @@ static js_interposer_t interposers[NUM_JS_INTERPOSERS] = { js_config : {}, }, { + type : DEV_TYPE_JS, open_dev_name : JS2_DEVICE_PATH, socket_path : JS2_SOCKET_PATH, sockfd : -1, @@ -109,50 +179,66 @@ static js_interposer_t interposers[NUM_JS_INTERPOSERS] = { js_config : {}, }, { + type : DEV_TYPE_JS, open_dev_name : JS3_DEVICE_PATH, socket_path : JS3_SOCKET_PATH, sockfd : -1, corr : {}, js_config : {}, }, + { + type : DEV_TYPE_EV, + open_dev_name : EV0_DEVICE_PATH, + socket_path : EV0_SOCKET_PATH, + sockfd : -1, + corr : {}, + js_config : {}, + }, + { + type : DEV_TYPE_EV, + open_dev_name : EV1_DEVICE_PATH, + socket_path : EV1_SOCKET_PATH, + sockfd : -1, + corr : {}, + js_config : {}, + }, + { + type : DEV_TYPE_EV, + open_dev_name : EV2_DEVICE_PATH, + socket_path : EV2_SOCKET_PATH, + sockfd : -1, + corr : {}, + js_config : {}, + }, + { + type : DEV_TYPE_EV, + open_dev_name : EV3_DEVICE_PATH, + socket_path : EV3_SOCKET_PATH, + sockfd : -1, + corr : {}, + js_config : {}, + }, }; -static FILE *log_file_fd = NULL; -void init_log_file() +int make_nonblocking(int sockfd) { - if (log_file_fd != NULL) - return; - log_file_fd = fopen(LOG_FILE, "a"); -} - -// Log messages from the interposer go to stderr -#define LOG_INFO "[INFO]" -#define LOG_WARN "[WARN]" -#define LOG_ERROR "[ERROR]" -static void interposer_log(const char *level, const char *msg, ...) -{ - init_log_file(); - va_list argp; - va_start(argp, msg); - fprintf(log_file_fd, "[%lu][Selkies Joystick Interposer]%s ", (unsigned long)time(NULL), level); - vfprintf(log_file_fd, msg, argp); - fprintf(log_file_fd, "\n"); - fflush(log_file_fd); - va_end(argp); -} + // Get the current file descriptor flags + int flags = fcntl(sockfd, F_GETFL, 0); + if (flags == -1) + { + interposer_log(LOG_ERROR, "Failed to get current flags on socket fd to make non-blocking"); + return -1; + } -void init_real_ioctl() -{ - if (real_ioctl != NULL) - return; - real_ioctl = (ioctl_func_t)dlsym(RTLD_NEXT, "ioctl"); -} + // Set the non-blocking flag + flags |= O_NONBLOCK; + if (fcntl(sockfd, F_SETFL, flags) == -1) + { + interposer_log(LOG_ERROR, "Failed to set flags on socket fd to make non-blocking"); + return -1; + } -void init_real_open() -{ - if (real_open != NULL) - return; - real_open = (open_func_t)dlsym(RTLD_NEXT, "open"); + return 0; // Success } int read_config(int fd, js_config_t *js_config) @@ -182,44 +268,13 @@ int read_config(int fd, js_config_t *js_config) return 0; } -// Interposer function for open syscall -int open(const char *pathname, int flags, ...) +int interposer_open_socket(js_interposer_t *interposer) { - init_real_open(); - if (real_open == NULL) - { - interposer_log("Error getting original open function: %s", dlerror()); - return -1; - } - - // Find matching device in interposer list - js_interposer_t *interposer = NULL; - for (size_t i = 0; i < NUM_JS_INTERPOSERS; i++) - { - if (strcmp(pathname, interposers[i].open_dev_name) == 0) - { - interposer = &interposers[i]; - break; - } - } - - // Call real open function if interposer was not found. - if (interposer == NULL) - { - va_list args; - va_start(args, flags); - mode_t mode = va_arg(args, mode_t); - va_end(args); - return real_open(pathname, flags, mode); - } - - interposer_log(LOG_INFO, "Intercepted open call for %s", interposer->open_dev_name); - // Open the existing Unix socket interposer->sockfd = socket(AF_UNIX, SOCK_STREAM, 0); if (interposer->sockfd == -1) { - interposer_log(LOG_ERROR, "Failed to create socket file descriptor when opening devcie: %s", interposer->open_dev_name); + interposer_log(LOG_ERROR, "Failed to create socket file descriptor when opening device: %s", interposer->open_dev_name); return -1; } @@ -255,133 +310,353 @@ int open(const char *pathname, int flags, ...) return -1; } - // Return the file descriptor of the unix socket. - return interposer->sockfd; + return 0; } -// Interposer function for ioctl syscall on joystick device -int ioctl(int fd, unsigned long request, ...) +// Interpose epoll_ctl to make joystck socket fd non-blocking. +int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event) { - init_real_ioctl(); - if (real_ioctl == NULL) + if (load_real_func((void *)&real_epoll_ctl, "epoll_ctl") < 0) return -1; + if (op == EPOLL_CTL_ADD) { - interposer_log(LOG_ERROR, "Error getting original ioctl function: %s", dlerror()); - return -1; + // Find matching device in interposer list + for (size_t i = 0; i < NUM_INTERPOSERS(); i++) + { + if (fd == interposers[i].sockfd) + { + interposer_log(LOG_INFO, "Socket %s (%d) was added to epoll (%d), set non-blocking", interposers[i].socket_path, fd, epfd); + if (make_nonblocking(fd) == -1) + { + interposer_log(LOG_ERROR, "Failed to make socket non-blocking"); + } + break; + } + } } - va_list args; - va_start(args, request); + return real_epoll_ctl(epfd, op, fd, event); +} - // Get interposer for fd +// Interposer function for open syscall +int open(const char *pathname, int flags, ...) +{ + if (load_real_func((void *)&real_open, "open") < 0) return -1; + // Find matching device in interposer list js_interposer_t *interposer = NULL; - for (size_t i = 0; i < NUM_JS_INTERPOSERS; i++) + for (size_t i = 0; i < NUM_INTERPOSERS(); i++) { - if (fd == interposers[i].sockfd) + if (strcmp(pathname, interposers[i].open_dev_name) == 0) { interposer = &interposers[i]; break; } } + // Call real open function if interposer was not found. if (interposer == NULL) { - // Not an ioctl on an interposed device, return real ioctl() call. + va_list args; + va_start(args, flags); void *arg = va_arg(args, void *); va_end(args); - return real_ioctl(fd, request, arg); + return real_open(pathname, flags, arg); + } + + if (interposer_open_socket(interposer) == -1) + return -1; + + interposer_log(LOG_INFO, "Started interposer for 'open' call on %s with fd: %d", interposer->open_dev_name, interposer->sockfd); + + // Return the file descriptor of the unix socket. + return interposer->sockfd; +} + +// Interposer function for open64 +int open64(const char *pathname, int flags, ...) +{ + if (load_real_func((void *)&real_open64, "open64") < 0) return -1; + // Find matching device in interposer list + js_interposer_t *interposer = NULL; + for (size_t i = 0; i < NUM_INTERPOSERS(); i++) + { + if (strcmp(pathname, interposers[i].open_dev_name) == 0) + { + interposer = &interposers[i]; + break; + } } - if (((request >> 8) & 0xFF) != (('j'))) + // Call real open64 + if (interposer == NULL) { - // Not a joystick type ioctl call, return real ioctl() call. + va_list args; + va_start(args, flags); void *arg = va_arg(args, void *); va_end(args); - return real_ioctl(fd, request, arg); + return real_open64(pathname, flags, arg); } + if (interposer_open_socket(interposer) == -1) + return -1; + + interposer_log(LOG_INFO, "Started interposer for 'open64' call on %s with fd: %d", interposer->open_dev_name, interposer->sockfd); + + // Return the file descriptor of the unix socket. + return interposer->sockfd; +} + +// Handler for joystick type ioctl calls +int intercept_js_ioctl(js_interposer_t *interposer, int fd, unsigned long request, ...) +{ + va_list args; + va_start(args, request); + void *arg = va_arg(args, void *); + va_end(args); + // Handle the spoofed behavior for the character device // Cases are the second argument to the _IOR and _IOW macro call found in linux/joystick.h // The type of joystick ioctl is the first byte in the request. - switch (request & 0xFF) + switch (_IOC_NR(request)) { case 0x01: /* JSIOCGVERSION get driver version */ - interposer_log(LOG_INFO, "Intercepted ioctl request %lu -> JSIOCGVERSION", request); - uint32_t *version = va_arg(args, uint32_t *); + interposer_log(LOG_INFO, "Intercepted ioctl JSIOCGVERSION(0x%08x) request for: %s", request, interposer->socket_path); + uint32_t *version = (uint32_t *)arg; *version = JS_VERSION; - - va_end(args); - return 0; // 0 indicates success + return 0; case 0x11: /* JSIOCGAXES get number of axes */ - interposer_log(LOG_INFO, "Intercepted ioctl request %lu -> JSIOCGAXES", request); - uint8_t *num_axes = va_arg(args, uint8_t *); + interposer_log(LOG_INFO, "Intercepted ioctl JSIOCGAXES(0x%08x) request for: %s", request, interposer->socket_path); + uint8_t *num_axes = (uint8_t *)arg; *num_axes = interposer->js_config.num_axes; - - va_end(args); - return 0; // 0 indicates success + return 0; case 0x12: /* JSIOCGBUTTONS get number of buttons */ - interposer_log(LOG_INFO, "Intercepted ioctl request %lu -> JSIOCGBUTTONS", request); - uint8_t *btn_count = va_arg(args, uint8_t *); + interposer_log(LOG_INFO, "Intercepted ioctl JSIOCGBUTTONS(0x%08x) request for: %s", request, interposer->socket_path); + uint8_t *btn_count = (uint8_t *)arg; *btn_count = interposer->js_config.num_btns; - - va_end(args); - return 0; // 0 indicates success + return 0; case 0x13: /* JSIOCGNAME(len) get identifier string */ - interposer_log(LOG_INFO, "Intercepted ioctl request %lu -> JSIOCGNAME", request); - char *name = va_arg(args, char *); - size_t *len = va_arg(args, size_t *); - strncpy(name, interposer->js_config.name, strlen(interposer->js_config.name)); - name[strlen(interposer->js_config.name)] = '\0'; - - va_end(args); - return 0; // 0 indicates success + interposer_log(LOG_INFO, "Intercepted ioctl JSIOCGNAME(0x%08x) request for: %s", request, interposer->socket_path); + char *name = (char *)arg; + size_t len = strlen(interposer->js_config.name); + name[len] = '\0'; + strncpy(name, interposer->js_config.name, len); + return len; case 0x21: /* JSIOCSCORR set correction values */ - interposer_log(LOG_INFO, "Intercepted ioctl request %lu -> JSIOCSCORR", request); - va_end(args); + interposer_log(LOG_INFO, "Intercepted ioctl JSIOCSCORR(0x%08x) request for: %s", request, interposer->socket_path); return 0; case 0x22: /* JSIOCGCORR get correction values */ - interposer_log(LOG_INFO, "Intercepted ioctl request %lu -> JSIOCGCORR", request); - js_corr_t *corr = va_arg(args, js_corr_t *); + interposer_log(LOG_INFO, "Intercepted ioctl JSIOCGCORR(0x%08x) request for: %s", request, interposer->socket_path); + js_corr_t *corr = (js_corr_t *)arg; memcpy(corr, &interposer->corr, sizeof(interposer->corr)); - va_end(args); - return 0; // 0 indicates success + return 0; case 0x31: /* JSIOCSAXMAP set axis mapping */ - interposer_log(LOG_INFO, "Intercepted ioctl request %lu -> JSIOCSAXMAP", request); - va_end(args); - return 0; // 0 indicates success + interposer_log(LOG_INFO, "Intercepted ioctl JSIOCSAXMAP(0x%08x) request for: %s", request, interposer->socket_path); + return 0; case 0x32: /* JSIOCGAXMAP get axis mapping */ - interposer_log(LOG_INFO, "Intercepted ioctl request %lu -> JSIOCGAXMAP", request); - uint8_t *axmap = va_arg(args, uint8_t *); + interposer_log(LOG_INFO, "Intercepted ioctl JSIOCGAXMAP(0x%08x) request for: %s", request, interposer->socket_path); + uint8_t *axmap = (uint8_t *)arg; memcpy(axmap, interposer->js_config.axes_map, interposer->js_config.num_axes * sizeof(uint8_t)); - va_end(args); - return 0; // 0 indicates success + return 0; case 0x33: /* JSIOCSBTNMAP set button mapping */ - interposer_log(LOG_INFO, "Intercepted ioctl request %lu -> JSIOCSBTNMAP", request); - va_end(args); - return 0; // 0 indicates success + interposer_log(LOG_INFO, "Intercepted ioctl JSIOCSBTNMAP(0x%08x) request for: %s", request, interposer->socket_path); + return 0; case 0x34: /* JSIOCGBTNMAP get button mapping */ - interposer_log(LOG_INFO, "Intercepted ioctl request %lu -> JSIOCGBTNMAP", request); - uint16_t *btn_map = va_arg(args, uint16_t *); + interposer_log(LOG_INFO, "Intercepted ioctl JSIOCGBTNMAP(0x%08x) request for: %s", request, interposer->socket_path); + uint16_t *btn_map = (uint16_t *)arg; memcpy(btn_map, interposer->js_config.btn_map, interposer->js_config.num_btns * sizeof(uint16_t)); - va_end(args); - return 0; // 0 indicates success + return 0; default: - interposer_log(LOG_WARN, "Unhandled Intercepted ioctl request %lu", request); - void *arg = va_arg(args, void *); - va_end(args); + interposer_log(LOG_WARN, "Unhandled 'joystick' ioctl intercept request 0x%08x", request, interposer->socket_path); return real_ioctl(fd, request, arg); } - // Handle other ioctl requests as needed - return -ENOTTY; // Not a valid ioctl request for this character device emulation + // Not a valid ioctl request for this character device emulation + return -ENOTTY; +} + +// Handler for event type ioctl calls +int intercept_ev_ioctl(js_interposer_t *interposer, int fd, unsigned long request, ...) +{ + va_list args; + va_start(args, request); + void *arg = va_arg(args, void *); + va_end(args); + + struct input_absinfo *absinfo; + struct input_id *id; + int fake_version = 0x010100; + int len; + + /* The EVIOCGABS(key) is a request to get the calibration values for the ABS type axes. + * Each axes returned by the ioctl EVIOCGBIT(EV_ABS) request is evaluated. + * since there is no need to emulate calibration values, the target value is zero. + */ + if ((request >= EVIOCGABS(ABS_X) && request <= EVIOCGABS(ABS_MAX))) + { + // The actual ABS axis is offset by the ioctl request, 0x40. + // https://github.com/libsdl-org/SDL/blob/cf249b0cb28336d14bbd6cb0bd4f711f6ad4db87/src/joystick/linux/SDL_sysjoystick.c#L1237 + uint8_t abs = (request & 0xFF) - 0x40; + absinfo = (struct input_absinfo *)arg; + absinfo->value = 0; + if (request <= EVIOCGABS(ABS_BRAKE)) + { + absinfo->minimum = ABS_AXIS_MIN; + absinfo->maximum = ABS_AXIS_MAX; + } + else if (request >= EVIOCGABS(ABS_HAT0X) && request <= EVIOCGABS(ABS_HAT3Y)) + { + absinfo->minimum = ABS_HAT_MIN; + absinfo->maximum = ABS_HAT_MAX; + } + + interposer_log(LOG_INFO, "Matched ioctl EVIOCGABS(0x%x)(0x%08x), request for %s", abs, request, interposer->socket_path); + return 0; + } + + switch (_IOC_NR(request)) + { + case (0x20 + EV_SYN): /* Handle EVIOCGBIT for EV_SYN: sync event */ + len = _IOC_SIZE(request); + memset(arg, 0, len); + interposer_log(LOG_INFO, "Intercepted ioctl EVIOCGBIT(EV_SYN, %d)(0x%x), request for %s", len, request, interposer->socket_path); + return 0; + + case (0x20 + EV_ABS): /* Handle EVIOCGBIT for EV_ABS: report supported axes. */ + len = _IOC_SIZE(request); + memset(arg, 0, len); + // set the bit corresponding to each supported axis. + for (size_t i = 0; i < interposer->js_config.num_axes; i++) + { + int bit = interposer->js_config.axes_map[i]; + int index = bit / 8; + if (index < len) + { + ((unsigned char *)arg)[index] |= (1 << (bit % 8)); + } + } + interposer_log(LOG_INFO, "Intercepted ioctl EVIOCGBIT(EV_ABS, %d)(0x%x), request for %s", len, request, interposer->socket_path); + + return 0; + + case (0x20 + EV_REL): /* Handle EVIOCGBIT for EV_REL: report supported relative events. */ + len = _IOC_SIZE(request); + memset(arg, 0, len); + interposer_log(LOG_INFO, "Intercepted ioctl EVIOCGBIT(EV_REL, %d)(0x%x), request for %s", len, request, interposer->socket_path); + return 0; + + case (0x20 + EV_KEY): /* Handle EVIOCGBIT for EV_KEY: report supported keys. */ + len = _IOC_SIZE(request); + memset(arg, 0, len); + // set the bit corresponding to each supported key. + for (size_t i = 0; i < interposer->js_config.num_btns; i++) + { + int bit = interposer->js_config.btn_map[i]; + int index = bit / 8; + if (index < len) + { + ((unsigned char *)arg)[index] |= (1 << (bit % 8)); + } + } + interposer_log(LOG_INFO, "Intercepted ioctl EVIOCGBIT(EV_KEY, %d)(0x%x), request for %s", len, request, interposer->socket_path); + return 0; + + case (0x20 + EV_FF): /* Handle EVIOCGBIT for EV_FF: report supported force feedback. */ + // TODO: Support force feedback + len = _IOC_SIZE(request); + memset(arg, 0, len); + interposer_log(LOG_INFO, "Intercepted ioctl EVIOCGBIT(EV_FF, %d)(0x%x), request for %s", len, request, interposer->socket_path); + return 0; + + case 0x06: /* Handle EVIOCGNAME: Get device name. */ + len = _IOC_SIZE(request); + memcpy(arg, interposer->js_config.name, sizeof(interposer->js_config.name) + 1); + interposer_log(LOG_INFO, "Intercepted ioctl EVIOCGNAME(%d)(0x%08x) for %s", len, request, interposer->socket_path); + return 0; + + case 0x01: /* Handle EVIOCGVERSION: device version request. */ + memcpy(arg, &fake_version, sizeof(fake_version)); + interposer_log(LOG_INFO, "Intercepted ioctl EVIOCGVERSION(0x%08x) for %s", request, interposer->socket_path); + return 0; + + case 0x02: /* Handle EVIOCGID: device ID request. */ + id = (struct input_id *)arg; + // Populate the fake input_id for a joystick device + id->bustype = BUS_VIRTUAL; // Example bus type (Virtual) + id->vendor = 0x045E; // Fake vendor ID (e.g., Microsoft) + id->product = 0x028E; // Fake product ID (e.g., Xbox Controller) + id->version = 0x0114; // Fake version + + interposer_log(LOG_INFO, "Intercepted ioctl EVIOCGID(0x%08x) for %s", request, interposer->socket_path); + return 0; + + case 0x09: /* Handle EVIOCGPROP(len): report device properties */ + len = _IOC_SIZE(request); + memset(arg, 0, len); + interposer_log(LOG_INFO, "Intercepted ioctl EVIOCGPROP(%d)(0x%x), request for %s", len, request, interposer->socket_path); + return 0; + + case 0x18: /* Handle EVIOCGKEY(len): report state of all buttons */ + len = _IOC_SIZE(request); + memset(arg, 0, len); + interposer_log(LOG_INFO, "Intercepted ioctl EVIOCGKEY(%d)(0x%x), request for %s", len, request, interposer->socket_path); + return 0; + + case 0x90: /* Handle EVIOCGRAB: grab device for exclusive access */ + interposer_log(LOG_INFO, "Matched ioctl EVIOCGRAB(0x%08x), request for %s", request, interposer->socket_path); + return 0; + + default: + interposer_log(LOG_WARN, "Unhandled EV ioctl request (0x%08x) for %s", request, interposer->socket_path); + return 0; + } +} + +// Interposer function for ioctl syscall +int ioctl(int fd, unsigned long request, ...) +{ + if (load_real_func((void *)&real_ioctl, "ioctl") < 0) return -1; + va_list args; + va_start(args, request); + void *arg = va_arg(args, void *); + va_end(args); + + // Get interposer for fd + js_interposer_t *interposer = NULL; + for (size_t i = 0; i < NUM_INTERPOSERS(); i++) + { + if (fd == interposers[i].sockfd) + { + interposer = &interposers[i]; + break; + } + } + + if (interposer == NULL) + { + // Not an ioctl on an interposed device, return real ioctl() call. + return real_ioctl(fd, request, arg); + } + else if ((_IOC_TYPE(request) == 'j')) + { + return intercept_js_ioctl(interposer, fd, request, arg); + } + else if ((_IOC_TYPE(request) == 'E')) + { + return intercept_ev_ioctl(interposer, fd, request, arg); + } + else + { + interposer_log(LOG_WARN, "No ioctl interceptor for request 0x%08x on %s", request, interposer->socket_path); + return real_ioctl(fd, request, arg); + } } diff --git a/addons/js-interposer/js-interposer-test.py b/addons/js-interposer/js-interposer-test.py index cb9982a3..660d2746 100755 --- a/addons/js-interposer/js-interposer-test.py +++ b/addons/js-interposer/js-interposer-test.py @@ -6,11 +6,23 @@ # import ctypes import os +import sys import struct import time import asyncio import socket +DEFAULT_SOCKET_PATH = "/tmp/selkies_js0.sock" + +# Event types +EV_SYN = 0x00 +EV_KEY = 0x01 +EV_REL = 0x02 +EV_ABS = 0x03 + +# Sync events +SYN_REPORT = 0 + # Types from https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/uapi/linux/input-event-codes.h#n380 BTN_MISC = 0x100 BTN_0 = 0x100 @@ -97,8 +109,6 @@ ABS_VOLUME = 0x20 ABS_PROFILE = 0x21 -SOCKET_PATH = "/tmp/selkies_js0.sock" - # From /usr/include/linux/joystick.h JS_EVENT_BUTTON = 0x01 JS_EVENT_AXIS = 0x02 @@ -147,7 +157,7 @@ } -def get_btn_event(btn_num, btn_val): +def get_js_btn_event(btn_num, btn_val): ts = int((time.time() * 1000) % 1000000000) # see js_event struct definition above. @@ -160,8 +170,26 @@ def get_btn_event(btn_num, btn_val): return event +def get_ev_btn_event(btn_num, btn_val): + now = time.time() + ts_sec = int(now) + ts_usec = int((now *1e6) % 1e6) + + ev_btn = XPAD_CONFIG["btn_map"][btn_num] + + # timestamp_sec, timestamp_usec, type, code, value + struct_format = 'llHHIllHHI' + event = struct.pack(struct_format, + ts_sec, ts_usec, EV_KEY, ev_btn, btn_val, + ts_sec, ts_usec, EV_SYN, SYN_REPORT, 0 + ) + + # debug + print(struct.unpack(struct_format, event)) + + return event -def get_axis_event(axis_num, axis_val): +def get_js_axis_event(axis_num, axis_val): ts = int((time.time() * 1000) % 1000000000) # see js_event struct definition above. @@ -174,6 +202,22 @@ def get_axis_event(axis_num, axis_val): return event +def get_ev_axis_event(axis_num, axis_val): + now = time.time() + ts_sec = int(now) + ts_usec = int((now *1e6) % 1e6) + + # evdev expects ev key codes, not axis numbers + ev_axis = XPAD_CONFIG["axes_map"][axis_num] + + # timestamp_sec, timestamp_usec, type, code, value + struct_format = 'llHHillHHi' + event = struct.pack(struct_format, + ts_sec, ts_usec, EV_ABS, ev_axis, axis_val, + ts_sec, ts_usec, EV_SYN, SYN_REPORT, 0 + ) + + return event def make_config(): cfg = XPAD_CONFIG @@ -197,11 +241,21 @@ def make_config(): ) return data +def get_axis_cylon(curr, minval, maxval, step): + new_step = step + if curr + step > maxval or curr + step < minval: + new_step = step * -1 + return new_step, curr + new_step -async def send_events(): +async def send_events(ev_type="JS"): loop = asyncio.get_event_loop() btn_num = 0 btn_val = 0 + axis_num = 0 + axis_val = 0 + axis_step = 4000 + hat_val = 0 + hat_step = 1 while True: if len(clients) < 1: await asyncio.sleep(0.1) @@ -211,8 +265,29 @@ async def send_events(): for fd in clients: try: client = clients[fd] - print("Sending event to client: %d" % fd) + + if ev_type == "JS": + get_btn_event = get_js_btn_event + get_axis_event = get_js_axis_event + elif ev_type == "EV": + get_btn_event = get_ev_btn_event + get_axis_event = get_ev_axis_event + else: + print(f"ERROR: unsupported ev_type: '{ev_type}'") + + print(f"Sending button {btn_num} value={btn_val} event to client: {fd}") await loop.sock_sendall(client, get_btn_event(btn_num, btn_val)) + + hat_step, hat_val = get_axis_cylon(hat_val, -1, 1, hat_step) + axis_step, axis_val = get_axis_cylon(axis_val, -32767, 32768, axis_step) + for axis_num in range(len(XPAD_CONFIG["axes_map"])): + if XPAD_CONFIG["axes_map"][axis_num] in [ABS_HAT0X, ABS_HAT0Y]: + print(f"Sending hat axis {axis_num} value={hat_val} event to client: {fd}") + await loop.sock_sendall(client, get_axis_event(axis_num, hat_val)) + else: + print(f"Sending axis {axis_num} value={axis_val} event to client: {fd}") + await loop.sock_sendall(client, get_axis_event(axis_num, axis_val)) + except BrokenPipeError: print("Client %d disconnected" % fd) closed_clients.append(fd) @@ -225,21 +300,24 @@ async def send_events(): btn_val = 0 if btn_val == 1 else 1 if btn_val == 1: - btn_num = (btn_num + 1) % 11 + btn_num = (btn_num + 1) % len(XPAD_CONFIG["btn_map"]) -async def run_server(): +async def run_server(socket_path): server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - server.bind(SOCKET_PATH) + server.bind(socket_path) server.listen(1) server.setblocking(False) loop = asyncio.get_event_loop() - print('Listening for connections on %s' % SOCKET_PATH) + print('Listening for connections on %s' % socket_path) # Create task that sends events to all connected clients. - loop.create_task(send_events()) + event_type = "JS" + if "event" in socket_path: + event_type = "EV" + loop.create_task(send_events(event_type)) try: while True: @@ -257,11 +335,20 @@ async def run_server(): server.close() if __name__ == "__main__": + if len(sys.argv) > 1 and sys.argv[1] == "-h": + print(f"USAGE: {sys.argv[0]} []") + sys.exit(-1) + + if len(sys.argv) > 1: + socket_path = sys.argv[1] + else: + socket_path = DEFAULT_SOCKET_PATH + # remove the socket file if it already exists try: - os.unlink(SOCKET_PATH) + os.unlink(socket_path) except OSError: - if os.path.exists(SOCKET_PATH): + if os.path.exists(socket_path): raise - asyncio.run(run_server()) + asyncio.run(run_server(socket_path)) diff --git a/addons/js-interposer/sdl-js-test.c b/addons/js-interposer/sdl-js-test.c new file mode 100644 index 00000000..c5cf9631 --- /dev/null +++ b/addons/js-interposer/sdl-js-test.c @@ -0,0 +1,98 @@ +#include +#include +#include + +void check_error(int result, const char *message) +{ + if (result < 0) + { + fprintf(stderr, "%s: %s\n", message, SDL_GetError()); + SDL_Quit(); + exit(EXIT_FAILURE); + } +} + +int main(int argc, char *argv[]) +{ + if (SDL_Init(SDL_INIT_JOYSTICK) < 0) + { + fprintf(stderr, "Failed to initialize SDL: %s\n", SDL_GetError()); + return EXIT_FAILURE; + } + + printf("SDL initialized.\n"); + + int num_joysticks = SDL_NumJoysticks(); + if (num_joysticks < 1) + { + printf("No joysticks connected.\n"); + SDL_Quit(); + return EXIT_SUCCESS; + } + + printf("Number of joysticks found: %d\n", num_joysticks); + + SDL_Joystick *joystick = SDL_JoystickOpen(0); + fprintf(stderr, "SDL_JoystickOpen response: 0x%lx\n", (long int)joystick); + if (!joystick) + { + fprintf(stderr, "Could not open joystick: %s\n", SDL_GetError()); + SDL_Quit(); + return EXIT_FAILURE; + } + + printf("Joystick opened: %s\n", SDL_JoystickName(joystick)); + + printf("Axes: %d\n", SDL_JoystickNumAxes(joystick)); + printf("Buttons: %d\n", SDL_JoystickNumButtons(joystick)); + printf("Hats: %d\n", SDL_JoystickNumHats(joystick)); + + SDL_Event event; + int running = 1; + + printf("Reading joystick input. Press Ctrl+C to quit.\n"); + + while (running) + { + while (SDL_PollEvent(&event)) + { + switch (event.type) + { + case SDL_JOYDEVICEADDED: + printf("Saw device added event\n"); + break; + + case SDL_JOYAXISMOTION: + printf("Axis %d moved to %d\n", event.jaxis.axis, event.jaxis.value); + break; + + case SDL_JOYBUTTONDOWN: + printf("Button %d pressed\n", event.jbutton.button); + break; + + case SDL_JOYBUTTONUP: + printf("Button %d released\n", event.jbutton.button); + break; + + case SDL_JOYHATMOTION: + printf("Hat %d moved to %d\n", event.jhat.hat, event.jhat.value); + break; + + case SDL_QUIT: + running = 0; + break; + + default: + printf("Unhandled input event type: 0x%x\n", event.type); + break; + } + } + + SDL_Delay(10); // Add a small delay to prevent CPU overuse + } + + SDL_JoystickClose(joystick); + printf("Joystick closed.\n"); + SDL_Quit(); + return EXIT_SUCCESS; +} diff --git a/docs/component.md b/docs/component.md index 2d84e4fc..005d8fcf 100644 --- a/docs/component.md +++ b/docs/component.md @@ -186,7 +186,8 @@ The following paths are required to exist for the Joystick Interposer to pass th ```bash sudo mkdir -pm1777 /dev/input sudo touch /dev/input/js0 /dev/input/js1 /dev/input/js2 /dev/input/js3 -sudo chmod 777 /dev/input/js* +sudo touch /dev/input/event1000 /dev/input/event1001 /dev/input/event1002 /dev/input/event1003 +sudo chmod 777 /dev/input/js* /dev/input/event* ``` The following environment variables are required to be set in the environment each application is being run in to receive the joystick/gamepad input. @@ -194,7 +195,6 @@ The following environment variables are required to be set in the environment ea ```bash export SELKIES_INTERPOSER='/usr/$LIB/selkies_joystick_interposer.so' export LD_PRELOAD="${SELKIES_INTERPOSER}${LD_PRELOAD:+:${LD_PRELOAD}}" -export SDL_JOYSTICK_DEVICE=/dev/input/js0 ``` You can replace `/usr/$LIB/selkies_joystick_interposer.so` with any non-root path of your choice if using the `.tar.gz` tarball. diff --git a/docs/start.md b/docs/start.md index e1c8e186..867ba136 100644 --- a/docs/start.md +++ b/docs/start.md @@ -185,10 +185,10 @@ export DISPLAY="${DISPLAY:-:0}" # Configure the Joystick Interposer export SELKIES_INTERPOSER='/usr/$LIB/selkies_joystick_interposer.so' export LD_PRELOAD="${SELKIES_INTERPOSER}${LD_PRELOAD:+:${LD_PRELOAD}}" -export SDL_JOYSTICK_DEVICE=/dev/input/js0 sudo mkdir -pm1777 /dev/input sudo touch /dev/input/js0 /dev/input/js1 /dev/input/js2 /dev/input/js3 -sudo chmod 777 /dev/input/js* +sudo touch /dev/input/event1000 /dev/input/event1001 /dev/input/event1002 /dev/input/event1003 +sudo chmod 777 /dev/input/js* /dev/input/event* # Commented sections are optional but may be mandatory based on setup diff --git a/src/selkies_gstreamer/gamepad.py b/src/selkies_gstreamer/gamepad.py index 7bf60d77..4443bddd 100644 --- a/src/selkies_gstreamer/gamepad.py +++ b/src/selkies_gstreamer/gamepad.py @@ -116,42 +116,6 @@ ABS_MIN = -32767 ABS_MAX = 32767 -# Joystick event struct -# https://www.kernel.org/doc/Documentation/input/joystick-api.txt -# struct js_event { -# __u32 time; /* event timestamp in milliseconds */ -# __s16 value; /* value */ -# __u8 type; /* event type */ -# __u8 number; /* axis/button number */ -# }; - -def get_btn_event(btn_num, btn_val): - ts = int((time.time() * 1000) % 1000000000) - - # see js_event struct definition above. - # https://docs.python.org/3/library/struct.html - struct_format = 'IhBB' - event = struct.pack(struct_format, ts, btn_val, - JS_EVENT_BUTTON, btn_num) - - logger.debug(struct.unpack(struct_format, event)) - - return event - - -def get_axis_event(axis_num, axis_val): - ts = int((time.time() * 1000) % 1000000000) - - # see js_event struct definition above. - # https://docs.python.org/3/library/struct.html - struct_format = 'IhBB' - event = struct.pack(struct_format, ts, axis_val, - JS_EVENT_AXIS, axis_num) - - logger.debug(struct.unpack(struct_format, event)) - - return event - def detect_gamepad_config(name): # TODO switch mapping based on name. return STANDARD_XPAD_CONFIG @@ -173,8 +137,8 @@ def normalize_axis_val(val): def normalize_trigger_val(val): return round(val * (ABS_MAX - ABS_MIN)) + ABS_MIN -class SelkiesGamepad: - def __init__(self, socket_path, loop): +class SelkiesGamepadBase: + def __init__(self, socket_path, loop, gamepad_mapper_class): self.socket_path = socket_path self.loop = loop @@ -197,11 +161,17 @@ def __init__(self, socket_path, loop): # flag indicating that loop is running. self.running = False + + # class used for mapping gamepad events + self.gamepad_mapper_class = gamepad_mapper_class + + def get_gamepad_mapper(self, config, name, num_btns, num_axes): + raise Exception("get_gamepad_mapper must be overriden") def set_config(self, name, num_btns, num_axes): self.name = name self.config = detect_gamepad_config(name) - self.mapper = GamepadMapper(self.config, name, num_btns, num_axes) + self.mapper = self.gamepad_mapper_class(self.config, name, num_btns, num_axes) def __make_config(self): ''' @@ -238,7 +208,7 @@ async def __send_events(self): await asyncio.sleep(0.001) continue while self.running and not self.events.empty(): - await self.send_event(self.events.get()) + await self.__send_event(self.events.get()) def send_btn(self, btn_num, btn_val): if not self.mapper: @@ -256,7 +226,7 @@ def send_axis(self, axis_num, axis_val): if event is not None: self.events.put(event) - async def send_event(self, event): + async def __send_event(self, event): if len(self.clients) < 1: return @@ -274,7 +244,7 @@ async def send_event(self, event): for fd in closed_clients: del self.clients[fd] - async def setup_client(self, client): + async def __setup_client(self, client): logger.info("Sending config to client with fd: %d" % client.fileno()) try: config_data = self.__make_config() @@ -321,7 +291,7 @@ async def run_server(self): logger.info("Client connected with fd: %d" % fd) # Send client the joystick configuration - await self.setup_client(client) + await self.__setup_client(client) # Add client to dictionary to receive events. self.clients[fd] = client @@ -342,13 +312,48 @@ def stop_server(self): except: pass -class GamepadMapper: +class SelkiesGamepad: + def __init__(self, js_socket_path, ev_socket_path, loop): + self.js_socket_path = js_socket_path + self.ev_socket_path = ev_socket_path + self.loop = loop + + self.js_gamepad = SelkiesJSGamepad(js_socket_path, loop) + self.ev_gamepad = SelkiesEVGamepad(ev_socket_path, loop) + + def set_config(self, name, num_btns, num_axes): + self.js_gamepad.set_config(name, num_btns, num_axes) + self.ev_gamepad.set_config(name, num_btns, num_axes) + + def send_btn(self, btn_num, btn_val): + self.js_gamepad.send_btn(btn_num, btn_val) + self.ev_gamepad.send_btn(btn_num, btn_val) + + def send_axis(self, axis_num, axis_val): + self.js_gamepad.send_axis(axis_num, axis_val) + self.ev_gamepad.send_axis(axis_num, axis_val) + + def run_server(self): + asyncio.ensure_future(self.js_gamepad.run_server(), loop=self.loop) + asyncio.ensure_future(self.ev_gamepad.run_server(), loop=self.loop) + + def stop_server(self): + self.js_gamepad.run_server() + self.ev_gamepad.run_server() + +class GamepadMapperBase: def __init__(self, config, name, num_btns, num_axes): self.config = config self.input_name = name self.input_num_btns = num_btns self.input_num_axes = num_axes - + + def get_btn_event(self, btn_num, btn_val): + raise Exception("get_btn_event not implemented") + + def get_axis_event(self, axis_num, axis_val): + raise Exception("get_axis_event not implemented") + def get_mapped_btn(self, btn_num, btn_val): ''' return either a button or axis event based on mapping. @@ -373,7 +378,7 @@ def get_mapped_btn(self, btn_num, btn_val): # Normalize to full range for input between 0 and 1. axis_val = normalize_trigger_val(btn_val) - return get_axis_event(axis_num, axis_val) + return self.get_axis_event(axis_num, axis_val) # Perform button mapping. mapped_btn = self.config["mapping"]["btns"].get(btn_num, btn_num) @@ -382,7 +387,7 @@ def get_mapped_btn(self, btn_num, btn_val): mapped_btn, len(self.config["btn_map"]) - 1)) return None - return get_btn_event(mapped_btn, int(btn_val)) + return self.get_btn_event(mapped_btn, int(btn_val)) def get_mapped_axis(self, axis_num, axis_val): mapped_axis = self.config["mapping"]["axes"].get(axis_num, axis_num) @@ -392,4 +397,97 @@ def get_mapped_axis(self, axis_num, axis_val): return None # Normalize axis value to be within range. - return get_axis_event(mapped_axis, normalize_axis_val(axis_val)) + return self.get_axis_event(mapped_axis, normalize_axis_val(axis_val)) + + +class JSGamepadMapper(GamepadMapperBase): + def __init__(self, config, name, num_btns, num_axes): + super().__init__(config, name, num_btns, num_axes) + + # https://www.kernel.org/doc/Documentation/input/joystick-api.txt + # struct js_event { + # __u32 time; /* event timestamp in milliseconds */ + # __s16 value; /* value */ + # __u8 type; /* event type */ + # __u8 number; /* axis/button number */ + # }; + self.struct_format = 'IhBB' + + def get_btn_event(self, btn_num, btn_val): + ts = int((time.time() * 1000) % 1000000000) + + event = struct.pack(self.struct_format, ts, btn_val, + JS_EVENT_BUTTON, btn_num) + + logger.debug(struct.unpack(self.struct_format, event)) + + return event + + def get_axis_event(self, axis_num, axis_val): + ts = int((time.time() * 1000) % 1000000000) + + event = struct.pack(self.struct_format, ts, axis_val, + JS_EVENT_AXIS, axis_num) + + logger.debug(struct.unpack(self.struct_format, event)) + + return event + +class EVGamepadMapper(GamepadMapperBase): + def __init__(self, config, name, num_btns, num_axes): + super().__init__(config, name, num_btns, num_axes) + + # https://www.kernel.org/doc/Documentation/input/joystick-api.txt + # struct input_event { + # struct timeval time; + # unsigned short type; + # unsigned short code; + # unsigned int value; + # }; + + # Double the input_event to include sycn, EV_SYN event. + self.struct_format = 'llHHillHHi' + + def get_btn_event(self, btn_num, btn_val): + now = time.time() + ts_sec = int(now) + ts_usec = int((now *1e6) % 1e6) + + # evdev expects ev key codes, not button numbers + ev_btn = self.config["btn_map"][btn_num] + + # timestamp_sec, timestamp_usec, type, code, value + event = struct.pack(self.struct_format, + ts_sec, ts_usec, EV_KEY, ev_btn, btn_val, + ts_sec, ts_usec, EV_SYN, SYN_REPORT, 0 + ) + + logger.debug(struct.unpack(self.struct_format, event)) + + return event + + def get_axis_event(self, axis_num, axis_val): + now = time.time() + ts_sec = int(now) + ts_usec = int((now *1e6) % 1e6) + + # evdev expects ev key codes, not axis numbers + ev_axis = self.config["axes_map"][axis_num] + + # timestamp_sec, timestamp_usec, type, code, value + event = struct.pack(self.struct_format, + ts_sec, ts_usec, EV_ABS, ev_axis, axis_val, + ts_sec, ts_usec, EV_SYN, SYN_REPORT, 0 + ) + + logger.debug(struct.unpack(self.struct_format, event)) + + return event + +class SelkiesJSGamepad(SelkiesGamepadBase): + def __init__(self, socket_path, loop): + super().__init__(socket_path, loop, JSGamepadMapper) + +class SelkiesEVGamepad(SelkiesGamepadBase): + def __init__(self, socket_path, loop): + super().__init__(socket_path, loop, EVGamepadMapper) \ No newline at end of file diff --git a/src/selkies_gstreamer/webrtc_input.py b/src/selkies_gstreamer/webrtc_input.py index 5509e0e9..626f3859 100644 --- a/src/selkies_gstreamer/webrtc_input.py +++ b/src/selkies_gstreamer/webrtc_input.py @@ -92,6 +92,7 @@ def __init__(self, uinput_mouse_socket_path="", js_socket_path="", enable_clipbo # Map of gamepad numbers to socket paths self.js_socket_path_map = {i: os.path.join(js_socket_path, "selkies_js%d.sock" % i) for i in range(4)} + self.ev_socket_path_map = {i: os.path.join(js_socket_path, "selkies_event%d.sock" % (1000 + i)) for i in range(4)} # Map of gamepad number to SelkiesGamepad objects self.js_map = {} @@ -172,16 +173,20 @@ def __js_connect(self, js_num, name, num_btns, num_axes): logger.info("creating selkies gamepad for js%d, name: '%s', buttons: %d, axes: %d" % (js_num, name, num_btns, num_axes)) - socket_path = self.js_socket_path_map.get(js_num, None) - if socket_path is None: + js_socket_path = self.js_socket_path_map.get(js_num, None) + if js_socket_path is None: logger.error("failed to connect js%d because socket_path was not found" % js_num) return + ev_socket_path = self.ev_socket_path_map.get(js_num, None) + if ev_socket_path is None: + logger.error("failed to connect EV joystick %d because socket_path was not found" % js_num) + return + # Create the gamepad and button config. - js = SelkiesGamepad(socket_path, self.loop) + js = SelkiesGamepad(js_socket_path, ev_socket_path, self.loop) js.set_config(name, num_btns, num_axes) - - asyncio.ensure_future(js.run_server(), loop=self.loop) + js.run_server() self.js_map[js_num] = js