Small Printed Circuit Board

Code, Docs & Tools

Joystick and joypad mapper for Linux

Joymap

Joymap helps you to configure a joystick or joypad on Linux to make it to work in keyboard emulation mode.

Many old games available online (i.e. the ones from archive.org - MS-DOS games, Classic PC Games, SEGA games), are controlled using the keyboard.

With joymap you can map very easily keyboard keys to buttons and axes of the joypad. Joymaps supports up to 6 axes and 16 buttons devices.

You can also check the source code of Joymap if you are looking for hints on how to use the XDO library to inject characters as if they were generated from a keyboard, how to read raw data from a device (/dev filesystem), or how to parse program arguments using getops.

The concept

When you press a button, the application gets the button/axis code from the joypad and injects a character into the window that has the focus as if it was sent pressing a key from the keyboard.

Every game uses its own set of keys, so I made this application easy to be configured directly from the command line using different switches.

What you have to do is to execute the application in a shell, passing as arguments the buttons and axes mapping.

Source code and compilation

You can download the source code here: Joymap

Yes, it's only one file.

It requires only the libxdo-dev library, other than the standard libraries and tools to compile C source code on Linux.

You can compile the source code with the command:

$ gcc joymap.c -lxdo -o joymap

This will generate an executable called joymap.

Usage

You can get the help using the -h switch:

$ ./joymap -h Usage: joymap [OPTIONS] -[0-1A-F] Set the button character map (i.e. "-0 X" map button 0 to character X) -[pqrstu] Axis 0/1/2/3/4/5, data set as "up/left down/right" (i.e. '-r "Up Down"' map up/down arrow keys to axis 2) -d Device (i.e. /dev/input/js0) -x Only print raw data (for debug purposes) -h Show this message -v Print version information and exit -v Print version information and exit Use any character or X11 KeySym name to map buttons and axes

You can map buttons and axes to send any single character or special character (such like the arrow keys) using their X11 KeySym name (Google for it if you want a full list).

For example, I can play to Fantasy Zone (Sega) launching joymap in this way:

$ ./joymap -d /dev/input/js1 -r "Left Right" -t "Left Right" -q "Up Down" -u "Up Down" -1 d -3 s

In this case, I set the arrow keys twice to be able to use both the D-pad and the analog stick.

If you don't know which are the ID of the buttons and axes, you can use he -x switch to print raw data from the joypad (with some comments):

$ ./joymap -d /dev/input/js1 -x Using device /dev/input/js1 Device mapping is running. Press CTRL+C to stop... Device raw data: Time: 1869100; Type: 2; Button/Axis ID: 0; Value: 32767 //Axis (type=2) #0 pressed forward (high positive value) Time: 1869364; Type: 2; Button/Axis ID: 0; Value: 0 //Axis #0 released (value=0) Time: 1869596; Type: 2; Button/Axis ID: 1; Value: 32767 //Axis #1 pressed forward (positive value) Time: 1869748; Type: 2; Button/Axis ID: 1; Value: 0 //Axis #1 released Time: 1869988; Type: 2; Button/Axis ID: 0; Value: -32767 //Axis #0 pressed backward (negative value) Time: 1870140; Type: 2; Button/Axis ID: 0; Value: 0 //Axis #0 released Time: 1870444; Type: 1; Button/Axis ID: 1; Value: 1 //Button #1 pressed Time: 1870596; Type: 1; Button/Axis ID: 1; Value: 0 //Button #1 released Time: 1871156; Type: 1; Button/Axis ID: 3; Value: 1 //Button #3 pressed Time: 1871284; Type: 1; Button/Axis ID: 3; Value: 0 //Button #3 released Time: 1872324; Type: 1; Button/Axis ID: 6; Value: 1 //Button #6 pressed Time: 1872388; Type: 1; Button/Axis ID: 6; Value: 0 //Button #6 released

Full source code - joymap.c

I'm publishing here the full source code only for indexing purposes (download joymap.c here).

