Created
July 8, 2025 22:42
-
-
Save TheGammaSqueeze/bde22bc76f0d0463adabf78af6aa6b70 to your computer and use it in GitHub Desktop.
Retroid Pocket Classic - Userspace controller driver
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* RetroidPad.c | |
* | |
* − Uses select() + ioctl(FIONREAD) to batch‐read all available bytes in one | |
* go (instead of VMIN) so that we only wake up once per MCU‐burst. | |
* − Adds an “idle skip” mechanism: if the button state remains unchanged for | |
* several consecutive packets, we skip the detailed per-packet parsing until | |
* a new packet arrives or a threshold is reached. | |
* | |
* Compile (on AArch64): | |
* gcc -O3 -march=armv8-a -o RetroidPad RetroidPad.c | |
* | |
* Run (as root): | |
* chmod +x RetroidPad | |
* ./RetroidPad "Retroid Pocket Controller" | |
* | |
* © 2025 | |
*/ | |
#define _POSIX_C_SOURCE 199309L /* for clock_gettime() */ | |
#include <stdio.h> | |
#include <stdlib.h> | |
#include <string.h> | |
#include <unistd.h> | |
#include <fcntl.h> | |
#include <errno.h> | |
#include <termios.h> | |
#include <sys/ioctl.h> | |
#include <linux/uinput.h> | |
#include <time.h> | |
#include <sys/time.h> | |
#include <stdint.h> | |
#include <sys/select.h> | |
/* Mapping of bits 4..7 and 10..15 to EV_KEY codes */ | |
typedef struct { | |
uint16_t mask; | |
int code; | |
} face_map_t; | |
static const face_map_t face_map[] = { | |
{ 1u << 4, BTN_NORTH }, /* bit 4 */ | |
{ 1u << 5, BTN_WEST }, /* bit 5 */ | |
{ 1u << 6, BTN_SOUTH }, /* bit 6 */ | |
{ 1u << 7, BTN_EAST }, /* bit 7 */ | |
{ 1u << 10, BTN_SELECT }, /* bit 10 */ | |
{ 1u << 11, BTN_START }, /* bit 11 */ | |
{ 1u << 12, BTN_THUMBL }, /* bit 12 */ | |
{ 1u << 13, BTN_THUMBR }, /* bit 13 */ | |
{ 1u << 14, KEY_BACK }, /* bit 14 */ | |
{ 1u << 15, KEY_BACK } /* bit 15 */ | |
}; | |
static const size_t face_map_count = sizeof(face_map)/sizeof(face_map[0]); | |
/* Mask covering bits 4..7 and 10..15 */ | |
#define FACE_MASK ((uint16_t)0xFCF0) /* 0x00F0 | 0xFC00 */ | |
/* Threshold: number of consecutive “no-change” packets to skip */ | |
#define IDLE_SKIP_THRESH 5 | |
/* Write exactly len bytes (handling partial writes) */ | |
static inline ssize_t write_all(int fd, const void *buf, size_t len) { | |
const uint8_t *p = buf; | |
size_t total = 0; | |
while (total < len) { | |
ssize_t ret = write(fd, p + total, len - total); | |
if (ret < 0) { | |
if (errno == EINTR) continue; | |
return -1; | |
} | |
total += ret; | |
} | |
return total; | |
} | |
/* Compute the 1‐byte XOR checksum over bytes [4..(count–2)] */ | |
static inline uint8_t compute_checksum(const uint8_t *buf, size_t count) { | |
uint8_t x = buf[4]; | |
for (size_t i = 5; i < count - 1; i++) { | |
x ^= buf[i]; | |
} | |
return x; | |
} | |
/* Send the 6 initialization sequences, with 100 ms between each */ | |
static int send_init_sequences(int serial_fd) { | |
static const uint8_t seq1[] = { | |
0xA5,0xD3,0x5A,0x3D, 0x00,0x01,0x02,0x00, 0x07,0x01,0x05 | |
}; | |
static const uint8_t seq2[] = { | |
0xA5,0xD3,0x5A,0x3D, 0x01,0x01,0x01,0x00, 0x06,0x07 | |
}; | |
static const uint8_t seq3[] = { | |
0xA5,0xD3,0x5A,0x3D, 0x02,0x01,0x01,0x00, 0x02,0x00 | |
}; | |
static const uint8_t seq4[] = { | |
0xA5,0xD3,0x5A,0x3D, | |
0x03,0x01,0x0A,0x00, 0x05,0x01,0x00,0x00, | |
0x00,0x28,0x00,0x00,0x00,0x07,0x23 | |
}; | |
static const uint8_t seq5[] = { | |
0xA5,0xD3,0x5A,0x3D, 0x04,0x01,0x01,0x00, 0x06,0x02 | |
}; | |
static const uint8_t seq6[] = { | |
0xA5,0xD3,0x5A,0x3D, 0x05,0x01,0x01,0x00, 0x02,0x07 | |
}; | |
const uint8_t *seqs[] = { seq1, seq2, seq3, seq4, seq5, seq6 }; | |
const size_t lens[] = { sizeof(seq1), sizeof(seq2), sizeof(seq3), | |
sizeof(seq4), sizeof(seq5), sizeof(seq6) }; | |
for (int i = 0; i < 6; i++) { | |
if (write_all(serial_fd, seqs[i], lens[i]) < 0) return -1; | |
usleep(100000); | |
} | |
return 0; | |
} | |
/* | |
* Open /dev/ttyHS1 as 115200 8N1, no flow control, raw mode, | |
* with VMIN=1, VTIME=0 (so read() will return as soon as ≥1 byte is ready). | |
* Returns a *blocking* fd or -1 on error. | |
*/ | |
static int open_serial(const char *path) { | |
int fd = open(path, O_RDWR | O_NOCTTY | O_NONBLOCK); | |
if (fd < 0) { | |
perror("open serial"); | |
return -1; | |
} | |
struct termios t; | |
if (tcgetattr(fd, &t) < 0) { | |
perror("tcgetattr"); | |
close(fd); | |
return -1; | |
} | |
t.c_iflag &= ~(INPCK|ISTRIP|IXON|IXOFF|BRKINT|ICRNL|IGNCR|IGNBRK); | |
t.c_oflag &= ~OPOST; | |
t.c_cflag &= ~(CSIZE|PARENB|CRTSCTS); | |
t.c_cflag |= CS8|CLOCAL|CREAD; | |
t.c_lflag &= ~(ICANON|ECHO|ECHOE|ISIG); | |
cfsetispeed(&t, B115200); | |
cfsetospeed(&t, B115200); | |
t.c_cc[VMIN] = 1; | |
t.c_cc[VTIME] = 0; | |
if (tcsetattr(fd, TCSANOW, &t) < 0) { | |
perror("tcsetattr"); | |
close(fd); | |
return -1; | |
} | |
int flags = fcntl(fd, F_GETFL); | |
fcntl(fd, F_SETFL, flags & ~O_NONBLOCK); | |
return fd; | |
} | |
/* | |
* Create a virtual gamepad via /dev/uinput, enabling: | |
* • face buttons + Select/Start/Thumb/Back (10 codes) | |
* • L1 (BTN_TL), R1 (BTN_TR), L2 (BTN_TL2), R2 (BTN_TR2) | |
* • ABS_HAT0X, ABS_HAT0Y (−1..+1) for the D‐pad. | |
* Returns uinput fd or -1 on error. | |
*/ | |
static int uinput_create(const char *devname) { | |
int fd = open("/dev/uinput", O_WRONLY | O_NONBLOCK); | |
if (fd < 0) { perror("open uinput"); return -1; } | |
#define UI_CHECK(x) if ((x) < 0) { perror(#x); close(fd); return -1; } | |
UI_CHECK(ioctl(fd, UI_SET_EVBIT, EV_KEY)); | |
for (size_t i = 0; i < face_map_count; i++) { | |
UI_CHECK(ioctl(fd, UI_SET_KEYBIT, face_map[i].code)); | |
} | |
UI_CHECK(ioctl(fd, UI_SET_KEYBIT, BTN_TL)); | |
UI_CHECK(ioctl(fd, UI_SET_KEYBIT, BTN_TR)); | |
UI_CHECK(ioctl(fd, UI_SET_KEYBIT, BTN_TL2)); | |
UI_CHECK(ioctl(fd, UI_SET_KEYBIT, BTN_TR2)); | |
UI_CHECK(ioctl(fd, UI_SET_EVBIT, EV_ABS)); | |
UI_CHECK(ioctl(fd, UI_SET_ABSBIT, ABS_HAT0X)); | |
UI_CHECK(ioctl(fd, UI_SET_ABSBIT, ABS_HAT0Y)); | |
/* Manually enable remaining ABS bits */ | |
UI_CHECK(ioctl(fd, UI_SET_ABSBIT, ABS_X)); | |
UI_CHECK(ioctl(fd, UI_SET_ABSBIT, ABS_Y)); | |
UI_CHECK(ioctl(fd, UI_SET_ABSBIT, ABS_RX)); | |
UI_CHECK(ioctl(fd, UI_SET_ABSBIT, ABS_RY)); | |
UI_CHECK(ioctl(fd, UI_SET_ABSBIT, ABS_GAS)); | |
UI_CHECK(ioctl(fd, UI_SET_ABSBIT, ABS_BRAKE)); | |
struct uinput_user_dev uidev; | |
memset(&uidev, 0, sizeof(uidev)); | |
snprintf(uidev.name, UINPUT_MAX_NAME_SIZE, "%s", devname); | |
uidev.id.bustype = BUS_USB; | |
uidev.id.vendor = 0x2020; | |
uidev.id.product = 0x3001; | |
uidev.absmin[ABS_HAT0X] = -1; uidev.absmax[ABS_HAT0X] = 1; | |
uidev.absmin[ABS_HAT0Y] = -1; uidev.absmax[ABS_HAT0Y] = 1; | |
uidev.absmin[ABS_X] = -32768; uidev.absmax[ABS_X] = 32767; | |
uidev.absmin[ABS_Y] = -32768; uidev.absmax[ABS_Y] = 32767; | |
uidev.absmin[ABS_RX] = -32768; uidev.absmax[ABS_RX] = 32767; | |
uidev.absmin[ABS_RY] = -32768; uidev.absmax[ABS_RY] = 32767; | |
uidev.absmin[ABS_GAS] = 0; uidev.absmax[ABS_GAS] = 32767; | |
uidev.absmin[ABS_BRAKE] = 0; uidev.absmax[ABS_BRAKE] = 32767; | |
UI_CHECK(write_all(fd, &uidev, sizeof(uidev))); | |
UI_CHECK(ioctl(fd, UI_DEV_CREATE)); | |
#undef UI_CHECK | |
return fd; | |
} | |
/* Returns current monotonic time in microseconds */ | |
static inline uint64_t now_us_monotonic(void) { | |
struct timespec ts; | |
clock_gettime(CLOCK_MONOTONIC, &ts); | |
return (uint64_t)ts.tv_sec * 1000000ULL + (ts.tv_nsec / 1000ULL); | |
} | |
/* ─── Fan control via MCU commands (no extra library) ───────────────────── */ | |
/* | |
* fun_pwm_init: wake up MCU PWM engine (cmd=5, chan=2) | |
*/ | |
static int fun_pwm_init(int sfd) { | |
uint8_t pkt[17] = { | |
0xA5,0xD3,0x5A,0x3D, /* header */ | |
0x05,0x02, /* cmd=5,chan=2 */ | |
0x08,0x00, /* data length = 8 */ | |
0x00,0x00,0x00,0xEF, /* payload[0..3] */ | |
0x00,0x00,0x00,0x0B, /* payload[4..7] */ | |
0x00 /* checksum placeholder */ | |
}; | |
pkt[16] = compute_checksum(pkt, 17); | |
return write_all(sfd, pkt, 17) == 17 ? 0 : -1; | |
} | |
/* | |
* set_fan_enable: turn fan on/off at given 32-bit speed (cmd=5, chan=3) | |
*/ | |
static int set_fan_enable(int sfd, uint8_t enable, uint32_t speed) { | |
uint8_t pkt[17] = { | |
0xA5,0xD3,0x5A,0x3D, /* header */ | |
0x05,0x03, /* cmd=5,chan=3 */ | |
0x08,0x00, /* data length = 8 */ | |
0x00,0x00,0x00, enable, /* enable */ | |
(uint8_t)(speed>>24),(uint8_t)(speed>>16), | |
(uint8_t)(speed>>8), (uint8_t)(speed), | |
0x00 /* checksum placeholder */ | |
}; | |
pkt[16] = compute_checksum(pkt, 17); | |
return write_all(sfd, pkt, 17) == 17 ? 0 : -1; | |
} | |
/* ─── MAIN ──────────────────────────────────────────────────────────────── */ | |
int main(int argc, char *argv[]) { | |
if (argc != 2) { | |
fprintf(stderr, "Usage: %s \"Retroid Pocket Controller\"\n", argv[0]); | |
return EXIT_FAILURE; | |
} | |
/* 1) Create uinput device */ | |
int ufd = uinput_create(argv[1]); | |
if (ufd < 0) return EXIT_FAILURE; | |
printf("[INFO] Virtual gamepad \"%s\" created (uinput_fd=%d)\n", argv[1], ufd); | |
/* 2) Open and configure /dev/ttyHS1 */ | |
int sfd = open_serial("/dev/ttyHS1"); | |
if (sfd < 0) return EXIT_FAILURE; | |
printf("[INFO] Opened MCU serial /dev/ttyHS1 (fd=%d)\n", sfd); | |
/* 3) Send MCU init sequences */ | |
if (send_init_sequences(sfd) < 0) { | |
fprintf(stderr, "[ERROR] send_init_sequences failed\n"); | |
close(sfd); | |
return EXIT_FAILURE; | |
} | |
printf("[INFO] Sent init sequences\n"); | |
/* 4) Initialize PWM engine & spin fan at full speed */ | |
if (fun_pwm_init(sfd) < 0) { | |
fprintf(stderr, "[ERROR] fun_pwm_init failed\n"); | |
close(sfd); | |
return EXIT_FAILURE; | |
} | |
printf("[DEBUG] fun_pwm_init() -> 0\n"); | |
if (set_fan_enable(sfd, 1, 0xFFFFFFFFu) < 0) { | |
fprintf(stderr, "[ERROR] set_fan_enable failed\n"); | |
close(sfd); | |
return EXIT_FAILURE; | |
} | |
printf("[DEBUG] set_fan_enable(100%%) -> 0\n"); | |
printf("[INFO] Fan should now be at full speed, entering input loop...\n"); | |
/* 5) Main loop: read and parse button packets */ | |
uint8_t buf[1024]; | |
size_t buf_len = 0; | |
uint64_t last_valid_us = now_us_monotonic(); | |
uint16_t prev_buttons = 0; | |
int prev_hatX = 0, prev_hatY = 0; | |
int prev_L1 = 0, prev_R1 = 0; | |
int prev_L2 = 0, prev_R2 = 0; | |
int idle_skip_counter = 0; | |
while (1) { | |
/* 5a) Resend init if >1 s without a valid packet */ | |
uint64_t now_us = now_us_monotonic(); | |
if (now_us - last_valid_us > 1000000ULL) { | |
send_init_sequences(sfd); | |
last_valid_us = now_us_monotonic(); | |
buf_len = 0; | |
idle_skip_counter = 0; | |
/* re-kick fan each reconnect */ | |
fun_pwm_init(sfd); | |
set_fan_enable(sfd, 1, 0xFFFFFFFFu); | |
} | |
/* 5b) Wait for data */ | |
fd_set rfds; | |
FD_ZERO(&rfds); | |
FD_SET(sfd, &rfds); | |
if (select(sfd + 1, &rfds, NULL, NULL, NULL) < 0) { | |
if (errno == EINTR) continue; | |
perror("select"); break; | |
} | |
/* 5c) Check available bytes */ | |
int waiting = 0; | |
if (ioctl(sfd, FIONREAD, &waiting) < 0) { | |
perror("ioctl FIONREAD"); break; | |
} | |
if (waiting <= 0) continue; | |
if (waiting > (int)(sizeof(buf) - buf_len)) waiting = sizeof(buf) - buf_len; | |
/* 5d) Read all waiting bytes */ | |
ssize_t r = read(sfd, buf + buf_len, waiting); | |
if (r < 0) { | |
if (errno == EINTR) continue; | |
perror("read"); break; | |
} | |
if (r > 0) buf_len += r; | |
/* 5e) Parse packets */ | |
size_t offset = 0; | |
while (buf_len - offset >= 8) { | |
if (buf[offset] != 0xA5 || buf[offset+1] != 0xD3 || | |
buf[offset+2] != 0x5A || buf[offset+3] != 0x3D) { | |
offset++; | |
continue; | |
} | |
uint8_t cmd = buf[offset+5]; | |
uint16_t data_len = buf[offset+6] | (buf[offset+7] << 8); | |
size_t packet_len = 4 + 1 + 1 + 2 + data_len + 1; | |
if (buf_len - offset < packet_len) break; | |
/* Validate checksum */ | |
uint8_t cs = buf[offset+4]; | |
for (size_t i = offset+5; i < offset+packet_len-1; i++) { | |
cs ^= buf[i]; | |
} | |
if (cs != buf[offset+packet_len-1]) { | |
offset++; | |
continue; | |
} | |
/* Process button state packets (cmd=0x02) */ | |
if (cmd == 0x02 && data_len >= 2) { | |
const uint8_t *data = buf + offset + 8; | |
uint16_t buttons = data[0] | (data[1] << 8); | |
if (buttons == prev_buttons) { | |
idle_skip_counter++; | |
if (idle_skip_counter < IDLE_SKIP_THRESH) { | |
offset += packet_len; | |
continue; | |
} | |
idle_skip_counter = 0; | |
} else { | |
idle_skip_counter = 0; | |
} | |
last_valid_us = now_us_monotonic(); | |
struct timeval tv; | |
gettimeofday(&tv, NULL); | |
struct input_event evs[16]; | |
int ev_count = 0; | |
/* D-pad → HAT axes */ | |
int hatX = ((buttons & (1u<<3))?1:0) - ((buttons & (1u<<2))?1:0); | |
int hatY = ((buttons & (1u<<1))?1:0) - ((buttons & (1u<<0))?1:0); | |
if (hatX != prev_hatX) { | |
evs[ev_count] = (struct input_event){ .time=tv, .type=EV_ABS, | |
.code=ABS_HAT0X, .value=hatX }; | |
ev_count++; | |
} | |
if (hatY != prev_hatY) { | |
evs[ev_count] = (struct input_event){ .time=tv, .type=EV_ABS, | |
.code=ABS_HAT0Y, .value=hatY }; | |
ev_count++; | |
} | |
/* Face & Select/Start/Thumb/Back */ | |
uint16_t changed_face = (buttons ^ prev_buttons) & FACE_MASK; | |
if (changed_face) { | |
for (size_t i = 0; i < face_map_count; i++) { | |
if (changed_face & face_map[i].mask) { | |
evs[ev_count] = (struct input_event){ | |
.time=tv, .type=EV_KEY, | |
.code=face_map[i].code, | |
.value=(buttons & face_map[i].mask)?1:0 | |
}; | |
ev_count++; | |
} | |
} | |
} | |
/* Shoulder buttons */ | |
int pressed_L1 = (buttons & (1u<<8))?1:0; | |
int pressed_R1 = (buttons & (1u<<9))?1:0; | |
int pressed_L2 = (data[2] & 0x01)?1:0; | |
int pressed_R2 = (data[4] & 0x01)?1:0; | |
if (pressed_L1 != prev_L1) { | |
evs[ev_count] = (struct input_event){ .time=tv, .type=EV_KEY, | |
.code=BTN_TL, .value=pressed_L1 }; | |
ev_count++; | |
} | |
if (pressed_R1 != prev_R1) { | |
evs[ev_count] = (struct input_event){ .time=tv, .type=EV_KEY, | |
.code=BTN_TR, .value=pressed_R1 }; | |
ev_count++; | |
} | |
if (pressed_L2 != prev_L2) { | |
evs[ev_count] = (struct input_event){ .time=tv, .type=EV_KEY, | |
.code=BTN_TL2, .value=pressed_L2 }; | |
ev_count++; | |
} | |
if (pressed_R2 != prev_R2) { | |
evs[ev_count] = (struct input_event){ .time=tv, .type=EV_KEY, | |
.code=BTN_TR2, .value=pressed_R2 }; | |
ev_count++; | |
} | |
/* EV_SYN */ | |
if (hatX!=prev_hatX || hatY!=prev_hatY || | |
changed_face || pressed_L1!=prev_L1 || | |
pressed_R1!=prev_R1 || pressed_L2!=prev_L2 || | |
pressed_R2!=prev_R2 || | |
(buttons==prev_buttons && changed_face==0 && ev_count==0 && idle_skip_counter==0)) | |
{ | |
evs[ev_count] = (struct input_event){ | |
.time=tv, .type=EV_SYN, .code=SYN_REPORT, .value=0 | |
}; | |
ev_count++; | |
} | |
if (ev_count > 0) { | |
write_all(ufd, evs, ev_count * sizeof(evs[0])); | |
} | |
prev_buttons = buttons; | |
prev_hatX = hatX; | |
prev_hatY = hatY; | |
prev_L1 = pressed_L1; | |
prev_R1 = pressed_R1; | |
prev_L2 = pressed_L2; | |
prev_R2 = pressed_R2; | |
} | |
offset += packet_len; | |
} | |
/* Shift leftover bytes down */ | |
if (offset > 0) { | |
memmove(buf, buf + offset, buf_len - offset); | |
buf_len -= offset; | |
} | |
} | |
/* Cleanup */ | |
close(sfd); | |
ioctl(ufd, UI_DEV_DESTROY); | |
close(ufd); | |
return EXIT_SUCCESS; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment