OreBolt-OS/modules/orebolt-retro/retro_input_mapper.c

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;
}