alarm-tegra-x1 / device-tools/jcmapper.c

raw file 16634 bytes
/*
 * jcmapper - Nintendo Switch Joy-Con -> virtual mouse+keyboard (uinput)
 *
 * Custom control scheme (supersedes the official Switchroot L4T table):
 *
 *   LEFT Joy-Con                      RIGHT Joy-Con
 *   ------------------------------    ------------------------------
 *   ZL / LT  -> right click           ZR / RT  -> left click
 *   L  / LB  -> Page Up               R  / RB  -> Page Down
 *   Minus    -> rotate screen         Plus     -> toggle on-screen kbd
 *   L3       -> Caps Lock             X        -> Escape
 *   Screenshot -> enable/disable all  A        -> Enter
 *   L-stick  -> mouse motion          B        -> Backspace
 *   D-pad    -> arrow keys            Y        -> task switcher (Alt+Tab)
 *                                     R3       -> middle click
 *                                     R-stick  -> scrolling (wheel)
 *                                     Home     -> meta key (Super)
 *
 * Screenshot and Plus work even while the mapping is disabled, so the
 * mapping can always be turned back on and the OSK always reachable.
 *
 * Why userspace: xf86-input-joystick no longer exists in Arch, and X's
 * evdev driver treats a spring-return analog stick as an ABSOLUTE pointer,
 * so releasing it teleports the cursor to screen centre. Emitting REL_X/
 * REL_Y with a deadzone fixes that.
 *
 * Two uinput devices are created (mouse / keyboard) because X attaches a
 * slave to exactly one master, so a combined device would only reach one.
 * The mouse device also advertises REL_WHEEL/REL_HWHEEL for R-stick scrolling.
 *
 * Usage: jcmapper [-v]      (-v dumps raw events, for verifying mappings)
 */
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <poll.h>
#include <errno.h>
#include <linux/input.h>
#include <linux/uinput.h>
#include <sys/ioctl.h>
#include <sys/wait.h>
#include <sys/file.h>
#include <signal.h>

#define JC_LEFT_NAME  "Nintendo Switch Left Joy-Con Serial"
#define JC_RIGHT_NAME "Nintendo Switch Right Joy-Con Serial"
#define MOUSE_NAME    "ALARM JoyCon Mouse"
#define KBD_NAME      "ALARM JoyCon Keyboard"

#define DEADZONE  5000   /* out of 32767 */
#define MAXPX     10     /* max pixels per 8ms tick at full deflection.
                         * 14 was too sensitive at full tilt (~1750 px/s);
                         * 10 gives ~1250 px/s while keeping the fine
                         * low-deflection control from the quadratic curve. */
#define TICK_MS   8

static int mouse_fd = -1, kbd_fd = -1;
static int verbose = 0;
static int enabled = 1;           /* Screenshot toggles this */
static int lock_fd = -1;          /* single-instance lock */
static pid_t osk_pid = 0;         /* on-screen keyboard child, if running */

/* fractional wheel accumulation for R-stick scrolling */
static double wheel_acc_y = 0.0, wheel_acc_x = 0.0;

/* fractional pixel accumulation for L-stick motion */
static double axis_acc_x = 0.0, axis_acc_y = 0.0;

static int iabs(int v) { return v < 0 ? -v : v; }

/* ---------------- uinput setup ---------------- */

static int uinput_create(const char *name, int with_rel, int with_kbd)
{
    int fd = open("/dev/uinput", O_WRONLY | O_NONBLOCK);
    if (fd < 0) { fprintf(stderr, "open /dev/uinput: %s\n", strerror(errno)); return -1; }

    ioctl(fd, UI_SET_EVBIT, EV_KEY);
    if (with_rel) {
        ioctl(fd, UI_SET_EVBIT, EV_REL);
        ioctl(fd, UI_SET_RELBIT, REL_X);
        ioctl(fd, UI_SET_RELBIT, REL_Y);
        ioctl(fd, UI_SET_RELBIT, REL_WHEEL);     /* R-stick vertical scroll */
        ioctl(fd, UI_SET_RELBIT, REL_HWHEEL);    /* R-stick horizontal      */
        ioctl(fd, UI_SET_KEYBIT, BTN_LEFT);
        ioctl(fd, UI_SET_KEYBIT, BTN_RIGHT);
        ioctl(fd, UI_SET_KEYBIT, BTN_MIDDLE);
    }
    if (with_kbd) {
        /* Broad key range so X reliably classifies this as a keyboard. */
        int k;
        for (k = 1; k < 256; k++) ioctl(fd, UI_SET_KEYBIT, k);
    }

    struct uinput_user_dev u;
    memset(&u, 0, sizeof u);
    snprintf(u.name, UINPUT_MAX_NAME_SIZE, "%s", name);
    u.id.bustype = BUS_VIRTUAL;
    u.id.vendor  = 0x057e;   /* Nintendo */
    u.id.product = with_rel ? 0x1001 : 0x1002;
    u.id.version = 1;

    if (write(fd, &u, sizeof u) != (ssize_t)sizeof u) {
        fprintf(stderr, "uinput write: %s\n", strerror(errno));
        close(fd); return -1;
    }
    if (ioctl(fd, UI_DEV_CREATE) < 0) {
        fprintf(stderr, "UI_DEV_CREATE: %s\n", strerror(errno));
        close(fd); return -1;
    }
    return fd;
}

