Поддержка MacOS (проверено в Tahoe)
This commit is contained in:
Executable
+87
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Конвертер дампа USBPcap (pcapng) → лог control-трансферов SET/GET_REPORT.
|
||||
|
||||
Использование: ./analyze_pcap.py <capture.pcapng> [out.txt]
|
||||
"""
|
||||
import struct
|
||||
import sys
|
||||
|
||||
# тип блока Enhanced Packet в pcapng
|
||||
BLOCK_ENHANCED_PACKET = 6
|
||||
# тип transfer'а control в USBPcap
|
||||
XFER_CONTROL = 2
|
||||
# биты направления в endpoint
|
||||
EP_DIR_IN = 0x80 # device → host; иначе host → device
|
||||
|
||||
|
||||
def read_packet_blocks(data: bytes) -> list[bytes]:
|
||||
"""Извлечь данные всех Enhanced Packet-блоков из pcapng-файла."""
|
||||
records = []
|
||||
off = 0
|
||||
while off + 12 <= len(data):
|
||||
block_type, block_len = struct.unpack_from("<II", data, off)
|
||||
if block_len < 12 or off + block_len > len(data):
|
||||
break
|
||||
if block_type == BLOCK_ENHANCED_PACKET:
|
||||
caplen = struct.unpack_from("<I", data, off + 20)[0]
|
||||
records.append(data[off + 28:off + 28 + caplen])
|
||||
off += block_len
|
||||
return records
|
||||
|
||||
|
||||
def parse_usb_header(packet: bytes):
|
||||
"""Разобрать заголовок USBPcap: → (irpid, endpoint, transfer, payload)."""
|
||||
header_len = struct.unpack_from("<H", packet, 0)[0]
|
||||
irpid = struct.unpack_from("<Q", packet, 2)[0]
|
||||
endpoint = packet[21]
|
||||
transfer = packet[22]
|
||||
data_len = struct.unpack_from("<I", packet, 23)[0]
|
||||
payload = packet[header_len:header_len + data_len]
|
||||
return irpid, endpoint, transfer, payload
|
||||
|
||||
|
||||
def parse_events(records: list[bytes]) -> list[tuple]:
|
||||
"""Собрать события SET/GET_REPORT, склеивая setup+data GET-ов по irpid."""
|
||||
events = []
|
||||
pending_get = {} # irpid → (iface, request_type, report_id)
|
||||
for packet in records:
|
||||
irpid, endpoint, transfer, data = parse_usb_header(packet)
|
||||
if transfer != XFER_CONTROL or not data:
|
||||
continue
|
||||
if (not endpoint & EP_DIR_IN and len(data) >= 8
|
||||
and data[0] == 0x21 and data[1] == 0x09):
|
||||
# merged SET_REPORT: setup(8 байт) + payload
|
||||
wValue = struct.unpack("<H", data[2:4])[0]
|
||||
wIndex = struct.unpack("<H", data[4:6])[0]
|
||||
events.append(("SET", wIndex, wValue >> 8, wValue & 0xFF, data[8:]))
|
||||
elif (endpoint & EP_DIR_IN and len(data) == 8
|
||||
and data[0] == 0xA1 and data[1] == 0x01):
|
||||
# setup-часть GET_REPORT: данные придут отдельным пакетом с тем же irpid
|
||||
wValue = struct.unpack("<H", data[2:4])[0]
|
||||
wIndex = struct.unpack("<H", data[4:6])[0]
|
||||
pending_get[irpid] = (wIndex, wValue >> 8, wValue & 0xFF)
|
||||
elif endpoint & EP_DIR_IN and irpid in pending_get:
|
||||
iface, request_type, report_id = pending_get.pop(irpid)
|
||||
events.append(("GET", iface, request_type, report_id, data))
|
||||
return events
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
raise SystemExit(__doc__)
|
||||
src = sys.argv[1]
|
||||
dst = sys.argv[2] if len(sys.argv) > 2 else "/tmp/tx_log.txt"
|
||||
|
||||
with open(src, "rb") as f:
|
||||
data = f.read()
|
||||
events = parse_events(read_packet_blocks(data))
|
||||
|
||||
with open(dst, "w") as f:
|
||||
for num, (kind, iface, rtype, rid, payload) in enumerate(events, 1):
|
||||
f.write(f"{kind} #{num:4d} if={iface} t={rtype} rid={rid:02x} "
|
||||
f"len={len(payload)}: {payload.hex(' ')}\n")
|
||||
print("events:", len(events))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Диагностика открытия HID-устройства через IOKit напрямую (ctypes).
|
||||
|
||||
Берёт путь устройства из hidapi (DevSrvsID:<entry_id> = registry entry ID),
|
||||
открывает сервис через IORegistryEntryIDMatching + IOHIDDeviceCreate (как
|
||||
делает сам hidapi) и печатает точный код IOReturn от IOHIDDeviceOpen:
|
||||
- 0x00000000 — успех (права есть, проблема в hidapi)
|
||||
- 0xE00002C1 — kIOReturnNotPrivileged (нет разрешения Input Monitoring)
|
||||
- другой код — иная причина (см. коды IOKit, iokit_common_err).
|
||||
"""
|
||||
import ctypes
|
||||
import ctypes.util
|
||||
import re
|
||||
import sys
|
||||
|
||||
import hid
|
||||
|
||||
iokit = ctypes.CDLL(ctypes.util.find_library("IOKit"))
|
||||
cf = ctypes.CDLL(ctypes.util.find_library("CoreFoundation"))
|
||||
|
||||
# прототипы функций
|
||||
iokit.IOServiceGetMatchingService.argtypes = [ctypes.c_int, ctypes.c_void_p]
|
||||
iokit.IOServiceGetMatchingService.restype = ctypes.c_uint
|
||||
iokit.IORegistryEntryIDMatching.argtypes = [ctypes.c_longlong]
|
||||
iokit.IORegistryEntryIDMatching.restype = ctypes.c_void_p
|
||||
iokit.IOObjectRelease.argtypes = [ctypes.c_uint]
|
||||
iokit.IOObjectRelease.restype = ctypes.c_int
|
||||
iokit.IOHIDDeviceCreate.argtypes = [ctypes.c_void_p, ctypes.c_uint]
|
||||
iokit.IOHIDDeviceCreate.restype = ctypes.c_void_p
|
||||
iokit.IOHIDDeviceOpen.argtypes = [ctypes.c_void_p, ctypes.c_int]
|
||||
iokit.IOHIDDeviceOpen.restype = ctypes.c_int
|
||||
iokit.IOHIDDeviceClose.argtypes = [ctypes.c_void_p, ctypes.c_int]
|
||||
iokit.IOHIDDeviceClose.restype = ctypes.c_int
|
||||
cf.CFRelease.argtypes = [ctypes.c_void_p]
|
||||
|
||||
|
||||
def main():
|
||||
"""Найти iface0 клавиатуры через hidapi и открыть её сервис в IOKit."""
|
||||
paths = [i["path"].decode() for i in hid.enumerate(0x0C45, 0x8006)
|
||||
if i["interface_number"] == 0]
|
||||
if not paths:
|
||||
raise SystemExit("устройство 0C45:8006 не найдено через hidapi")
|
||||
m = re.fullmatch(r"DevSrvsID:(\d+)", paths[0])
|
||||
if not m:
|
||||
raise SystemExit(f"неожиданный формат пути: {paths[0]!r}")
|
||||
entry_id = int(m.group(1))
|
||||
|
||||
service = iokit.IOServiceGetMatchingService(
|
||||
0, iokit.IORegistryEntryIDMatching(entry_id))
|
||||
if not service:
|
||||
raise SystemExit(f"сервис с entry_id {entry_id} не найден в реестре")
|
||||
print(f"сервис найден: {service:#x}")
|
||||
|
||||
dev = iokit.IOHIDDeviceCreate(None, service)
|
||||
iokit.IOObjectRelease(service)
|
||||
if not dev:
|
||||
raise SystemExit("IOHIDDeviceCreate вернул NULL")
|
||||
print("IOHIDDeviceCreate: ok")
|
||||
|
||||
r = iokit.IOHIDDeviceOpen(dev, 0) # kIOHIDOptionsTypeNone
|
||||
meaning = {0x00000000: "OK (права есть)",
|
||||
0xE00002C1: "NotPrivileged — нет разрешения Input Monitoring",
|
||||
}.get(r, "см. коды IOKit (iokit_common_err)")
|
||||
print(f"IOHIDDeviceOpen: {r:#010x} ({meaning})")
|
||||
if r == 0:
|
||||
iokit.IOHIDDeviceClose(dev, 0)
|
||||
cf.CFRelease(dev)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+163
@@ -0,0 +1,163 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Карта «LED-индекс → физическая клавиша» для GANSS ARDOR_Katana (0c45:8006).
|
||||
|
||||
Построена экспериментально: группы индексов красились контрастными цветами,
|
||||
пользователь сообщал, какие клавиши загорелись (методика и открытые вопросы —
|
||||
в docs/protocol.md).
|
||||
|
||||
Имена клавиш — позиции US-раскладки.
|
||||
"""
|
||||
|
||||
__all__ = ["KEYMAP", "BLIND", "UNKNOWN", "PROBE_COLORS", "KEY_SYMBOL_ALIASES",
|
||||
"name"]
|
||||
|
||||
# Подтверждённая карта (индекс → имя клавиши)
|
||||
KEYMAP = {
|
||||
0x01: "Esc",
|
||||
0x02: "F1", 0x03: "F2", 0x04: "F3", 0x05: "F4",
|
||||
0x06: "F5", 0x07: "F6", 0x08: "F7", 0x09: "F8",
|
||||
0x0A: "F9", 0x0B: "F10", 0x0C: "F11", 0x0D: "F12",
|
||||
|
||||
0x13: "`(~)", 0x14: "1", 0x15: "2", 0x16: "3", 0x17: "4",
|
||||
0x18: "5", 0x19: "6", 0x1A: "7", 0x1B: "8", 0x1C: "9", 0x1D: "0",
|
||||
0x1E: "-(_)", 0x1F: "=(+)",
|
||||
|
||||
0x20: "NumLock", 0x21: "Num/", 0x22: "Num*",
|
||||
0x25: "Tab", 0x26: "Q", 0x27: "W", 0x28: "E", 0x29: "R",
|
||||
0x2A: "T", 0x2B: "Y", 0x2C: "U", 0x2D: "I", 0x2E: "O", 0x2F: "P",
|
||||
|
||||
0x30: "[({)", 0x31: "](})", 0x32: "Num7", 0x33: "Num8", 0x34: "Num9",
|
||||
0x37: "Caps", 0x38: "A", 0x39: "S", 0x3A: "D", 0x3B: "F",
|
||||
0x3C: "G", 0x3D: "H", 0x3E: "J", 0x3F: "K",
|
||||
|
||||
0x40: "L", 0x41: ";", 0x42: "'", 0x43: "\\",
|
||||
0x44: "Num4", 0x45: "Num5", 0x46: "Num6",
|
||||
|
||||
0x49: "LShift", 0x4A: "Z", 0x4B: "X", 0x4C: "C", 0x4D: "V",
|
||||
0x4E: "B", 0x4F: "N",
|
||||
0x50: "M", 0x51: ",", 0x52: ".", 0x53: "/", 0x54: "RShift",
|
||||
0x55: "Enter", 0x56: "Num1", 0x57: "Num2", 0x58: "Num3",
|
||||
|
||||
0x5B: "LCtrl", 0x5C: "LWin", 0x5D: "LAlt",
|
||||
0x5E: "Space", 0x5F: "RAlt", 0x60: "Fn", 0x61: "Menu", 0x62: "RCtrl",
|
||||
|
||||
0x63: "ArrowLeft", 0x64: "ArrowDown", 0x65: "ArrowUp",
|
||||
0x66: "ArrowRight", 0x67: "Backspace", 0x68: "Num0",
|
||||
0x69: "Num.(Del)", 0x6A: "NumEnter",
|
||||
0x70: "PrtSc", 0x71: "ScrLk", 0x73: "Pause", 0x74: "Insert",
|
||||
0x75: "Home", 0x76: "PgUp", 0x77: "Delete", 0x78: "End",
|
||||
0x79: "PgDown", 0x7A: "Num-", 0x7B: "Num+",
|
||||
}
|
||||
|
||||
# Слоты, которые красились, но не светятся: ни к какому LED не подключены
|
||||
BLIND = {0x00, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x23, 0x24, 0x35, 0x36,
|
||||
0x47, 0x48, 0x59, 0x5A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, 0x72,
|
||||
0x7C, 0x7D, 0x7E, 0x7F, 0x80, 0x81, 0x82, 0x83, 0x84, 0x85,
|
||||
0x86, 0x87, 0x88, 0x89, 0x8A, 0x8B, 0x8C, 0x8D, 0x8E}
|
||||
|
||||
# Индексы, не идентифицированные на момент снятия карты
|
||||
UNKNOWN = sorted(set(range(0x00, 0x8F)) - set(KEYMAP) - BLIND)
|
||||
|
||||
# Контрастная палитра для картирования (максимально различимые цвета,
|
||||
# за раз — немного точек)
|
||||
PROBE_COLORS = [
|
||||
(0xFF, 0x00, 0x00), # красный
|
||||
(0x00, 0xFF, 0x00), # зелёный
|
||||
(0x00, 0x00, 0xFF), # синий
|
||||
(0xFF, 0xFF, 0x00), # жёлтый
|
||||
(0xFF, 0x00, 0xFF), # маджента
|
||||
(0x00, 0xFF, 0xFF), # циан
|
||||
(0xFF, 0x80, 0x00), # оранжевый
|
||||
]
|
||||
|
||||
# Дополнительные алиасы клавиш: синоним → каноническое имя из KEYMAP
|
||||
# (в нижнем регистре). Позволяют писать --key tilde=red, --key lbracket=blue.
|
||||
# Символы-псевдонимы ('[', '~', ';') работают напрямую из KEYMAP.
|
||||
KEY_SYMBOL_ALIASES = {
|
||||
# цифровой ряд
|
||||
"tilde": "`", "grave": "`", "backquote": "`", "backtick": "`",
|
||||
"minus": "-", "dash": "-", "hyphen": "-",
|
||||
"equal": "=", "equals": "=",
|
||||
# основной блок
|
||||
"lbracket": "[", "leftbracket": "[", "openbracket": "[",
|
||||
"rbracket": "]", "rightbracket": "]", "closebracket": "]",
|
||||
"semicolon": ";", "colon": ";",
|
||||
"quote": "'", "apostrophe": "'", "singlequote": "'",
|
||||
"backslash": chr(92), "bslash": chr(92),
|
||||
"comma": ",",
|
||||
"period": ".", "dot": ".",
|
||||
"slash": "/", "question": "/",
|
||||
"lessthan": ",", "greaterthan": ".",
|
||||
# пробел и служебные
|
||||
"spacebar": "space",
|
||||
"capslock": "caps",
|
||||
"leftshift": "lshift", "shift": "lshift",
|
||||
"rightshift": "rshift",
|
||||
"leftctrl": "lctrl", "ctrl": "lctrl",
|
||||
"rightctrl": "rctrl",
|
||||
"leftalt": "lalt", "alt": "lalt",
|
||||
"rightalt": "ralt", "altgr": "ralt",
|
||||
"windows": "lwin", "super": "lwin", "meta": "lwin",
|
||||
"menu": "menu", "context": "menu", "apps": "menu",
|
||||
"enter": "enter", "return": "enter", "cr": "enter",
|
||||
"tab": "tab",
|
||||
"escape": "esc",
|
||||
"backspace": "backspace", "bksp": "backspace",
|
||||
"delete": "delete", "del": "delete",
|
||||
"insert": "insert", "ins": "insert",
|
||||
"home": "home", "end": "end",
|
||||
"pageup": "pgup", "pgdn": "pgdown", "pagedown": "pgdown",
|
||||
"printscreen": "prtsc", "prntscr": "prtsc", "pscr": "prtsc",
|
||||
"scrolllock": "scrlk", "slck": "scrlk",
|
||||
"pause": "pause", "break": "pause",
|
||||
"leftarrow": "arrowleft", "rightarrow": "arrowright",
|
||||
"uparrow": "arrowup", "downarrow": "arrowdown",
|
||||
"left": "arrowleft", "right": "arrowright",
|
||||
"up": "arrowup", "down": "arrowdown",
|
||||
# нумпад
|
||||
"numlock": "numlock", "numasterisk": "num*", "numtimes": "num*",
|
||||
"numminus": "num-", "numplus": "num+", "numpoint": "num.",
|
||||
"numdot": "num.", "numdecimal": "num.", "numdel": "num.",
|
||||
"numcomma": "num.", "numperiod": "num.",
|
||||
"numslash": "num/", "numdivide": "num/",
|
||||
"numenter": "numenter",
|
||||
"num0": "num0", "num1": "num1", "num2": "num2", "num3": "num3",
|
||||
"num4": "num4", "num5": "num5", "num6": "num6", "num7": "num7",
|
||||
"num8": "num8", "num9": "num9",
|
||||
}
|
||||
|
||||
|
||||
def name(idx: int) -> str:
|
||||
"""Имя слота: клавиша, «(blind)» для слепых или 0xNN? для неизвестных."""
|
||||
if idx in KEYMAP:
|
||||
return KEYMAP[idx]
|
||||
if idx in BLIND:
|
||||
return "(blind)"
|
||||
return f"0x{idx:02x}?"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Confirmed keymap:")
|
||||
for i in sorted(KEYMAP):
|
||||
print(f" 0x{i:02x}: {KEYMAP[i]}")
|
||||
print(f"\nBlind slots: {' '.join(f'{i:02x}' for i in sorted(BLIND))}")
|
||||
print(f"Unidentified ({len(UNKNOWN)}): "
|
||||
+ " ".join(f"{i:02x}" for i in UNKNOWN))
|
||||
|
||||
# алиасы, сгруппированные по клавишам в порядке раскладки
|
||||
from collections import defaultdict
|
||||
groups = defaultdict(list)
|
||||
for alias, canonical in KEY_SYMBOL_ALIASES.items():
|
||||
groups[canonical].append(alias)
|
||||
|
||||
def _idx(canonical: str) -> int:
|
||||
"""LED-индекс канонического имени (0xFF — если не найдено)."""
|
||||
for i, lbl in KEYMAP.items():
|
||||
if lbl.lower() == canonical or lbl.split("(")[0].lower() == canonical:
|
||||
return i
|
||||
return 0xFF
|
||||
|
||||
print(f"\nKey aliases ({len(KEY_SYMBOL_ALIASES)}):")
|
||||
for canonical in sorted(groups, key=_idx):
|
||||
aliases = ", ".join(sorted(groups[canonical]))
|
||||
print(f" {canonical:12s} <- {aliases}")
|
||||
Executable
+77
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Зонд конфиг-канала GANSS ARDOR_Katana (0c45:8006) через hidraw ioctl.
|
||||
|
||||
HIDIOCGFEATURE(len) = _IOWR('H', 0x07, len) — размер кодируется в номере ioctl.
|
||||
Для состояния клавиатуры зонд безопасен: только GET_FEATURE-запросы,
|
||||
ничего не записывается.
|
||||
|
||||
Использование: misc/probe.py [hidrawN]
|
||||
(по умолчанию — автоопределение всех hidraw-узлов клавиатуры)
|
||||
"""
|
||||
import array
|
||||
import fcntl
|
||||
import glob
|
||||
import os
|
||||
import struct
|
||||
import sys
|
||||
|
||||
# размеры буфера, которые пробуем по очереди для каждого report id
|
||||
PROBE_SIZES = (33, 65, 21, 17)
|
||||
|
||||
|
||||
def hid_iocgfeature(size: int) -> int:
|
||||
"""Номер ioctl _IOWR('H', 0x07, size) для чтения feature-репорта."""
|
||||
return (3 << 30) | (size << 16) | (0x48 << 8) | 0x07
|
||||
|
||||
|
||||
def find_katana_hidraws() -> list[str]:
|
||||
"""Имена всех hidraw-узлов клавиатуры (0C45:8006)."""
|
||||
links = glob.glob("/sys/bus/hid/devices/0003:0C45:8006.*/hidraw/hidraw*")
|
||||
return sorted(os.path.basename(link) for link in links)
|
||||
|
||||
|
||||
def read_report_id(device_path: str, report_id: int, size: int):
|
||||
"""Прочитать feature-репорт; None при ошибке (нет такого rid и т.п.)."""
|
||||
fd = os.open(device_path, os.O_RDWR)
|
||||
try:
|
||||
buf = array.array("B", bytes(1 + size)) # первый байт = report id
|
||||
buf[0] = report_id
|
||||
res = fcntl.ioctl(fd, hid_iocgfeature(len(buf)), buf)
|
||||
n = struct.unpack("i", res[:4])[0] # ioctl возвращает [count, данные]
|
||||
return bytes(res[4:4 + n])
|
||||
except OSError:
|
||||
return None
|
||||
finally:
|
||||
os.close(fd)
|
||||
|
||||
|
||||
def hexs(data: bytes) -> str:
|
||||
"""Байты в строку вида 'aa bb cc'."""
|
||||
return " ".join(f"{x:02x}" for x in data)
|
||||
|
||||
|
||||
def main():
|
||||
targets = [sys.argv[1]] if len(sys.argv) > 1 else find_katana_hidraws()
|
||||
if not targets:
|
||||
raise SystemExit("GANSS ARDOR_Katana hidraw not found")
|
||||
|
||||
for name in targets:
|
||||
path = f"/dev/{name}"
|
||||
print(f"=== {path} ===")
|
||||
for rid in range(0x01, 0x09):
|
||||
found = None
|
||||
for size in PROBE_SIZES:
|
||||
data = read_report_id(path, rid, size)
|
||||
if data:
|
||||
found = (size, data)
|
||||
break
|
||||
if found:
|
||||
size, data = found
|
||||
print(f" GET_FEATURE id=0x{rid:02x} req_size={size}: "
|
||||
f"{hexs(data)} ({len(data)} bytes)")
|
||||
else:
|
||||
print(f" GET_FEATURE id=0x{rid:02x}: empty")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user