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, thendx_final = clamp_scale(dx).clamp_scale: clampsdxto 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 loweringg_deadzonenever 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 32760 → 32759, 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.