static void emit(int fd, unsigned short type, unsigned short code, int value)
{
    struct input_event e;
    if (fd < 0) return;
    memset(&e, 0, sizeof e);
    e.type = type; e.code = code; e.value = value;
    if (write(fd, &e, sizeof e) != (ssize_t)sizeof e)
        fprintf(stderr, "emit write: %s\n", strerror(errno));
}

static void send_sync(int fd) { emit(fd, EV_SYN, SYN_REPORT, 0); }

static void kbd_key(int code, int val)
{
    emit(kbd_fd, EV_KEY, code, val);
    send_sync(kbd_fd);
    if (verbose) fprintf(stderr, "KBD %s %d -> %s\n",
        val ? "down" : "up", code, val ? "" : "");
}

static void mouse_btn(int code, int val)
{
    emit(mouse_fd, EV_KEY, code, val);
    send_sync(mouse_fd);
}

/* Toggle the on-screen keyboard: spawn it if not running, kill it if it is.
 * Child is started detached with DISPLAY/XAUTHORITY set because jcmapper is
 * launched from the shell-based init and inherits no X environment. */
static void osk_toggle(void)
{
    if (osk_pid > 0) {
        if (kill(osk_pid, SIGTERM) == 0)
            fprintf(stderr, "jcmapper: on-screen keyboard closed\n");
        waitpid(osk_pid, NULL, 0);
        osk_pid = 0;
        return;
    }

    pid_t p = fork();
    if (p < 0) {
        fprintf(stderr, "jcmapper: fork failed: %s\n", strerror(errno));
        return;
    }
    if (p == 0) {
        setsid();
        setenv("DISPLAY", ":0", 1);
        setenv("XAUTHORITY", "/root/.Xauthority", 1);
        execl("/usr/local/bin/jcosk", "jcosk", (char *)NULL);
        _exit(127);
    }
    osk_pid = p;
    fprintf(stderr, "jcmapper: on-screen keyboard started (pid %d)\n", (int)p);
}

/* ---------------- control scheme ---------------- */
/* side: 0 = left Joy-Con, 1 = right Joy-Con */

/* press+release a key immediately (for one-shot actions) */
static void tap_key(int code)
{
    kbd_key(code, 1);
    kbd_key(code, 0);
}

/* Y: Alt+Tab window switcher (OpenBox default binding) */
static void task_switcher(void)
{
    kbd_key(KEY_LEFTALT, 1);
    kbd_key(KEY_TAB, 1);
    kbd_key(KEY_TAB, 0);
    kbd_key(KEY_LEFTALT, 0);
}

/* Minus: cycle CW -> CCW -> UD -> none, then restart X to apply.
 * fbdev rotation is only read at server start and xrandr rejects it
 * ("using driver rotation"), so a restart is unavoidable. */
static void rotate_screen(void)
{
    pid_t p = fork();
    if (p == 0) {
        setsid();
        execl("/usr/local/bin/rotate-next", "rotate-next", (char *)NULL);
        _exit(127);
    }
    fprintf(stderr, "jcmapper: rotating screen\n");
}

/* Disabling mid-drag would otherwise leave a mouse button stuck down. */
static void release_buttons(void)
{
    mouse_btn(BTN_LEFT, 0);
    mouse_btn(BTN_RIGHT, 0);
    mouse_btn(BTN_MIDDLE, 0);
}

