Full walkthrough — how I fixed the analog stick asymmetry on my TrimUI Smart Pro (muOS)

1. Access and backup

# I got in via the ttyd web terminal (http://<my-device-ip>:8080/) — SSH root@<ip>, password root (default)
cp /usr/bin/trimui_inputd_smart_pro /root/trimui_inputd_smart_pro.backup
md5sum /root/trimui_inputd_smart_pro.backup   # noted this hash as my "original" reference

2. I diagnosed before touching anything

I didn’t assume it was g_deadzone right away — I went and measured the raw axis first:

which evtest   # already installed on my muOS build
timeout 3 evtest /dev/input/eventN </dev/null   # found the right N via: cat /proc/bus/input/devices

evtest’s initial dump already showed me the real axis names (ABS_X, ABS_Y, etc.) and the min/max/fuzz/flat declared by the driver.

To read the axis’s current absolute value without depending on change events (more reliable than just watching the stream — if the stick is already parked at an extreme, no new event fires), I used the EVIOCGABS ioctl:

# get_absinfo.py — ran it on the device: python3 get_absinfo.py <code> [<code> ...]
# code 0 = ABS_X, 1 = ABS_Y (confirmed the right codes for my device via evtest)
import fcntl, struct, sys
path = '/dev/input/eventN'  # adjusted N
codes = [int(c) for c in sys.argv[1:]] or [0, 1]
EVIOCGABS_BASE = (2 << 30) | (24 << 16) | (ord('E') << 8)
with open(path, 'rb') as f:
    fd = f.fileno()
    for code in codes:
        req = EVIOCGABS_BASE | (0x40 + code)
        buf = bytearray(24)
        fcntl.ioctl(fd, req, buf)
        value, minimum, maximum, fuzz, flat, resolution = struct.unpack('<6i', buf)
        print(f'code={code}: value={value} min={minimum} max={maximum} fuzz={fuzz} flat={flat}')

I held the stick at each extreme (left/right/up/down) and ran the script for each position. One side hit the declared max/min and the other didn’t — range asymmetry, not deadzone.

3. I listed the binary’s symbols (possible because it’s not stripped)

nm /usr/bin/trimui_inputd_smart_pro | sort
# also pulled it to my PC to analyze at leisure:
scp root@<ip>:/usr/bin/trimui_inputd_smart_pro ./trimui_inputd_orig

Functions showed up with clear names: do_deadzone, clamp_scale, calibrate_port, thread_left, thread_right, and the constant RAW_MAX.

4. I disassembled the binary (aarch64 — my x86 PC’s objdump couldn’t disassemble it; I used capstone via Python instead)

uv init disasm && cd disasm && uv add capstone
# disasm.py — disassembles a function given its address/size (got these via `readelf -s`)
from capstone import CS_ARCH_ARM64, CS_MODE_ARM, Cs
with open('trimui_inputd_orig', 'rb') as f:
    data = f.read()
TEXT_VADDR, TEXT_OFFSET = 0x400e80, 0xe80   # confirmed via: readelf -S <binary> | grep -A1 .text
addr, size = 0x4018e0, 184   # function address/size (via readelf -s)
foff = addr - TEXT_VADDR + TEXT_OFFSET
code = data[foff:foff+size]
md = Cs(CS_ARCH_ARM64, CS_MODE_ARM)
for insn in md.disasm(code, addr):
    print(f'0x{insn.address:x}: {insn.mnemonic}\t{insn.op_str}')

