Read the pad, detect unknown ones, layout editor
added a feature that now can read the inputs off the board, meaning if you already have saved settings and you uninstall / reinstall the app or use it on a new device it can auto detect what each key has saved. using the above feature i now also do believe it is possible to auto detect the macros pads rough layout / how many keys and dials it has!. hopfully this means that more people can use the app easily without needing to hard code a bunch of new layouts. its not truly universal but it should open the door to much more pads working out of the box right now! and lastly in case the extra layouts do work i made a little layout editor so you can the app to look like your actual macropad!
This commit is contained in:
184
macropad-read.py
Normal file
184
macropad-read.py
Normal file
@@ -0,0 +1,184 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
macropad-read - find out what the pad will tell us about itself.
|
||||
|
||||
Two ways of listening, neither of which writes a configuration:
|
||||
|
||||
1. Feature reports. A HID "get feature" is a read. We ask for each
|
||||
report id in turn and print anything that comes back.
|
||||
|
||||
2. Passive listening. Several programs can have a hidraw device open at
|
||||
once, and input reports go to all of them. So we can sit and watch
|
||||
while the vendor software talks to the pad, and see the pad's
|
||||
replies, with no kernel module and no root.
|
||||
|
||||
Neither of these changes your keybinds.
|
||||
|
||||
python3 macropad-read.py # find the pad, do both
|
||||
python3 macropad-read.py /dev/hidraw10 # a specific node
|
||||
python3 macropad-read.py --listen 60 # listen for 60 seconds
|
||||
|
||||
To catch the pad answering the vendor software:
|
||||
|
||||
1. Start: python3 macropad-read.py --listen 60
|
||||
2. In another window: wine MINI_KEYBOARD.exe
|
||||
3. Press whatever it calls "view settings" or "read".
|
||||
4. Come back here. Anything the pad said is printed and saved.
|
||||
|
||||
It writes macropad-read.txt in the same format as macropad-capture.py,
|
||||
so the same tools can read it.
|
||||
"""
|
||||
|
||||
import fcntl
|
||||
import os
|
||||
import select
|
||||
import sys
|
||||
import time
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import macropad as proto # noqa: E402
|
||||
|
||||
OUTFILE = "macropad-read.txt"
|
||||
REPORT_IDS = (0, 1, 2, 3, 4)
|
||||
LEN = 65
|
||||
|
||||
G = "\033[32m"; R = "\033[31m"; Y = "\033[33m"; B = "\033[1m"
|
||||
DIM = "\033[2m"; O = "\033[0m"
|
||||
|
||||
|
||||
def hidiocgfeature(size):
|
||||
"""_IOC(READ|WRITE, 'H', 0x07, size) - hidraw's GET_FEATURE ioctl."""
|
||||
return (3 << 30) | (size << 16) | (ord("H") << 8) | 0x07
|
||||
|
||||
|
||||
def pretty(data, width=16):
|
||||
lines = []
|
||||
for off in range(0, len(data), width):
|
||||
chunk = data[off:off + width]
|
||||
hexs = " ".join(f"{b:02x}" for b in chunk)
|
||||
text = "".join(chr(b) if 32 <= b < 127 else "." for b in chunk)
|
||||
lines.append(f" {off:04x} {hexs:<{width * 3}} {text}")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def try_features(path, out):
|
||||
"""Ask the pad for each report id. Reading can't change anything."""
|
||||
print(f"{B}Feature reports{O} {DIM}asking the pad for each report id{O}")
|
||||
found = 0
|
||||
for rid in REPORT_IDS:
|
||||
buf = bytearray(LEN)
|
||||
buf[0] = rid
|
||||
try:
|
||||
fd = os.open(path, os.O_RDWR)
|
||||
except OSError as e:
|
||||
print(f" {R}can't open {path}: {e.strerror}{O}")
|
||||
return 0
|
||||
try:
|
||||
fcntl.ioctl(fd, hidiocgfeature(len(buf)), buf, True)
|
||||
except OSError as e:
|
||||
print(f" id {rid}: {DIM}nothing ({e.strerror}){O}")
|
||||
continue
|
||||
finally:
|
||||
os.close(fd)
|
||||
|
||||
if not any(buf[1:]):
|
||||
print(f" id {rid}: {DIM}all zeros{O}")
|
||||
continue
|
||||
found += 1
|
||||
print(f" id {rid}: {G}answered{O}")
|
||||
print(pretty(bytes(buf)))
|
||||
out.write(f"[feature] report id {rid}\n {bytes(buf).hex()}\n")
|
||||
return found
|
||||
|
||||
|
||||
def listen(path, seconds, out):
|
||||
print(f"\n{B}Listening for {seconds}s{O} "
|
||||
f"{DIM}anything the pad sends shows up here{O}")
|
||||
print(f"{DIM}Now use the vendor software and press its read or view "
|
||||
f"settings button.{O}\n")
|
||||
try:
|
||||
fd = os.open(path, os.O_RDONLY | os.O_NONBLOCK)
|
||||
except OSError as e:
|
||||
print(f"{R}Can't open {path}: {e.strerror}{O}")
|
||||
return 0
|
||||
|
||||
seen = 0
|
||||
end = time.time() + seconds
|
||||
try:
|
||||
while time.time() < end:
|
||||
ready, _, _ = select.select([fd], [], [], 0.5)
|
||||
if not ready:
|
||||
continue
|
||||
try:
|
||||
data = os.read(fd, 128)
|
||||
except BlockingIOError:
|
||||
continue
|
||||
except OSError as e:
|
||||
print(f"{R}read failed: {e.strerror}{O}")
|
||||
break
|
||||
if not data:
|
||||
continue
|
||||
seen += 1
|
||||
left = int(end - time.time())
|
||||
print(f"{G}<-{O} {len(data)} bytes {DIM}({left}s left){O}")
|
||||
print(pretty(data))
|
||||
out.write(f"[{seen}] interrupt in\n {data.hex()}\n")
|
||||
out.flush()
|
||||
except KeyboardInterrupt:
|
||||
print(f"\n{DIM}stopped early{O}")
|
||||
finally:
|
||||
os.close(fd)
|
||||
return seen
|
||||
|
||||
|
||||
def main():
|
||||
args = [a for a in sys.argv[1:] if not a.startswith("--")]
|
||||
seconds = 30
|
||||
if "--listen" in sys.argv:
|
||||
i = sys.argv.index("--listen")
|
||||
if i + 1 < len(sys.argv) and sys.argv[i + 1].isdigit():
|
||||
seconds = int(sys.argv[i + 1])
|
||||
|
||||
if args:
|
||||
path = args[0]
|
||||
else:
|
||||
pads = proto.find_devices()
|
||||
if not pads:
|
||||
print(f"{R}No pad found.{O} Run macropad-probe.py to see what's there,")
|
||||
print("or pass the node yourself: python3 macropad-read.py /dev/hidrawN")
|
||||
return 1
|
||||
if len(pads) > 1:
|
||||
print(f"{B}Several candidates:{O}")
|
||||
for p in pads:
|
||||
print(f" {p['path']} {p['vid']}:{p['pid']}")
|
||||
print("\nPick one and pass it as an argument.")
|
||||
return 1
|
||||
path = pads[0]["path"]
|
||||
|
||||
if not os.access(path, os.R_OK):
|
||||
print(f"{R}{path} isn't readable.{O} Add the udev rule, or use sudo.")
|
||||
return 1
|
||||
|
||||
print(f"{B}Reading from {path}{O}")
|
||||
print(f"{DIM}Nothing here writes a configuration.{O}\n")
|
||||
|
||||
with open(OUTFILE, "w") as out:
|
||||
out.write(f"# read attempt on {path}\n\n")
|
||||
found = try_features(path, out)
|
||||
heard = listen(path, seconds, out)
|
||||
|
||||
print("\n" + "-" * 62)
|
||||
if found or heard:
|
||||
print(f"{G}Got something.{O} "
|
||||
f"{found} feature report(s), {heard} message(s) while listening.")
|
||||
print(f"Saved to {B}{OUTFILE}{O} - send that over.")
|
||||
else:
|
||||
print(f"{Y}The pad said nothing.{O}")
|
||||
print("That doesn't prove it can't be read, only that it wasn't")
|
||||
print("answering us. The next thing to try is capturing the vendor")
|
||||
print("software with macropad-capture.py.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user