/** * @package Joymap - Joystick and joypad mapper * @author WizLab.it * @version 0.7.1 * @date 2017-08-26 * * How to compile: * $ gcc joymap.c -lxdo -o joymap * Requirements: libxdo-dev * * LICENSE * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * To get a copy of the GNU General Public License, see <http://www.gnu.org/licenses/>. */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <xdo.h> #include <fcntl.h> #define VERSION "0.7.1" #define MAX_AXES 6 #define MAX_BUTTONS 16 #define MAP_MAX_STRING_LENGTH 10 #define DEVICE_MAX_STRING_LENGTH 40 #define XDO_KEYSEQUENCE_DELAY 12000 #define JS_EVENT_BUTTON 0x01 #define JS_EVENT_AXIS 0x02 struct js_event { unsigned int time; short value; unsigned char type; unsigned char number; }; int main (int argc, char **argv) { char device[DEVICE_MAX_STRING_LENGTH + 1]; char axes[MAX_AXES][2][MAP_MAX_STRING_LENGTH + 1]; char buttons[MAX_BUTTONS][MAP_MAX_STRING_LENGTH + 1]; int axesCounters[MAX_AXES][2]; int buttonCounters[MAX_BUTTONS]; struct js_event jsEvent; xdo_t* xdo = xdo_new(":0.0"); int js; int rawDataFlag = 0; char *t; int c, i; //Initialize data memset(device, 0, (DEVICE_MAX_STRING_LENGTH + 1)); memset(axes, 0, (MAX_AXES * 2 * (MAP_MAX_STRING_LENGTH + 1))); memset(buttons, 0, (MAX_BUTTONS * (MAP_MAX_STRING_LENGTH + 1))); for(i=0; i<MAX_AXES; i++) { axesCounters[i][0] = 0; axesCounters[i][1] = 0; } for(i=0; i<MAX_BUTTONS; i++) buttonCounters[i] = 0; //parse arguments while((c = getopt(argc, argv, "0:1:2:3:4:5:6:7:8:9:A:B:C:D:E:F:d:p:q:r:s:t:u:hvx")) != -1) { switch (c) { //Usage case 'h': printf(" Usage: joymap [OPTIONS]\n"); printf(" -[0-1A-F]\t\tSet the button character map (i.e. \"-0 X\" map button 0 to character X)\n"); printf(" -[pqrstu]\t\tAxis 0/1/2/3/4/5, data set as \"up/left down/right\" (i.e. '-r \"Up Down\"' map up/down arrow keys to axis 2)\n"); printf(" -d\t\t\tDevice (i.e. /dev/input/js0)\n"); printf(" -x\t\t\tOnly print raw data (for debug purposes)\n"); printf(" -h\t\t\tShow this message\n"); printf(" -v\t\t\tPrint version information and exit\n"); printf(" Use any character or X11 KeySym name to map buttons and axes\n\n"); exit(0); //Author, version and license case 'v': printf(" Joymap - Joystick and joypad mapper ver. %s\n", VERSION); printf(" Copyright (C) 2017 WizLab.it <https://www.wizlab.it>\n\n"); printf(" This program is free software: you can redistribute it and/or modify\n"); printf(" it under the terms of the GNU General Public License as published by\n"); printf(" the Free Software Foundation, either version 3 of the License, or\n"); printf(" (at your option) any later version.\n\n"); printf(" This program is distributed in the hope that it will be useful,\n"); printf(" but WITHOUT ANY WARRANTY; without even the implied warranty of\n"); printf(" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n"); printf(" GNU General Public License for more details.\n\n"); printf(" To get a copy of the GNU General Public License, see <http://www.gnu.org/licenses/>.\n\n"); exit(0); //Device case 'd': strncpy(device, optarg, DEVICE_MAX_STRING_LENGTH); break; //Axes case 'p': case 'q': case 'r': case 's': case 't': case 'u': if((c >= 'p') && (c <= 'u')) c = c - 'p'; else break; i = 0; t = strtok(optarg, " "); while(t != NULL) { strncpy(axes[c][i], t, MAP_MAX_STRING_LENGTH); i++; if(i == 2) break; t = strtok(NULL, " "); } if(i < 2) memset(axes[c], 0, (2 * (MAP_MAX_STRING_LENGTH + 1))); break; //Buttons case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': case 'A': case 'B': case 'C': case 'D': case 'E': case 'F': if((c >= '0') && (c <= '9')) c = c - '0'; else if((c >= 'A') && (c <= 'F')) c = c - 'A' + 10; else break; strncpy(buttons[c], optarg, MAP_MAX_STRING_LENGTH); break; //Set raw data flag case 'x': rawDataFlag = 1; break; //Unknown and invalid arguments case '?': fprintf(stderr, " Invalid argiments\n\n"); return 1; default: abort(); } } //If device is not set, use the default one if(device[0] == '\0') strcpy(device, "/dev/input/js0"); //Print configuration printf(" Using device %s\n", device); for(i=0; i<MAX_BUTTONS; i++) { if(buttons[i][0] != '\0') printf(" Button %d => %s\n", i, buttons[i]); } for(i=0; i<MAX_AXES; i++) { if(axes[i][0][0] != '\0') printf(" Axis %d:\n High => %s\n Low => %s\n", i, axes[i][0], axes[i][1]); } //Open device js = open(device, O_RDONLY); if(js < 0) { fprintf(stderr, "\n ERROR: device %s is not available\n\n", device); exit(0); } //Device opened printf("\n Device mapping is running. Press CTRL+C to stop...\n\n"); if(rawDataFlag == 1) printf("Device raw data:\n"); //Run! while(1) { read(js, &jsEvent, sizeof(jsEvent)); if(rawDataFlag == 1) { printf("Time: %d; Type: %d; Button/Axis ID: %d; Value: %d\n", jsEvent.time, jsEvent.type, jsEvent.number, jsEvent.value); } else { switch(jsEvent.type) { case JS_EVENT_BUTTON: if((jsEvent.number >= 0) && (jsEvent.number < MAX_BUTTONS)) { if(jsEvent.value == 1) { xdo_send_keysequence_window_down(xdo, CURRENTWINDOW, buttons[jsEvent.number], XDO_KEYSEQUENCE_DELAY); buttonCounters[jsEvent.number]++; } else { for(; buttonCounters[jsEvent.number]>0; buttonCounters[jsEvent.number]--) xdo_send_keysequence_window_up(xdo, CURRENTWINDOW, buttons[jsEvent.number], XDO_KEYSEQUENCE_DELAY); } } break; case JS_EVENT_AXIS: if((jsEvent.number >= 0) && (jsEvent.number < MAX_AXES)) { if(jsEvent.value > 0) { for(; axesCounters[jsEvent.number][0]>0; axesCounters[jsEvent.number][0]--) xdo_send_keysequence_window_up(xdo, CURRENTWINDOW, axes[jsEvent.number][0], XDO_KEYSEQUENCE_DELAY); xdo_send_keysequence_window_down(xdo, CURRENTWINDOW, axes[jsEvent.number][1], XDO_KEYSEQUENCE_DELAY); axesCounters[jsEvent.number][1]++; } else if(jsEvent.value < 0) { for(; axesCounters[jsEvent.number][1]>0; axesCounters[jsEvent.number][1]--) xdo_send_keysequence_window_up(xdo, CURRENTWINDOW, axes[jsEvent.number][1], XDO_KEYSEQUENCE_DELAY); xdo_send_keysequence_window_down(xdo, CURRENTWINDOW, axes[jsEvent.number][0], XDO_KEYSEQUENCE_DELAY); axesCounters[jsEvent.number][0]++; } else { for(; axesCounters[jsEvent.number][1]>0; axesCounters[jsEvent.number][1]--) xdo_send_keysequence_window_up(xdo, CURRENTWINDOW, axes[jsEvent.number][1], XDO_KEYSEQUENCE_DELAY); for(; axesCounters[jsEvent.number][0]>0; axesCounters[jsEvent.number][0]--) xdo_send_keysequence_window_up(xdo, CURRENTWINDOW, axes[jsEvent.number][0], XDO_KEYSEQUENCE_DELAY); } } break; } } } return 0; }