static void handle_key(int side, unsigned short code, int val)
{
    if (verbose)
        fprintf(stderr, "[%s] code=%u val=%d%s\n",
                side ? "R" : "L", code, val, enabled ? "" : " (off)");

    /* Screenshot: enable/disable the whole scheme. Always honoured so the
     * mapping can always be turned back on. */
    if (side == 0 && code == BTN_Z) {
        if (val == 1) {
            if (enabled) release_buttons();
            enabled = !enabled;
            fprintf(stderr, "jcmapper: mapping %s\n",
                    enabled ? "ENABLED" : "DISABLED");
        }
        return;
    }

    /* Plus: always toggles the OSK, so there is a way to type even while
     * the rest of the mapping is disabled. */
    if (side == 1 && code == BTN_START) {
        if (val == 1) osk_toggle();
        return;
    }

    if (!enabled) return;

    if (side == 0) {                       /* LEFT Joy-Con */
        switch (code) {
        case BTN_TL2:       mouse_btn(BTN_RIGHT,  val); return;   /* ZL/LT right  */
        case BTN_TL:        kbd_key(KEY_PAGEUP,   val); return;   /* L/LB  PgUp   */
        case BTN_SELECT:    if (val == 1) rotate_screen(); return;/* -  rotate    */
        case BTN_THUMBL:    if (val == 1) tap_key(KEY_CAPSLOCK); return; /* L3 caps */
        case BTN_DPAD_UP:   kbd_key(KEY_UP,       val); return;
        case BTN_DPAD_DOWN: kbd_key(KEY_DOWN,     val); return;
        case BTN_DPAD_LEFT: kbd_key(KEY_LEFT,     val); return;
        case BTN_DPAD_RIGHT:kbd_key(KEY_RIGHT,    val); return;
        }
    } else {                               /* RIGHT Joy-Con */
        switch (code) {
        case BTN_TR2:    mouse_btn(BTN_LEFT,  val); return;        /* ZR/RT left   */
        case BTN_TR:     kbd_key(KEY_PAGEDOWN, val); return;       /* R/RB  PgDn   */
        case BTN_NORTH:  if (val == 1) tap_key(KEY_ESC); return;   /* X  Escape    */
        case BTN_EAST:   kbd_key(KEY_ENTER,   val); return;        /* A  Enter     */
        case BTN_SOUTH:  kbd_key(KEY_BACKSPACE, val); return;      /* B  Backspace */
        case BTN_WEST:   if (val == 1) task_switcher(); return;    /* Y  Alt+Tab   */
        case BTN_THUMBR: mouse_btn(BTN_MIDDLE, val); return;       /* R3 middle    */
        case BTN_MODE:   if (val == 1) tap_key(KEY_LEFTMETA); return; /* Home Super */
        }
    }
}

/* stick -> relative motion, quadratic curve for fine control near centre.
 *
 * Returns a FRACTIONAL pixel count; the caller accumulates it and emits
 * whole pixels. Doing the division in double is essential: the old integer
 * version truncated twice (`a*a/span`, then `*MAXPX/span`), which collapsed
 * the output to 0 until ~38% deflection instead of the intended 15%
 * deadzone, then jumped straight into a steep ramp. That dead-then-lurch
 * response is what read as "twitchy". */
static double axis_delta(int v)
{
    int a, span;
    double d;
    if (iabs(v) < DEADZONE) return 0.0;
    span = 32767 - DEADZONE;
    a = iabs(v) - DEADZONE;
    d = (double)a * a / span;      /* quadratic: gentle near centre */
    d = d * MAXPX / span;          /* 0..MAXPX */
    return v < 0 ? -d : d;
}

/* Right stick -> wheel notches. Returns a fractional notch rate; the caller
 * accumulates and emits whole REL_WHEEL units. MAXNOTCH caps how many notches
 * per 8ms tick, i.e. how fast a document can fly past.
 *
 * Same truncation bug as axis_delta: with MAXNOTCH=3 the integer path stayed
 * at 0 until ~64% deflection. Computed in double it responds from the
 * deadzone onward, and the accumulator smooths it into whole notches. */
#define MAXNOTCH 3
static double scroll_delta(int v)
{
    int a, span;
    double d;
    if (iabs(v) < DEADZONE) return 0.0;
    span = 32767 - DEADZONE;
    a = iabs(v) - DEADZONE;
    d = (double)a * a / span;
    d = d * MAXNOTCH / span;
    return v < 0 ? -d : d;
}

/* ---------------- device discovery ---------------- */

static int open_by_name(const char *want)
{
    FILE *f = fopen("/proc/bus/input/devices", "r");
    char line[512], handlers[256] = "";
    int match = 0, result = -1;
    if (!f) return -1;
    (void)0;

    while (fgets(line, sizeof line, f)) {
        if (!strncmp(line, "N: Name=", 8)) {
            match = (strstr(line, want) != NULL);
            handlers[0] = 0;
        } else if (!strncmp(line, "H: Handlers=", 12)) {
            snprintf(handlers, sizeof handlers, "%s", line + 12);
        } else if (line[0] == '\n') {
            if (match && handlers[0]) {
                char *p = strstr(handlers, "event");
                if (p) {
                    char path[64];
                    snprintf(path, sizeof path, "/dev/input/event%d", atoi(p + 5));
                    result = open(path, O_RDONLY | O_NONBLOCK);
                    if (result >= 0) break;
                }
            }
            match = 0; handlers[0] = 0;
        }
    }
    fclose(f);
    return result;
}

