/* * 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 #include #include #include #include #include #include #include #include #include #include /* ------------------------------------------------------------------ */ /* 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; }