Skip to content

Instantly share code, notes, and snippets.

@TheGammaSqueeze
Created January 15, 2025 13:15
Show Gist options
  • Select an option

  • Save TheGammaSqueeze/97ff17c6798a940d1d302eda3c6dd459 to your computer and use it in GitHub Desktop.

Select an option

Save TheGammaSqueeze/97ff17c6798a940d1d302eda3c6dd459 to your computer and use it in GitHub Desktop.
TrimUI smart pro android joypad userspace driver
/*******************************************************************************
* File: trimui_corrected.c
*
* Changes vs previous:
* - #define VENDOR_ID, PRODUCT_ID, VERSION_ID => 0x0000,0x0000,0x0001
* - Use ABS_Z, ABS_RZ instead of ABS_RX, ABS_RY
* - If L2 or R2 pressed => emit additional "gas" or "brake" events
* - Clamping from ±900 => ±32760
* - No repeated SYN if values are unchanged
******************************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdbool.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <pthread.h>
#include <termios.h>
#include <linux/input.h>
#include <linux/uinput.h>
#include <sys/ioctl.h>
/* --------------------------------------------------------------------------
Constants / defines
-------------------------------------------------------------------------- */
#define UINPUT_PATH "/dev/uinput"
#define VENDOR_ID 0x0000
#define PRODUCT_ID 0x0000
#define VERSION_ID 0x0001
#define DEVICE_NAME "retrogame_joypad"
// For 8-byte Trimui frames
#define MAGIC_START 0xFF
#define MAGIC_END 0xFE
#define FRAME_LEN 8
// We'll read from:
#define SERIAL_LEFT "/dev/ttyS4"
#define SERIAL_RIGHT "/dev/ttyS3"
// We calibrate using 50 frames
#define CAL_FRAMES 50
// Axis range in uinput
#define AXIS_MIN (-32760)
#define AXIS_MAX ( 32760)
// We'll clamp ±900 -> ±32760
static const int RAW_MAX = 900;
// 10% radial deadzone
static float g_deadzone = 0.10f * (float)AXIS_MAX;
// Additional definitions for gas/brake if not in your headers
#ifndef ABS_BRAKE
#define ABS_BRAKE 0x0115
#endif
#ifndef ABS_GAS
#define ABS_GAS 0x0116
#endif
// Button bits (for left side & right side)
static unsigned char old_btn_left = 0;
static unsigned char old_btn_right = 0;
// We'll keep track of the HAT states
static int old_hat_x = 0;
static int old_hat_y = 0;
/* --------------------------------------------------------------------------
We'll store last axis states to avoid re-emitting the same values
-------------------------------------------------------------------------- */
static int last_left_x = 999999;
static int last_left_y = 999999;
static int last_right_x = 999999;
static int last_right_y = 999999;
// We'll store last brake/gas for L2/R2
static int last_brake = -1; // 0 or 255
static int last_gas = -1;
// We also want to store last EV_KEY states to avoid spamming
// But we already do this by comparing old_btn_left, old_btn_right
/* --------------------------------------------------------------------------
Data structure for simple calibration
We'll accumulate sums of rawX, rawY for 50 frames => average => offset
-------------------------------------------------------------------------- */
typedef struct {
long sum_x;
long sum_y;
int count;
int offset_x;
int offset_y;
bool done;
} CalData;
static CalData calLeft = {0};
static CalData calRight = {0};
// The uinput fd
static int g_uinput_fd = -1;
/* --------------------------------------------------------------------------
(ADDED) Initialize power GPIO pins for the Trimui sticks.
This must run BEFORE opening /dev/ttyS4 or /dev/ttyS3 so the sticks are powered.
-------------------------------------------------------------------------- */
static void trimui_init_power_gpio(void)
{
// Make sure these exist and are set to output = 1, so the joysticks are powered
system("echo 110 > /sys/class/gpio/export");
system("echo -n out > /sys/class/gpio/gpio110/direction");
system("echo -n 1 > /sys/class/gpio/gpio110/value");
system("echo 114 > /sys/class/gpio/export");
system("echo -n out > /sys/class/gpio/gpio114/direction");
system("echo -n 1 > /sys/class/gpio/gpio114/value");
system("echo 243 > /sys/class/gpio/export");
system("echo -n in > /sys/class/gpio/gpio243/direction");
system("echo 107 > /sys/class/gpio/export");
system("echo -n out > /sys/class/gpio/gpio107/direction");
system("echo -n 1 > /sys/class/gpio/gpio107/value");
// short delay to let hardware settle
usleep(100 * 1000); // 100ms
}
/* --------------------------------------------------------------------------
Setup uinput
We'll map:
- Left stick => ABS_X, ABS_Y
- Right stick => ABS_Z, ABS_RZ
- D-Pad => ABS_HAT0X, ABS_HAT0Y
- Buttons => L1=BTN_TL, L2=BTN_TL2, R1=BTN_TR, R2=BTN_TR2, A=BTN_A, ...
We'll also set ABS_BRAKE, ABS_GAS => range 0..255
-------------------------------------------------------------------------- */
static int setup_uinput(void)
{
int fd = open(UINPUT_PATH, O_WRONLY | O_NONBLOCK);
if(fd < 0) {
fprintf(stderr, "Cannot open %s: %s\n", UINPUT_PATH, strerror(errno));
return -1;
}
// Key bits
ioctl(fd, UI_SET_EVBIT, EV_KEY);
ioctl(fd, UI_SET_KEYBIT, BTN_TL); // L1
ioctl(fd, UI_SET_KEYBIT, BTN_TL2); // L2
ioctl(fd, UI_SET_KEYBIT, BTN_TR); // R1
ioctl(fd, UI_SET_KEYBIT, BTN_TR2); // R2
ioctl(fd, UI_SET_KEYBIT, BTN_MODE); // Left side mode
ioctl(fd, UI_SET_KEYBIT, BTN_A);
ioctl(fd, UI_SET_KEYBIT, BTN_B);
ioctl(fd, UI_SET_KEYBIT, BTN_X);
ioctl(fd, UI_SET_KEYBIT, BTN_Y);
ioctl(fd, UI_SET_KEYBIT, BTN_SELECT);
ioctl(fd, UI_SET_KEYBIT, BTN_START);
// Abs bits
ioctl(fd, UI_SET_EVBIT, EV_ABS);
// We'll define these codes: ABS_X, ABS_Y => left stick
// ABS_Z, ABS_RZ => right stick
ioctl(fd, UI_SET_ABSBIT, ABS_X);
ioctl(fd, UI_SET_ABSBIT, ABS_Y);
ioctl(fd, UI_SET_ABSBIT, ABS_Z);
ioctl(fd, UI_SET_ABSBIT, ABS_RZ);
// D-pad => hat
ioctl(fd, UI_SET_ABSBIT, ABS_HAT0X);
ioctl(fd, UI_SET_ABSBIT, ABS_HAT0Y);
// Gas/Brake => 0..255
ioctl(fd, UI_SET_ABSBIT, ABS_BRAKE);
ioctl(fd, UI_SET_ABSBIT, ABS_GAS);
// Helper for setting up ranges
struct uinput_abs_setup abs_setup;
memset(&abs_setup, 0, sizeof(abs_setup));
#define SET_ABS(_code, _min, _max) do { \
memset(&abs_setup, 0, sizeof(abs_setup)); \
abs_setup.code = (_code); \
abs_setup.absinfo.minimum = (_min); \
abs_setup.absinfo.maximum = (_max); \
abs_setup.absinfo.fuzz = 0; \
abs_setup.absinfo.flat = 0; \
if(ioctl(fd, UI_ABS_SETUP, &abs_setup) < 0) { \
fprintf(stderr, "UI_ABS_SETUP code %d failed\n", (int)(_code)); \
} \
} while(0)
// left stick + right stick => [-32760..32760]
SET_ABS(ABS_X, AXIS_MIN, AXIS_MAX);
SET_ABS(ABS_Y, AXIS_MIN, AXIS_MAX);
SET_ABS(ABS_Z, AXIS_MIN, AXIS_MAX);
SET_ABS(ABS_RZ, AXIS_MIN, AXIS_MAX);
// d-pad => -1..+1
SET_ABS(ABS_HAT0X, -1, 1);
SET_ABS(ABS_HAT0Y, -1, 1);
// gas/brake => [0..255]
SET_ABS(ABS_BRAKE, 0, 255);
SET_ABS(ABS_GAS, 0, 255);
// finalize device
struct uinput_setup uset;
memset(&uset, 0, sizeof(uset));
uset.id.bustype = BUS_USB;
uset.id.vendor = VENDOR_ID;
uset.id.product = PRODUCT_ID;
uset.id.version = VERSION_ID;
strncpy(uset.name, DEVICE_NAME, UINPUT_MAX_NAME_SIZE);
if(ioctl(fd, UI_DEV_SETUP, &uset) < 0) {
fprintf(stderr, "UI_DEV_SETUP failed\n");
close(fd);
return -1;
}
if(ioctl(fd, UI_DEV_CREATE) < 0) {
fprintf(stderr, "UI_DEV_CREATE failed\n");
close(fd);
return -1;
}
printf("Created uinput device: %s\n", DEVICE_NAME);
return fd;
}
/* --------------------------------------------------------------------------
We'll only emit an EV_KEY or EV_ABS if the new value differs from old value
We'll handle the SYN manually when a change occurs.
This avoids spamming repeated events.
-------------------------------------------------------------------------- */
static void emit_key_if_changed(int code, int newVal, int oldVal)
{
if(newVal == oldVal) return; // no change
struct input_event ev;
memset(&ev, 0, sizeof(ev));
ev.type = EV_KEY;
ev.code = code;
ev.value = newVal;
write(g_uinput_fd, &ev, sizeof(ev));
// then a SYN
memset(&ev, 0, sizeof(ev));
ev.type = EV_SYN;
ev.code = SYN_REPORT;
ev.value = 0;
write(g_uinput_fd, &ev, sizeof(ev));
}
static void emit_abs_if_changed(int code, int newVal, int oldVal)
{
if(newVal == oldVal) return; // no change
struct input_event ev;
memset(&ev, 0, sizeof(ev));
ev.type = EV_ABS;
ev.code = code;
ev.value = newVal;
write(g_uinput_fd, &ev, sizeof(ev));
// SYN
memset(&ev, 0, sizeof(ev));
ev.type = EV_SYN;
ev.code = SYN_REPORT;
ev.value = 0;
write(g_uinput_fd, &ev, sizeof(ev));
}
/* --------------------------------------------------------------------------
clamp from ±900 => ±32760
-------------------------------------------------------------------------- */
static int clamp_scale(int raw)
{
if(raw < -RAW_MAX) raw = -RAW_MAX;
if(raw > RAW_MAX) raw = RAW_MAX;
// scale
float ratio = (float)AXIS_MAX / (float)RAW_MAX; // ~32.76
float valf = (float)raw * ratio;
int vali = (int)valf;
if(vali < AXIS_MIN) vali = AXIS_MIN;
if(vali > AXIS_MAX) vali = AXIS_MAX;
return vali;
}
/* --------------------------------------------------------------------------
radial deadzone
-------------------------------------------------------------------------- */
static void do_deadzone(int *px, int *py)
{
int x = *px;
int y = *py;
long long mag = (long long)x*x + (long long)y*y;
long long dz2 = (long long)g_deadzone * (long long)g_deadzone;
if(mag <= dz2) {
*px = 0;
*py = 0;
}
}
/* --------------------------------------------------------------------------
handle left bits:
B0 => L1 => BTN_TL
B1 => L2 => BTN_TL2 => also brake
B2 => DPad Up => hatY=-1
B3 => DPad Left => hatX=-1
B4 => DPad Right => hatX=+1
B5 => DPad Down => hatY=+1
B7 => Mode => BTN_MODE
-------------------------------------------------------------------------- */
static void handle_left_buttons(unsigned char newB)
{
// L1 => bit0
bool currL1 = (newB & (1<<0))?true:false;
bool oldL1 = (old_btn_left & (1<<0))?true:false;
if(currL1 != oldL1) {
// 1 => pressed, 0 => released
emit_key_if_changed(BTN_TL, currL1?1:0, oldL1?1:0);
}
// L2 => bit1 => also brake
bool currL2 = (newB & (1<<1))?true:false;
bool oldL2 = (old_btn_left & (1<<1))?true:false;
if(currL2 != oldL2) {
emit_key_if_changed(BTN_TL2, currL2?1:0, oldL2?1:0);
}
// also brake => 255 if pressed else 0
int newBrake = currL2?255:0;
emit_abs_if_changed(ABS_BRAKE, newBrake, last_brake);
last_brake = newBrake;
// Mode => bit7 => BTN_MODE
bool currMode = (newB & (1<<7))?true:false;
bool oldMode = (old_btn_left & (1<<7))?true:false;
if(currMode != oldMode) {
emit_key_if_changed(BTN_MODE, currMode?1:0, oldMode?1:0);
}
// DPad => bits2/3/4/5 => up/left/right/down
int hatx=0, haty=0;
if(newB & (1<<2)) { // up
haty = -1;
}
if(newB & (1<<5)) { // down
haty = 1;
}
if(newB & (1<<3)) { // left
hatx = -1;
}
if(newB & (1<<4)) { // right
hatx = 1;
}
emit_abs_if_changed(ABS_HAT0X, hatx, old_hat_x);
emit_abs_if_changed(ABS_HAT0Y, haty, old_hat_y);
old_hat_x = hatx;
old_hat_y = haty;
old_btn_left = newB;
}
/* --------------------------------------------------------------------------
handle right bits:
B0 => R1 => BTN_TR
B1 => R2 => BTN_TR2 => also gas
B2 => X => BTN_X
B3 => Y => BTN_Y
B4 => A => BTN_A
B5 => B => BTN_B
B6 => Select => BTN_SELECT
B7 => Start => BTN_START
-------------------------------------------------------------------------- */
static void handle_right_buttons(unsigned char newB)
{
// R1 => bit0
bool currR1 = (newB & (1<<0))?true:false;
bool oldR1 = (old_btn_right & (1<<0))?true:false;
if(currR1 != oldR1) {
emit_key_if_changed(BTN_TR, currR1?1:0, oldR1?1:0);
}
// R2 => bit1 => also gas
bool currR2 = (newB & (1<<1))?true:false;
bool oldR2 = (old_btn_right & (1<<1))?true:false;
if(currR2 != oldR2) {
emit_key_if_changed(BTN_TR2, currR2?1:0, oldR2?1:0);
}
// gas => 255 if pressed else 0
int newGas = currR2?255:0;
emit_abs_if_changed(ABS_GAS, newGas, last_gas);
last_gas = newGas;
// X => bit2 => BTN_X
bool curX = (newB & (1<<2))?true:false;
bool oldX = (old_btn_right & (1<<2))?true:false;
if(curX != oldX) {
emit_key_if_changed(BTN_X, curX?1:0, oldX?1:0);
}
// Y => bit3 => BTN_Y
bool curY = (newB & (1<<3))?true:false;
bool oldY = (old_btn_right & (1<<3))?true:false;
if(curY != oldY) {
emit_key_if_changed(BTN_Y, curY?1:0, oldY?1:0);
}
// A => bit4 => BTN_A
bool curA = (newB & (1<<4))?true:false;
bool oldA = (old_btn_right & (1<<4))?true:false;
if(curA != oldA) {
emit_key_if_changed(BTN_A, curA?1:0, oldA?1:0);
}
// B => bit5 => BTN_B
bool curB = (newB & (1<<5))?true:false;
bool oldB = (old_btn_right & (1<<5))?true:false;
if(curB != oldB) {
emit_key_if_changed(BTN_B, curB?1:0, oldB?1:0);
}
// Select => bit6 => BTN_SELECT
bool curSel = (newB & (1<<6))?true:false;
bool oldSel = (old_btn_right & (1<<6))?true:false;
if(curSel != oldSel) {
emit_key_if_changed(BTN_SELECT, curSel?1:0, oldSel?1:0);
}
// Start => bit7 => BTN_START
bool curSt = (newB & (1<<7))?true:false;
bool oldSt = (old_btn_right & (1<<7))?true:false;
if(curSt != oldSt) {
emit_key_if_changed(BTN_START, curSt?1:0, oldSt?1:0);
}
old_btn_right = newB;
}
/* --------------------------------------------------------------------------
We'll do 50 frames calibration:
sum raw x,y => offset_x,y
-------------------------------------------------------------------------- */
static void calibrate_port(int fd, CalData *cal)
{
printf("Calibrating port ... keep stick centered for 50 frames\n");
unsigned char tmp[8];
int frames=0;
while(frames < CAL_FRAMES) {
ssize_t n = read(fd, tmp, 8);
if(n<8) {
usleep(5000);
continue;
}
if(tmp[0]==MAGIC_START && tmp[7]==MAGIC_END) {
int16_t rx = (tmp[3]<<8) | tmp[4];
int16_t ry = (tmp[5]<<8) | tmp[6];
cal->sum_x += rx;
cal->sum_y += ry;
cal->count++;
frames++;
}
}
int ox = (int)(cal->sum_x / cal->count);
int oy = (int)(cal->sum_y / cal->count);
cal->offset_x = ox;
cal->offset_y = oy;
cal->done = true;
printf("cal done => offsetX=%d offsetY=%d\n", ox, oy);
}
/* --------------------------------------------------------------------------
left side thread => parse frames => handle buttons => handle axes => emit
We'll fix left-right swap by negating X
-------------------------------------------------------------------------- */
static void *thread_left(void *arg)
{
int fd = *(int*)arg;
calibrate_port(fd, &calLeft);
while(1) {
unsigned char buf[8];
ssize_t n = read(fd, buf, 8);
if(n<8) {
usleep(2000);
continue;
}
if(buf[0]==MAGIC_START && buf[7]==MAGIC_END) {
// parse
unsigned char b = buf[2];
handle_left_buttons(b);
int16_t rx = (buf[3]<<8)|buf[4];
int16_t ry = (buf[5]<<8)|buf[6];
// offset
int ax = rx - calLeft.offset_x;
int ay = ry - calLeft.offset_y;
// fix left-right => negate X
ax = -ax;
// clamp & scale
ax = clamp_scale(ax);
ay = clamp_scale(ay);
// deadzone
do_deadzone(&ax, &ay);
// invert Y => up negative
ay = -ay;
// only emit if changed
// store new => compare last_left_x, last_left_y
emit_abs_if_changed(ABS_X, ax, last_left_x);
emit_abs_if_changed(ABS_Y, ay, last_left_y);
last_left_x = ax;
last_left_y = ay;
}
}
return NULL;
}
/* --------------------------------------------------------------------------
right side thread => parse frames => handle buttons => handle axes => emit
also fix left-right => negate X
-------------------------------------------------------------------------- */
static void *thread_right(void *arg)
{
int fd = *(int*)arg;
calibrate_port(fd, &calRight);
while(1) {
unsigned char buf[8];
ssize_t n = read(fd, buf, 8);
if(n<8) {
usleep(2000);
continue;
}
if(buf[0]==MAGIC_START && buf[7]==MAGIC_END) {
unsigned char b = buf[2];
handle_right_buttons(b);
int16_t rx = (buf[3]<<8)|buf[4];
int16_t ry = (buf[5]<<8)|buf[6];
int ax = rx - calRight.offset_x;
int ay = ry - calRight.offset_y;
// also fix left-right => negate X
ax = -ax;
ax = clamp_scale(ax);
ay = clamp_scale(ay);
do_deadzone(&ax, &ay);
// invert y
ay = -ay;
// but now we map to ABS_Z, ABS_RZ
emit_abs_if_changed(ABS_Z, ax, last_right_x);
emit_abs_if_changed(ABS_RZ, ay, last_right_y);
last_right_x = ax;
last_right_y = ay;
}
}
return NULL;
}
/* --------------------------------------------------------------------------
Setup the serial device at 19200 8N1, raw
-------------------------------------------------------------------------- */
static int setup_serial(const char *dev)
{
int fd = open(dev, O_RDWR | O_NOCTTY | O_NDELAY);
if(fd<0) {
fprintf(stderr, "Cannot open %s: %s\n", dev, strerror(errno));
return -1;
}
fcntl(fd, F_SETFL, 0); // blocking
struct termios tty;
memset(&tty, 0, sizeof(tty));
if(tcgetattr(fd, &tty)<0) {
fprintf(stderr, "tcgetattr fail on %s\n", dev);
close(fd);
return -1;
}
cfsetispeed(&tty, B19200);
cfsetospeed(&tty, B19200);
tty.c_cflag = (tty.c_cflag & ~CSIZE)|CS8;
tty.c_cflag &= ~PARENB;
tty.c_cflag &= ~CSTOPB;
tty.c_cflag |= (CLOCAL|CREAD);
tty.c_cflag &= ~CRTSCTS;
tty.c_iflag &= ~(IXON|IXOFF|IXANY);
tty.c_lflag &= ~(ICANON|ECHO|ECHOE|ISIG);
tty.c_oflag &= ~OPOST;
tty.c_cc[VMIN] = 1;
tty.c_cc[VTIME] = 0;
tcflush(fd, TCIOFLUSH);
if(tcsetattr(fd, TCSANOW, &tty)!=0) {
fprintf(stderr, "tcsetattr fail on %s\n", dev);
close(fd);
return -1;
}
printf("Opened serial: %s\n", dev);
return fd;
}
/* --------------------------------------------------------------------------
main
-------------------------------------------------------------------------- */
int main(int argc, char** argv)
{
// *** IMPORTANT ***: Power on the joystick hardware
trimui_init_power_gpio();
// 1) setup uinput
g_uinput_fd = setup_uinput();
if(g_uinput_fd < 0) {
return 1;
}
// 2) open left
int fdL = setup_serial(SERIAL_LEFT);
if(fdL<0) {
ioctl(g_uinput_fd, UI_DEV_DESTROY);
close(g_uinput_fd);
return 1;
}
// 3) open right
int fdR = setup_serial(SERIAL_RIGHT);
if(fdR<0) {
close(fdL);
ioctl(g_uinput_fd, UI_DEV_DESTROY);
close(g_uinput_fd);
return 1;
}
// 4) spawn threads
pthread_t tidL, tidR;
pthread_create(&tidL, NULL, thread_left, &fdL);
pthread_create(&tidR, NULL, thread_right, &fdR);
// 5) just loop forever
while(1) {
sleep(1);
}
// not reached
close(fdL);
close(fdR);
ioctl(g_uinput_fd, UI_DEV_DESTROY);
close(g_uinput_fd);
return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment