Skip to content

Instantly share code, notes, and snippets.

@TheGammaSqueeze
Created July 8, 2025 22:42
Show Gist options
  • Save TheGammaSqueeze/bde22bc76f0d0463adabf78af6aa6b70 to your computer and use it in GitHub Desktop.
Save TheGammaSqueeze/bde22bc76f0d0463adabf78af6aa6b70 to your computer and use it in GitHub Desktop.
Retroid Pocket Classic - Userspace controller driver
/*
* 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