Last active
July 9, 2025 10:59
-
-
Save TheGammaSqueeze/b4d3fcbf3403c8c38a00ea85c5d3312f to your computer and use it in GitHub Desktop.
Retroid Pocket 4 - controller userspace 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. | |
* − Performs a simple 50‐frame auto‐center calibration of both analog sticks | |
* at startup. | |
* | |
* Compile (on AArch64): | |
* gcc -O3 -march=armv8-a -o RetroidPad RetroidPad.c -lm | |
* | |
* 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> | |
#include <math.h> /* for powf() */ | |
/* 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, BTN_MODE }, /* 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 | |
/* Raw ADC bits (12-bit) → uinput range */ | |
#define TRIGGER_MAX 0x0FFF /* 4095 */ | |
/* Deadzone in output units (0..4095) */ | |
#define TRIGGER_DEADZONE 200 | |
/* Fraction of raw travel that maps to full output */ | |
#define TRIGGER_THRESHOLD 0.85f | |
/* Number of frames to sample for stick centering */ | |
#define STICK_CAL_FRAMES 50 | |
/* stick axis clipping range */ | |
#define STICK_CLIP 1300 | |
/* file‐scope stick zero offsets (auto‐calibrated) */ | |
static int calib_zero_LX = 0, calib_zero_LY = 0, calib_zero_RX = 0, calib_zero_RY = 0; | |
/* 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/ttyS1 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. | |
* • ABS_X/ABS_Y, ABS_Z/ABS_RZ for analog sticks. | |
* • ABS_GAS and ABS_BRAKE (0..4095) for analog triggers. | |
* 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)); | |
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_Z)); /* second stick X */ | |
UI_CHECK(ioctl(fd, UI_SET_ABSBIT, ABS_RZ)); /* second stick Y */ | |
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] = -STICK_CLIP; uidev.absmax[ABS_X] = STICK_CLIP; | |
uidev.absmin[ABS_Y] = -STICK_CLIP; uidev.absmax[ABS_Y] = STICK_CLIP; | |
uidev.absmin[ABS_Z] = -STICK_CLIP; uidev.absmax[ABS_Z] = STICK_CLIP; | |
uidev.absmin[ABS_RZ] = -STICK_CLIP; uidev.absmax[ABS_RZ] = STICK_CLIP; | |
uidev.absmin[ABS_GAS] = 0; uidev.absmax[ABS_GAS] = TRIGGER_MAX; | |
uidev.absmin[ABS_BRAKE] = 0; uidev.absmax[ABS_BRAKE] = TRIGGER_MAX; | |
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) ───────────────────── */ | |
static int fun_pwm_init(int sfd) { | |
uint8_t pkt[17] = { | |
0xA5,0xD3,0x5A,0x3D, | |
0x05,0x02,0x08,0x00, | |
0x00,0x00,0x00,0xEF, | |
0x00,0x00,0x00,0x0B, | |
0x00 | |
}; | |
pkt[16] = compute_checksum(pkt,17); | |
return write_all(sfd,pkt,17)==17?0:-1; | |
} | |
static int set_fan_enable(int sfd, uint8_t e, uint32_t sp) { | |
uint8_t pkt[17] = { | |
0xA5,0xD3,0x5A,0x3D, | |
0x05,0x03,0x08,0x00, | |
0x00,0x00,0x00, e, | |
(uint8_t)(sp>>24),(uint8_t)(sp>>16), | |
(uint8_t)(sp>>8),(uint8_t)sp, | |
0x00 | |
}; | |
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/ttyS1 */ | |
int sfd = open_serial("/dev/ttyS1"); | |
if (sfd < 0) return EXIT_FAILURE; | |
printf("[INFO] Opened MCU serial /dev/ttyS1 (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"); | |
/***** Stick Auto‐Center Calibration *****/ | |
{ | |
printf("[INFO] Calibrating sticks: sampling %d frames...\n", STICK_CAL_FRAMES); | |
int64_t sum_LX = 0, sum_LY = 0, sum_RX = 0, sum_RY = 0; | |
int count = 0; | |
uint8_t buf[1500]; size_t buf_len = 0; | |
while (count < STICK_CAL_FRAMES) { | |
/* 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; | |
} | |
/* read available */ | |
int waiting=0; | |
ioctl(sfd, FIONREAD, &waiting); | |
if (waiting <= 0) continue; | |
if (waiting > (int)(sizeof(buf)-buf_len)) waiting = sizeof(buf)-buf_len; | |
ssize_t r = read(sfd, buf+buf_len, waiting); | |
if (r <= 0) continue; | |
buf_len += r; | |
/* parse for first 0x02 packet */ | |
size_t off = 0; | |
while (buf_len - off >= 8 && count < STICK_CAL_FRAMES) { | |
if (buf[off]!=0xA5||buf[off+1]!=0xD3||buf[off+2]!=0x5A||buf[off+3]!=0x3D) { | |
off++; continue; | |
} | |
uint8_t cmd = buf[off+5]; | |
uint16_t dlen = buf[off+6] | (buf[off+7]<<8); | |
size_t plen = 4+1+1+2+dlen+1; | |
if (buf_len-off < plen) break; | |
/* checksum */ | |
uint8_t cs = buf[off+4]; | |
for (size_t i=off+5; i<off+plen-1; i++) cs ^= buf[i]; | |
if (cs != buf[off+plen-1]) { off++; continue; } | |
if (cmd == 0x02 && dlen >= 14) { | |
const uint8_t *d = buf + off + 8; | |
int16_t lx = (int16_t)(d[6] | (d[7]<<8)); | |
int16_t ly = (int16_t)(d[8] | (d[9]<<8)); | |
int16_t rx = (int16_t)(d[10] | (d[11]<<8)); | |
int16_t ry = (int16_t)(d[12] | (d[13]<<8)); | |
sum_LX += lx; sum_LY += ly; | |
sum_RX += rx; sum_RY += ry; | |
count++; | |
} | |
off += plen; | |
} | |
/* shift leftover */ | |
if (off>0) { | |
memmove(buf, buf+off, buf_len-off); | |
buf_len -= off; | |
} | |
} | |
/* compute averages */ | |
calib_zero_LX = (int)(sum_LX / STICK_CAL_FRAMES); | |
calib_zero_LY = (int)(sum_LY / STICK_CAL_FRAMES); | |
calib_zero_RX = (int)(sum_RX / STICK_CAL_FRAMES); | |
calib_zero_RY = (int)(sum_RY / STICK_CAL_FRAMES); | |
printf("[INFO] Stick centers: LX=%d, LY=%d, RX=%d, RY=%d\n", | |
calib_zero_LX, calib_zero_LY, calib_zero_RX, calib_zero_RY); | |
} | |
/***** End calibration *****/ | |
/* 5) Main loop: read and parse button packets */ | |
uint8_t buf[1500]; 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_analog = 0, prev_R2_analog = 0; | |
int prev_btn_L2 = 0, prev_btn_R2 = 0; /* digital trigger state */ | |
int prev_LX = 0, prev_LY = 0, prev_Z = 0, prev_RZ = 0; | |
int idle_skip_counter = 0; | |
/* dynamic rest calibration triggers */ | |
int rest_L2 = -1, rest_R2 = -1; | |
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 >= 14) { | |
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[32]; | |
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 | |
}; | |
} | |
if (hatY != prev_hatY) { | |
evs[ev_count++] = (struct input_event){ | |
.time=tv, .type=EV_ABS, | |
.code=ABS_HAT0Y, .value=hatY | |
}; | |
} | |
/* 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 | |
}; | |
} | |
} | |
} | |
/* Shoulder buttons L1/R1 (digital) */ | |
int pressed_L1 = (buttons & (1u<<8))?1:0; | |
int pressed_R1 = (buttons & (1u<<9))?1:0; | |
if (pressed_L1 != prev_L1) { | |
evs[ev_count++] = (struct input_event){ | |
.time=tv, .type=EV_KEY, | |
.code=BTN_TL, .value=pressed_L1 | |
}; | |
} | |
if (pressed_R1 != prev_R1) { | |
evs[ev_count++] = (struct input_event){ | |
.time=tv, .type=EV_KEY, | |
.code=BTN_TR, .value=pressed_R1 | |
}; | |
} | |
/* ─── Analog sticks ───────────────────────────────── */ | |
int16_t raw_LX = (int16_t)(data[6] | (data[7]<<8)); | |
int16_t raw_LY = (int16_t)(data[8] | (data[9]<<8)); | |
int16_t raw_RX = (int16_t)(data[10] | (data[11]<<8)); | |
int16_t raw_RY = (int16_t)(data[12] | (data[13]<<8)); | |
/* subtract calibrated center */ | |
raw_LX -= calib_zero_LX; | |
raw_LY -= calib_zero_LY; | |
raw_RX -= calib_zero_RX; | |
raw_RY -= calib_zero_RY; | |
/* flip X/Y */ | |
raw_LX = -raw_LX; | |
raw_LY = -raw_LY; | |
raw_RX = -raw_RX; | |
raw_RY = -raw_RY; | |
/* clamp to ±STICK_CLIP */ | |
if (raw_LX > STICK_CLIP) raw_LX = STICK_CLIP; | |
if (raw_LX < -STICK_CLIP) raw_LX = -STICK_CLIP; | |
if (raw_LY > STICK_CLIP) raw_LY = STICK_CLIP; | |
if (raw_LY < -STICK_CLIP) raw_LY = -STICK_CLIP; | |
if (raw_RX > STICK_CLIP) raw_RX = STICK_CLIP; | |
if (raw_RX < -STICK_CLIP) raw_RX = -STICK_CLIP; | |
if (raw_RY > STICK_CLIP) raw_RY = STICK_CLIP; | |
if (raw_RY < -STICK_CLIP) raw_RY = -STICK_CLIP; | |
if (raw_LX != prev_LX) { | |
evs[ev_count++] = (struct input_event){ | |
.time=tv, .type=EV_ABS, | |
.code=ABS_X, .value=raw_LX | |
}; | |
} | |
if (raw_LY != prev_LY) { | |
evs[ev_count++] = (struct input_event){ | |
.time=tv, .type=EV_ABS, | |
.code=ABS_Y, .value=raw_LY | |
}; | |
} | |
/* right stick → Z/RZ */ | |
if (raw_RX != prev_Z) { | |
evs[ev_count++] = (struct input_event){ | |
.time=tv, .type=EV_ABS, | |
.code=ABS_Z, .value=raw_RX | |
}; | |
} | |
if (raw_RY != prev_RZ) { | |
evs[ev_count++] = (struct input_event){ | |
.time=tv, .type=EV_ABS, | |
.code=ABS_RZ, .value=raw_RY | |
}; | |
} | |
/* ─── Analog triggers ───────────────────────────────── */ | |
uint16_t raw_L2_12 = (data[2] | (data[3]<<8)) >> 4; | |
uint16_t raw_R2_12 = (data[4] | (data[5]<<8)) >> 4; | |
if (rest_R2 < 0) rest_R2 = raw_R2_12; | |
if (rest_L2 < 0) rest_L2 = raw_L2_12; | |
float frac_gas = (rest_R2 - raw_R2_12) / (float)rest_R2; | |
float frac_brake = (rest_L2 - raw_L2_12) / (float)rest_L2; | |
if (frac_gas<0) frac_gas=0; if (frac_gas>1) frac_gas=1; | |
if (frac_brake<0) frac_brake=0; if (frac_brake>1) frac_brake=1; | |
float dz = TRIGGER_DEADZONE / (float)TRIGGER_MAX; | |
if (frac_gas < dz) frac_gas = 0; | |
if (frac_brake < dz) frac_brake = 0; | |
float sg = frac_gas / TRIGGER_THRESHOLD; if (sg > 1) sg = 1; | |
float sb = frac_brake / TRIGGER_THRESHOLD; if (sb > 1) sb = 1; | |
int analog_GAS = (int)(sg * TRIGGER_MAX + 0.5f); | |
int analog_BRAKE = (int)(sb * TRIGGER_MAX + 0.5f); | |
if (analog_GAS != prev_R2_analog) { | |
evs[ev_count++] = (struct input_event){ | |
.time=tv, .type=EV_ABS, | |
.code=ABS_GAS, .value=analog_GAS | |
}; | |
} | |
if (analog_BRAKE != prev_L2_analog) { | |
evs[ev_count++] = (struct input_event){ | |
.time=tv, .type=EV_ABS, | |
.code=ABS_BRAKE, .value=analog_BRAKE | |
}; | |
} | |
/* ─── Digital trigger fallback at 60% ───────────────────── */ | |
int thresh = (int)(0.6f * (float)TRIGGER_MAX + 0.5f); | |
int btn_R2 = analog_GAS >= thresh ? 1 : 0; | |
int btn_L2 = analog_BRAKE >= thresh ? 1 : 0; | |
if (btn_R2 != prev_btn_R2) { | |
evs[ev_count++] = (struct input_event){ | |
.time=tv, .type=EV_KEY, | |
.code=BTN_TR2, .value=btn_R2 | |
}; | |
} | |
if (btn_L2 != prev_btn_L2) { | |
evs[ev_count++] = (struct input_event){ | |
.time=tv, .type=EV_KEY, | |
.code=BTN_TL2, .value=btn_L2 | |
}; | |
} | |
/* EV_SYN + write */ | |
if (ev_count > 0) { | |
evs[ev_count++] = (struct input_event){ | |
.time=tv, .type=EV_SYN, | |
.code=SYN_REPORT, .value=0 | |
}; | |
write_all(ufd, evs, ev_count * sizeof(evs[0])); | |
} | |
/* Update previous state */ | |
prev_buttons = buttons; | |
prev_hatX = hatX; | |
prev_hatY = hatY; | |
prev_L1 = pressed_L1; | |
prev_R1 = pressed_R1; | |
prev_L2_analog = analog_BRAKE; | |
prev_R2_analog = analog_GAS; | |
prev_btn_L2 = btn_L2; | |
prev_btn_R2 = btn_R2; | |
prev_LX = raw_LX; | |
prev_LY = raw_LY; | |
prev_Z = raw_RX; | |
prev_RZ = raw_RY; | |
} | |
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