int main(int argc, char **argv)
{
    int fds[2] = { -1, -1 };
    const char *names[2] = { JC_LEFT_NAME, JC_RIGHT_NAME };
    int stick[2][2] = { {0,0}, {0,0} };   /* last known stick pos per side */
    int i, retry = 0;

    for (i = 1; i < argc; i++)
        if (!strcmp(argv[i], "-v")) verbose = 1;

    /* Single instance. Two mappers both attach to the same Joy-Con fds and
     * both create a jcosk window, which then fight over XRaiseWindow and
     * flicker; every button also fires twice. flock fails fast if held. */
    lock_fd = open("/var/run/jcmapper.lock", O_CREAT | O_RDWR, 0644);
    if (lock_fd < 0 || flock(lock_fd, LOCK_EX | LOCK_NB) < 0) {
        fprintf(stderr, "jcmapper: another instance is running, exiting\n");
        if (lock_fd >= 0) close(lock_fd);
        return 0;
    }

    mouse_fd = uinput_create(MOUSE_NAME, 1, 0);
    kbd_fd   = uinput_create(KBD_NAME,   0, 1);
    if (mouse_fd < 0 || kbd_fd < 0) return 1;
    usleep(300000);   /* let the nodes register */

    fprintf(stderr, "jcmapper: created '%s' and '%s'%s\n",
            MOUSE_NAME, KBD_NAME, verbose ? " (verbose)" : "");

    for (;;) {
        struct pollfd p[2];
        int n = 0, idx[2] = { -1, -1 };

        for (i = 0; i < 2; i++) {
            if (fds[i] < 0 && retry <= 0) {
                fds[i] = open_by_name(names[i]);
                if (fds[i] >= 0)
                    fprintf(stderr, "jcmapper: attached to %s Joy-Con\n",
                            i ? "right" : "left");
            }
            if (fds[i] >= 0) { p[n].fd = fds[i]; p[n].events = POLLIN; idx[n] = i; n++; }
        }
        if (retry > 0) retry--;

        if (poll(p, n, TICK_MS) > 0) {
            for (i = 0; i < n; i++) {
                struct input_event e;
                ssize_t r;
                if (!(p[i].revents & POLLIN)) continue;
                while ((r = read(p[i].fd, &e, sizeof e)) == (ssize_t)sizeof e) {
                    if (e.type == EV_KEY) {
                        handle_key(idx[i], e.code, e.value);
                    } else if (e.type == EV_ABS) {
                        /* remember position; motion is emitted below so a
                           held stick keeps moving (evdev only reports change) */
                        if (e.code == ABS_X || e.code == ABS_RX)
                            stick[idx[i]][0] = e.value;
                        else if (e.code == ABS_Y || e.code == ABS_RY)
                            stick[idx[i]][1] = e.value;
                    }
                }
                if (r < 0 && errno == ENODEV) {   /* Joy-Con reseated */
                    fprintf(stderr, "jcmapper: %s Joy-Con gone, will reattach\n",
                            idx[i] ? "right" : "left");
                    close(fds[i]); fds[i] = -1; retry = 125;
                }
            }
        }

        /* reap the OSK if it exited on its own */
        if (osk_pid > 0 && waitpid(osk_pid, NULL, WNOHANG) == osk_pid)
            osk_pid = 0;

        if (enabled) {
            /* left stick -> mouse motion.
             *
             * axis_delta() now returns a fractional pixel count, so accumulate
             * it and only emit on whole pixels. Casting straight to int here
             * would reintroduce the truncation that made the curve dead below
             * 38% deflection. Residue is deliberately kept: it is what lets a
             * slow, near-centre nudge still move the cursor one pixel. */
            axis_acc_x += axis_delta(stick[0][0]);
            axis_acc_y += axis_delta(stick[0][1]);
            {
                int dx = (int)axis_acc_x;
                int dy = (int)axis_acc_y;
                if (dx) { emit(mouse_fd, EV_REL, REL_X, dx); axis_acc_x -= dx; }
                if (dy) { emit(mouse_fd, EV_REL, REL_Y, dy); axis_acc_y -= dy; }
            }

            /* right stick -> scrolling, accumulated into whole notches.
             * stick down scrolls down, stick right scrolls right. */
            wheel_acc_y -= scroll_delta(stick[1][1]);
            wheel_acc_x += scroll_delta(stick[1][0]);
            {
                int ny = (int)wheel_acc_y;
                int nx = (int)wheel_acc_x;
                if (ny) { emit(mouse_fd, EV_REL, REL_WHEEL, ny); wheel_acc_y -= ny; }
                if (nx) { emit(mouse_fd, EV_REL, REL_HWHEEL, nx); wheel_acc_x -= nx; }
            }
        }
        send_sync(mouse_fd);
    }
    return 0;
}