456 lines
18 KiB
C
Executable File
456 lines
18 KiB
C
Executable File
/*
|
|
* retro_input_mapper.c -- H2 Gamepad Input Translator Daemon
|
|
*
|
|
* Translates the HiFiWalker H2's physical controls into a standard
|
|
* Linux gamepad (uinput) that emulators can read natively.
|
|
*
|
|
* Creates /dev/input/js0 (or next available) as a virtual gamepad
|
|
* with: d-pad (4-way), 6 action buttons (A B X Y L R), Start, Select.
|
|
*
|
|
* ================================================================
|
|
* PHYSICAL LAYOUT → GAMEPAD MAPPING
|
|
* ================================================================
|
|
*
|
|
* HiFiWalker H2 Top View
|
|
* ┌──────────────────────────┐
|
|
* │ │
|
|
* │ LCD 320x240 │
|
|
* │ │
|
|
* └──────────────────────────┘
|
|
*
|
|
* ┌──────────────────────────┐
|
|
* │ │
|
|
* │ ╭──────────╮ │
|
|
* │ │ ROTARY │ │ ROTARY WHEEL
|
|
* │ │ ENCODER │ │
|
|
* │ ╰──────────╯ │
|
|
* │ │
|
|
* │ [PREV] [NEXT] │ L-SHOULDER R-SHOULDER
|
|
* │ │
|
|
* │ [PLAY] │ A BUTTON (confirm/jump/shoot)
|
|
* │ [BACK] │ B BUTTON (cancel/run)
|
|
* │ │
|
|
* └──────────────────────────┘
|
|
*
|
|
* ┌─────────────────────────────────────────────────────────────┐
|
|
* │ GAMEPAD LAYOUT │
|
|
* │ │
|
|
* │ [SELECT] [START] │
|
|
* │ │
|
|
* │ ┌───┐ │
|
|
* │ │ ▲ │ Rotary CCW │
|
|
* │ ┌───┼───┼───┐ │
|
|
* │ │◄ │ │ ►│ Rotary: click-switch toggles │
|
|
* │ └───┼───┼───┘ between V-axis (U/D) and │
|
|
* │ │ ▼ │ Rotary CW H-axis (L/R) │
|
|
* │ └───┘ │
|
|
* │ │
|
|
* │ [L] [R] │
|
|
* │ PREV btn NEXT btn │
|
|
* │ │
|
|
* │ [Y] │
|
|
* │ [X] [B] PLAY = A │
|
|
* │ [A] BACK = B (short press) │
|
|
* │ BACK = SELECT (long press) │
|
|
* └─────────────────────────────────────────────────────────────┘
|
|
*
|
|
* ================================================================
|
|
* MAPPING TABLE
|
|
* ================================================================
|
|
*
|
|
* H2 Physical Virtual Gamepad Notes
|
|
* -------------- ---------------- -----
|
|
* Rotary CCW D-pad Up / Left Axis toggled by rotary click
|
|
* Rotary CW D-pad Down / Right
|
|
* Rotary press (EV_KEY) Toggle d-pad axis V-mode(U/D) ↔ H-mode(L/R)
|
|
* PLAY (164) short A (BTN_SOUTH) Confirm / jump / shoot
|
|
* PLAY (164) long 1.5s START (BTN_START) Pause / menu
|
|
* BACK (158) short B (BTN_EAST) Cancel / run
|
|
* BACK (158) long 1.5s SELECT (BTN_SELECT) In-game menu
|
|
* PREV (165) L (BTN_TL) Shoulder left
|
|
* NEXT (163) R (BTN_TR) Shoulder right
|
|
* (rotary+PLAY combo) X (BTN_NORTH) Context action
|
|
* (rotary+BACK combo) Y (BTN_WEST) Context action
|
|
*
|
|
* When the d-pad is in V-mode (default):
|
|
* Rotary CW = D-pad Down
|
|
* Rotary CCW = D-pad Up
|
|
* Left/Right = hold rotary-click + rotate
|
|
*
|
|
* When the d-pad is in H-mode (after rotary click):
|
|
* Rotary CW = D-pad Right
|
|
* Rotary CCW = D-pad Left
|
|
* Up/Down = hold rotary-click + rotate
|
|
*
|
|
* Auto-revert: d-pad returns to V-mode after 3 seconds of no
|
|
* rotary activity (prevents stuck in H-mode).
|
|
*
|
|
* ================================================================
|
|
* USAGE
|
|
* ================================================================
|
|
*
|
|
* This daemon should be started by the init system BEFORE any
|
|
* emulator is launched. It reads /dev/input/event0 (the H2's
|
|
* physical input) and writes to the virtual uinput gamepad.
|
|
*
|
|
* Emulators should be configured to read from the uinput gamepad
|
|
* device rather than /dev/input/event0 directly.
|
|
*
|
|
* The daemon exits cleanly on SIGTERM or SIGINT.
|
|
*
|
|
* Build: mipsel-linux-musl-gcc -o retro_input_mapper \
|
|
* retro_input_mapper.c
|
|
*
|
|
* ================================================================
|
|
*/
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <unistd.h>
|
|
#include <fcntl.h>
|
|
#include <signal.h>
|
|
#include <errno.h>
|
|
#include <linux/input.h>
|
|
#include <linux/uinput.h>
|
|
#include <sys/time.h>
|
|
#include <time.h>
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* H2 input event codes */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
#define H2_KEY_PLAY 164
|
|
#define H2_KEY_BACK 158
|
|
#define H2_KEY_PREV 165 /* previous track / left side button */
|
|
#define H2_KEY_NEXT 163 /* next track / right side button */
|
|
#define H2_ROTARY_REL 0 /* EV_REL code for rotary encoder */
|
|
#define H2_ROTARY_PRESS 115 /* EV_KEY code if rotary is pressable */
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Virtual gamepad button indices (Linux input subsystem) */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
#define VBTN_A BTN_SOUTH /* 304 - confirm / jump / shoot */
|
|
#define VBTN_B BTN_EAST /* 305 - cancel / run */
|
|
#define VBTN_X BTN_NORTH /* 308 - context action */
|
|
#define VBTN_Y BTN_WEST /* 307 - context action */
|
|
#define VBTN_L BTN_TL /* 310 - left shoulder */
|
|
#define VBTN_R BTN_TR /* 311 - right shoulder */
|
|
#define VBTN_SELECT BTN_SELECT /* 314 - in-game select */
|
|
#define VBTN_START BTN_START /* 315 - in-game start / pause */
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* D-pad axis mode */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
#define DAXIS_VERT 0 /* rotary -> up/down (default) */
|
|
#define DAXIS_HORIZ 1 /* rotary -> left/right */
|
|
|
|
static volatile sig_atomic_t running = 1;
|
|
|
|
static void signal_handler(int sig) {
|
|
(void)sig;
|
|
running = 0;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Time helper: milliseconds since epoch */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
static long long now_ms(void) {
|
|
struct timespec ts;
|
|
clock_gettime(CLOCK_MONOTONIC, &ts);
|
|
return (long long)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Create the virtual uinput gamepad device */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
static int create_gamepad(int *uinput_fd) {
|
|
int fd = open("/dev/uinput", O_WRONLY | O_NONBLOCK);
|
|
if (fd < 0) {
|
|
/* try /dev/input/uinput as fallback */
|
|
fd = open("/dev/input/uinput", O_WRONLY | O_NONBLOCK);
|
|
}
|
|
if (fd < 0) {
|
|
perror("open /dev/uinput");
|
|
return -1;
|
|
}
|
|
|
|
/* --- device identification --- */
|
|
struct uinput_setup usetup;
|
|
memset(&usetup, 0, sizeof(usetup));
|
|
snprintf(usetup.name, UINPUT_MAX_NAME_SIZE, "H2 Virtual Gamepad");
|
|
usetup.id.bustype = BUS_VIRTUAL;
|
|
usetup.id.vendor = 0x4832; /* "H2" */
|
|
usetup.id.product = 0x0001;
|
|
usetup.id.version = 1;
|
|
|
|
/* --- enable d-pad axes --- */
|
|
ioctl(fd, UI_SET_EVBIT, EV_ABS);
|
|
ioctl(fd, UI_SET_ABSBIT, ABS_X); /* d-pad horizontal */
|
|
ioctl(fd, UI_SET_ABSBIT, ABS_Y); /* d-pad vertical */
|
|
|
|
struct uinput_abs_setup abs_x = { .code = ABS_X,
|
|
.min = -1, .max = 1, .fuzz = 0, .flat = 0, .resolution = 0 };
|
|
struct uinput_abs_setup abs_y = { .code = ABS_Y,
|
|
.min = -1, .max = 1, .fuzz = 0, .flat = 0, .resolution = 0 };
|
|
ioctl(fd, UI_ABS_SETUP, &abs_x);
|
|
ioctl(fd, UI_ABS_SETUP, &abs_y);
|
|
|
|
/* --- enable buttons --- */
|
|
ioctl(fd, UI_SET_EVBIT, EV_KEY);
|
|
ioctl(fd, UI_SET_KEYBIT, VBTN_A);
|
|
ioctl(fd, UI_SET_KEYBIT, VBTN_B);
|
|
ioctl(fd, UI_SET_KEYBIT, VBTN_X);
|
|
ioctl(fd, UI_SET_KEYBIT, VBTN_Y);
|
|
ioctl(fd, UI_SET_KEYBIT, VBTN_L);
|
|
ioctl(fd, UI_SET_KEYBIT, VBTN_R);
|
|
ioctl(fd, UI_SET_KEYBIT, VBTN_SELECT);
|
|
ioctl(fd, UI_SET_KEYBIT, VBTN_START);
|
|
|
|
/* --- create the device --- */
|
|
if (ioctl(fd, UI_DEV_SETUP, &usetup) < 0) {
|
|
perror("UI_DEV_SETUP");
|
|
close(fd);
|
|
return -1;
|
|
}
|
|
if (ioctl(fd, UI_DEV_CREATE) < 0) {
|
|
perror("UI_DEV_CREATE");
|
|
close(fd);
|
|
return -1;
|
|
}
|
|
|
|
*uinput_fd = fd;
|
|
return 0;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Emit a single uinput event */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
static void emit_event(int fd, uint16_t type, uint16_t code,
|
|
int32_t value) {
|
|
struct input_event ie;
|
|
memset(&ie, 0, sizeof(ie));
|
|
ie.type = type;
|
|
ie.code = code;
|
|
ie.value = value;
|
|
(void)write(fd, &ie, sizeof(ie));
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Emit a button press/release pair with a 10ms gap */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
static void emit_button(int fd, uint16_t btn, int pressed) {
|
|
emit_event(fd, EV_KEY, btn, pressed);
|
|
/* sync after each event for low-latency input */
|
|
emit_event(fd, EV_SYN, SYN_REPORT, 0);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Emit d-pad axis value and sync */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
static void emit_dpad(int fd, int axis_x, int axis_y) {
|
|
emit_event(fd, EV_ABS, ABS_X, axis_x);
|
|
emit_event(fd, EV_ABS, ABS_Y, axis_y);
|
|
emit_event(fd, EV_SYN, SYN_REPORT, 0);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* main */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
int main(void) {
|
|
/* --- signal handling --- */
|
|
struct sigaction sa;
|
|
memset(&sa, 0, sizeof(sa));
|
|
sa.sa_handler = signal_handler;
|
|
sigaction(SIGTERM, &sa, NULL);
|
|
sigaction(SIGINT, &sa, NULL);
|
|
|
|
/* --- create virtual gamepad --- */
|
|
int ufd = -1;
|
|
if (create_gamepad(&ufd) < 0) {
|
|
fprintf(stderr, "retro_input_mapper: failed to create "
|
|
"virtual gamepad\n");
|
|
return 1;
|
|
}
|
|
|
|
/* --- open H2 physical input --- */
|
|
int phys_fd = open("/dev/input/event0", O_RDONLY | O_NONBLOCK);
|
|
if (phys_fd < 0) {
|
|
perror("open /dev/input/event0");
|
|
ioctl(ufd, UI_DEV_DESTROY);
|
|
close(ufd);
|
|
return 1;
|
|
}
|
|
|
|
/*
|
|
* State machine
|
|
*/
|
|
int dpad_mode = DAXIS_VERT; /* current axis mode */
|
|
int dpad_x = 0; /* current d-pad X (-1/0/1) */
|
|
int dpad_y = 0; /* current d-pad Y (-1/0/1) */
|
|
int play_held = 0; /* is PLAY currently held? */
|
|
int back_held = 0; /* is BACK currently held? */
|
|
long long play_down_ms = 0; /* timestamp PLAY was pressed */
|
|
long long back_down_ms = 0; /* timestamp BACK was pressed */
|
|
long long last_rotary_ms = 0; /* last rotary activity */
|
|
int play_long_fired = 0; /* long-press already fired? */
|
|
int back_long_fired = 0; /* long-press already fired? */
|
|
|
|
#define LONG_PRESS_MS 1500
|
|
#define AXIS_REVERT_MS 3000
|
|
|
|
struct input_event ev;
|
|
|
|
while (running) {
|
|
ssize_t n = read(phys_fd, &ev, sizeof(ev));
|
|
|
|
if (n <= 0) {
|
|
/* no data -- check axis auto-revert */
|
|
if (dpad_mode == DAXIS_HORIZ) {
|
|
long long idle = now_ms() - last_rotary_ms;
|
|
if (idle > AXIS_REVERT_MS) {
|
|
dpad_mode = DAXIS_VERT;
|
|
}
|
|
}
|
|
|
|
/* check long-press timeouts */
|
|
long long now = now_ms();
|
|
if (play_held && !play_long_fired &&
|
|
(now - play_down_ms) >= LONG_PRESS_MS) {
|
|
play_long_fired = 1;
|
|
emit_button(ufd, VBTN_START, 1);
|
|
emit_button(ufd, VBTN_START, 0);
|
|
}
|
|
if (back_held && !back_long_fired &&
|
|
(now - back_down_ms) >= LONG_PRESS_MS) {
|
|
back_long_fired = 1;
|
|
emit_button(ufd, VBTN_SELECT, 1);
|
|
emit_button(ufd, VBTN_SELECT, 0);
|
|
}
|
|
|
|
usleep(5000); /* 5ms poll when idle */
|
|
continue;
|
|
}
|
|
|
|
/* ---- ROTARY ENCODER ---- */
|
|
if (ev.type == EV_REL && ev.code == H2_ROTARY_REL) {
|
|
last_rotary_ms = now_ms();
|
|
|
|
int step = (ev.value > 0) ? 1 : -1;
|
|
|
|
if (dpad_mode == DAXIS_VERT) {
|
|
/* vertical: rotary -> up/down */
|
|
dpad_y = step;
|
|
dpad_x = 0;
|
|
emit_dpad(ufd, 0, dpad_y);
|
|
/* release after 80ms for discrete steps */
|
|
usleep(80000);
|
|
dpad_y = 0;
|
|
emit_dpad(ufd, 0, 0);
|
|
} else {
|
|
/* horizontal: rotary -> left/right */
|
|
dpad_x = step;
|
|
dpad_y = 0;
|
|
emit_dpad(ufd, dpad_x, 0);
|
|
usleep(80000);
|
|
dpad_x = 0;
|
|
emit_dpad(ufd, 0, 0);
|
|
}
|
|
|
|
/* combo detection: if PLAY held during rotary -> X button */
|
|
if (play_held && !play_long_fired) {
|
|
emit_button(ufd, VBTN_X, 1);
|
|
emit_button(ufd, VBTN_X, 0);
|
|
}
|
|
/* combo: if BACK held during rotary -> Y button */
|
|
if (back_held && !back_long_fired) {
|
|
emit_button(ufd, VBTN_Y, 1);
|
|
emit_button(ufd, VBTN_Y, 0);
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
/* ---- ROTARY PRESS (toggle axis) ---- */
|
|
if (ev.type == EV_KEY && ev.code == H2_ROTARY_PRESS) {
|
|
if (ev.value == 1) {
|
|
/* toggle d-pad axis mode */
|
|
dpad_mode = (dpad_mode == DAXIS_VERT)
|
|
? DAXIS_HORIZ : DAXIS_VERT;
|
|
/* reset d-pad position on toggle */
|
|
dpad_x = 0;
|
|
dpad_y = 0;
|
|
emit_dpad(ufd, 0, 0);
|
|
last_rotary_ms = now_ms();
|
|
}
|
|
continue;
|
|
}
|
|
|
|
/* ---- BUTTONS ---- */
|
|
if (ev.type == EV_KEY) {
|
|
if (ev.code == H2_KEY_PLAY) {
|
|
if (ev.value == 1) {
|
|
play_held = 1;
|
|
play_long_fired = 0;
|
|
play_down_ms = now_ms();
|
|
} else {
|
|
/* released */
|
|
if (!play_long_fired) {
|
|
/* short press -> A button */
|
|
emit_button(ufd, VBTN_A, 1);
|
|
emit_button(ufd, VBTN_A, 0);
|
|
} else {
|
|
/* long press already sent START, release it */
|
|
emit_button(ufd, VBTN_START, 0);
|
|
}
|
|
play_held = 0;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (ev.code == H2_KEY_BACK) {
|
|
if (ev.value == 1) {
|
|
back_held = 1;
|
|
back_long_fired = 0;
|
|
back_down_ms = now_ms();
|
|
} else {
|
|
if (!back_long_fired) {
|
|
/* short press -> B button */
|
|
emit_button(ufd, VBTN_B, 1);
|
|
emit_button(ufd, VBTN_B, 0);
|
|
} else {
|
|
emit_button(ufd, VBTN_SELECT, 0);
|
|
}
|
|
back_held = 0;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (ev.code == H2_KEY_PREV) {
|
|
emit_button(ufd, VBTN_L, ev.value);
|
|
continue;
|
|
}
|
|
|
|
if (ev.code == H2_KEY_NEXT) {
|
|
emit_button(ufd, VBTN_R, ev.value);
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* --- cleanup --- */
|
|
emit_dpad(ufd, 0, 0);
|
|
ioctl(ufd, UI_DEV_DESTROY);
|
|
close(ufd);
|
|
close(phys_fd);
|
|
|
|
return 0;
|
|
} |