Add files via upload
This commit is contained in:
152
macropad-handshake.py
Normal file
152
macropad-handshake.py
Normal file
@@ -0,0 +1,152 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
macropad-handshake - find out how your pad wants to be talked to.
|
||||
|
||||
Sends only the benign "version check" report upstream uses on connect:
|
||||
all zeros, no configuration payload. It cannot change your keybinds.
|
||||
|
||||
It tries each report id with two different transfer methods, and checks
|
||||
after every attempt whether the device survived. If the pad resets, the
|
||||
script waits for it to come back before continuing.
|
||||
|
||||
Usage:
|
||||
python3 macropad-handshake.py /dev/hidraw10
|
||||
"""
|
||||
|
||||
import fcntl
|
||||
import glob
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
REPORT_IDS = [0, 2, 3]
|
||||
PAYLOAD_LEN = 64
|
||||
|
||||
G = "\033[32m"; R = "\033[31m"; Y = "\033[33m"; B = "\033[1m"; DIM = "\033[2m"; O = "\033[0m"
|
||||
|
||||
|
||||
def hidioc_sfeature(size):
|
||||
"""_IOWR('H', 0x06, size) - the SET_FEATURE ioctl hidraw exposes."""
|
||||
return (3 << 30) | (size << 16) | (ord('H') << 8) | 0x06
|
||||
|
||||
|
||||
def device_present(path):
|
||||
return os.path.exists(path)
|
||||
|
||||
|
||||
def wait_for_device(path, timeout=6.0):
|
||||
"""After a reset the node usually reappears with the same name."""
|
||||
start = time.time()
|
||||
while time.time() - start < timeout:
|
||||
if os.path.exists(path):
|
||||
time.sleep(0.4) # let udev reapply the ACL
|
||||
return True
|
||||
time.sleep(0.2)
|
||||
return False
|
||||
|
||||
|
||||
def try_write(path, report_id, method):
|
||||
"""
|
||||
Returns (ok, detail). 'ok' means the device accepted the transfer
|
||||
without erroring and was still present afterwards.
|
||||
"""
|
||||
data = bytes([report_id]) + bytes(PAYLOAD_LEN)
|
||||
|
||||
try:
|
||||
if method == "write":
|
||||
fd = os.open(path, os.O_WRONLY)
|
||||
try:
|
||||
os.write(fd, data)
|
||||
finally:
|
||||
os.close(fd)
|
||||
else: # feature report via ioctl
|
||||
fd = os.open(path, os.O_RDWR)
|
||||
try:
|
||||
buf = bytearray(data)
|
||||
fcntl.ioctl(fd, hidioc_sfeature(len(buf)), buf)
|
||||
finally:
|
||||
os.close(fd)
|
||||
except OSError as e:
|
||||
return False, f"{e.__class__.__name__}: {e.strerror} (errno {e.errno})"
|
||||
except Exception as e:
|
||||
return False, str(e)
|
||||
|
||||
time.sleep(0.3)
|
||||
if not device_present(path):
|
||||
return False, "device vanished - firmware reset"
|
||||
|
||||
return True, "accepted"
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) > 1:
|
||||
path = sys.argv[1]
|
||||
else:
|
||||
cands = sorted(glob.glob("/dev/hidraw*"))
|
||||
print("Pass the device path, e.g.:")
|
||||
print(f" python3 {sys.argv[0]} /dev/hidraw10")
|
||||
print(f"\n{DIM}Available: {' '.join(cands)}{O}")
|
||||
return 1
|
||||
|
||||
if not os.path.exists(path):
|
||||
print(f"{R}{path} doesn't exist.{O}")
|
||||
return 1
|
||||
if not os.access(path, os.W_OK):
|
||||
print(f"{R}{path} isn't writable.{O} Check your udev rule.")
|
||||
return 1
|
||||
|
||||
print(f"{B}Handshake probe on {path}{O}")
|
||||
print(f"{DIM}Sending zero-filled version-check reports only.")
|
||||
print(f"Your saved keybinds are not touched.{O}\n")
|
||||
|
||||
results = []
|
||||
|
||||
for method in ("write", "feature"):
|
||||
label = "interrupt OUT (write)" if method == "write" else "SET_FEATURE (ioctl)"
|
||||
print(f"{B}{label}{O}")
|
||||
|
||||
for rid in REPORT_IDS:
|
||||
if not device_present(path):
|
||||
print(f" {DIM}waiting for device to come back...{O}")
|
||||
if not wait_for_device(path):
|
||||
print(f" {R}device did not return. Replug it and rerun.{O}")
|
||||
return 1
|
||||
|
||||
ok, detail = try_write(path, rid, method)
|
||||
mark = f"{G}OK {O}" if ok else f"{R}FAIL {O}"
|
||||
print(f" report id {rid}: {mark} {detail}")
|
||||
results.append((method, rid, ok, detail))
|
||||
|
||||
if not ok and "vanished" in detail:
|
||||
if not wait_for_device(path):
|
||||
print(f" {R}device did not come back.{O} Replug and rerun.")
|
||||
return 1
|
||||
print()
|
||||
|
||||
# ------------------------------------------------------------ verdict
|
||||
print(f"{B}Verdict{O}")
|
||||
good = [(m, r) for m, r, ok, _ in results if ok]
|
||||
|
||||
if not good:
|
||||
print(f" {R}Nothing was accepted.{O}")
|
||||
print(" This pad may not use the MacroPad protocol at all, or it")
|
||||
print(" wants a vendor-specific unlock sequence first.")
|
||||
print("\n Worth trying: the other config interface, if the probe found one.")
|
||||
return 1
|
||||
|
||||
print(f" {G}Accepted combinations:{O}")
|
||||
for m, r in good:
|
||||
how = "write" if m == "write" else "feature"
|
||||
print(f" report id {r} via {how}")
|
||||
|
||||
m, r = good[0]
|
||||
print(f"\n Upstream picks the first report id that accepts a write,")
|
||||
print(f" so your pad looks like {B}protocol version {r}{O}.")
|
||||
print(f"\n Try a real binding with:")
|
||||
extra = "" if m == "write" else " (needs --feature support adding)"
|
||||
print(f" python3 macropad.py set key1 ctrl+c --report-id {r}{extra}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user