t1d-probe.py
Files / gamesir-t1d / t1d-probe.py
Files in gamesir-t1d/
Directorygamesir-t1d/
- 60-t1d-bridge-uinput.rules 350 bytes
- LICENSE 1.0 KB
- t1d-bridge.py 24.1 KB
- t1d-probe.py 3.8 KB
From Dictation on Linux with Handy, and tidy text with OpenCode Go and Use the Tello's GameSir T1d controller as a Linux gamepad. MIT licence: LICENSE.
#!/usr/bin/env python3# SPDX-License-Identifier: MIT# Copyright (c) 2026 jacq.cc"""Probe a GameSir T1d over BLE: list its GATT services, read the DeviceInformation service, then print decoded input for a while.
Setup (Kubuntu 26.04, once): python3 -m venv ~/.venvs/t1d && ~/.venvs/t1d/bin/pip install bleakRun (controller on, LEDs blinking = advertising): ~/.venvs/t1d/bin/python gamesir-ctrl/t1d-probe.py [seconds]
No pairing is needed: the T1d accepts a plain BLE connection."""import asyncioimport sysimport time
from bleak import BleakClient, BleakScanner
NAME_PREFIX = "Gamesir-T1d"INPUT_CHAR = "00008651-0000-1000-8000-00805f9b34fb" # 20-byte input reports (notify)DIS = { # Device Information service "2a24": "model", "2a25": "serial", "2a26": "firmware", "2a27": "hardware", "2a50": "pnp_id", "2a00": "name", "2a04": "conn_params",}AXES = ("lx", "ly", "rx", "ry", "l2", "r2")
def decode(d: bytes) -> dict: """Byte layout from dg-hub/gamesir-t1d-driver and the ElishaAz gist. Sticks are 10-bit (0..1023), triggers 8-bit (0..255).""" return { "type": d[0:2].hex(), "lx": (d[2] << 2) | (d[3] >> 6), "ly": ((d[3] & 0x3F) << 4) | (d[4] >> 4), "rx": ((d[4] & 0x0F) << 6) | (d[5] >> 2), "ry": ((d[5] & 0x03) << 8) | d[6], "l2": d[7], "r2": d[8], "keys": f"{d[9]:08b}", # A/B/X/Y/L1/R1 bits "k2": f"{d[10]:08b}", # C1/C2, L2/R2 digital, L3/R3 bits "dpad": d[11], # 1 up, 3 right, 5 down, 7 left (diagonals in between) }
def changed(a: dict, b: dict) -> bool: if a["type"] != b["type"] or a["keys"] != b["keys"] or a["k2"] != b["k2"] or a["dpad"] != b["dpad"]: return True return sum(abs(a[k] - b[k]) for k in AXES) > 12 # ignore pot jitter
async def main(seconds: float) -> int: print("scanning for", NAME_PREFIX, "...") dev = await BleakScanner.find_device_by_filter( lambda d, _ad: (d.name or "").startswith(NAME_PREFIX), timeout=10) if dev is None: print("not found: is the controller on and blinking?") return 1 print("found", dev.name, dev.address)
async with BleakClient(dev, timeout=20) as client: for svc in client.services: print("service", svc.uuid) for ch in svc.characteristics: print(" char", ch.uuid, ",".join(ch.properties)) for short, label in DIS.items(): try: raw = await client.read_gatt_char(f"0000{short}-0000-1000-8000-00805f9b34fb") except Exception as exc: # noqa: BLE001 print(f"{label:12s} error: {exc}") continue text = raw.decode(errors="replace").rstrip("\x00") if short not in ("2a50", "2a04") else "" print(f"{label:12s} {raw.hex():34s} {text}")
count = 0 prev = None stamps: list[float] = [] t0 = time.monotonic()
def on_notify(_handle, data: bytearray) -> None: nonlocal count, prev count += 1 stamps.append(time.monotonic()) cur = decode(bytes(data)) if prev is None or changed(prev, cur): print(f"{time.monotonic() - t0:6.2f}s {bytes(data).hex()} {cur}") prev = cur
await client.start_notify(INPUT_CHAR, on_notify) print(f"listening {seconds:.0f} s: move the sticks, press buttons") await asyncio.sleep(seconds) await client.stop_notify(INPUT_CHAR)
gaps = sorted(b - a for a, b in zip(stamps, stamps[1:])) if gaps: print(f"{count} notifications; shortest gap {gaps[0] * 1000:.0f} ms, median {gaps[len(gaps) // 2] * 1000:.0f} ms") return 0
if __name__ == "__main__": sys.exit(asyncio.run(main(float(sys.argv[1]) if len(sys.argv) > 1 else 15.0)))Text, photos and drawings: CC BY 4.0. Code: MIT. Share and adapt them, crediting HomeLab Guides (blog.jacq.cc). Licence and privacy