5. What I found (reconstructed logic)

  • calibrate_port: reads 50 samples from the serial line at rest, averages them, and stores that as the stick’s “center” (calLeft.avgX/avgY). It never measures the actual range per direction.
  • thread_left: dx = rawX - calLeft.avgX, then dx_final = clamp_scale(dx).
  • clamp_scale: clamps dx to a fixed ±900 (mov w0, #0x384, appears 5 times in the function), converts to float, multiplies by (32767.998 / 900), converts back to int, and clamps the final output to ±32760.
  • do_deadzone: runs after the scaling, on the final ±32767 range — which is why lowering g_deadzone never fixed anything for me; it was never the limiting factor.

In other words: the code assumes ±900 symmetry in the raw delta, but my stick’s real electrical range isn’t symmetric.

6. The patch I applied

I don’t have the source code or a compiler toolchain for the device, so I couldn’t do the “proper” fix (calibrating an asymmetric min/max per direction). I went with the pragmatic fix instead: lower the fixed threshold from 900 to a value that fits within the real range of the shorter side.

The 5 mov w0, #0x384 instructions (900 = 0x384) became mov w0, #0x1f4 (500 = 0x1f4), at file offsets 0x18e8, 0x18fc, 0x1908, 0x1918, 0x1920 in the binary.

The 32-bit ARM64 MOVZ encoding is mechanical — for any new value V (0–65535):

word = 0x52800000 | (V << 5)

For V=500: word = 0x52803e80 → little-endian bytes 80 3e 80 52.

I validated the encoding with capstone before applying it (that’s how I caught an arithmetic mistake I’d made by hand, before ever touching the device):

import struct
from capstone import CS_ARCH_ARM64, CS_MODE_ARM, Cs
V = 500
raw = struct.pack('<I', 0x52800000 | (V << 5))
md = Cs(CS_ARCH_ARM64, CS_MODE_ARM)
for insn in md.disasm(raw, 0x4018e8):
    print(insn.mnemonic, insn.op_str)   # printed: mov w0, #0x1f4

File offsets (found via capstone, specific to my build/MD5 855e7d25e20942edb6be633455829f94):
0x18e8, 0x18fc, 0x1908, 0x1918, 0x1920 — all originally 80 70 80 52, changed to 80 3e 80 52.

7. I applied it on the device, carefully

# 1. confirmed a single process and killed it
ps -o pid,ppid,args | grep trimui_inputd
kill -9 <PID>
ps -o pid,ppid,args | grep trimui_inputd   # came back empty

# 2. patched it (python right on the device)
python3 -c "
path = '/usr/bin/trimui_inputd_smart_pro'
offsets = [0x18e8, 0x18fc, 0x1908, 0x1918, 0x1920]
old, new = bytes.fromhex('80708052'), bytes.fromhex('803e8052')
with open(path, 'r+b') as f:
    for off in offsets:
        f.seek(off); assert f.read(4) == old
    for off in offsets:
        f.seek(off); f.write(new)
    for off in offsets:
        f.seek(off); assert f.read(4) == new
print('ok')
"

# 3. restarted it and confirmed a single instance
nohup /usr/bin/trimui_inputd_smart_pro >/dev/null 2>&1 &
sleep 2
ps -o pid,ppid,args | grep trimui_inputd   # only 1 PID

8. I validated it without even opening a game

I repeated step 2 (EVIOCGABS) while holding the extremes. In my case: right went from 3276032759, left went from -19800-32759. Symmetric now.

Finally, I did a full reboot on the device to make sure everything settled cleanly, confirmed the patch survived it (the 5 offsets still had the new bytes), and tested it in an actual game — fixed.


If you’re going to reproduce this: my offsets only apply to this exact muOS build (MD5 above). On a different version they’ll very likely be different — you’ll need to relocate them via objdump/nm first. And always back up the original binary before touching anything.

Okay well this is… a very interesting write up.

However the concerns that you have found are going to be completely moot by the fact that input is completely changed for Andromeda.

Additionally if the specific AI tool that was used to investigate this and wrote the report glaringly missed the fact that we have a CLI joystick calibration tool available in /opt/muos/bin which can alleviate sticks in all sorts of conditions.

Plus your other issue that you had apparently raised is the fact that dpad swap never occurs on the smart pro because… well, it doesn’t need to swap. That is only used on devices that do not have sticks.