Support Super X RGB HID interface

This commit is contained in:
Hermes 2026-05-11 19:36:59 +02:00
parent 4eed66b8ea
commit 9cb92f1ba5
2 changed files with 46 additions and 8 deletions

View file

@ -9,9 +9,16 @@ import argparse
import json
from pathlib import Path
DEFAULT_VENDOR_ID = 0x1A2C
DEFAULT_PRODUCT_ID = 0xB001
DEFAULT_INTERFACE = 0
# Observed OneXPlayer Super X RGB HID variants.
# 1a86:1305 exposes keyboard/touch-style interfaces plus a vendor-defined
# output interface at bInterfaceNumber 02. Older captures used 1a2c:b001:00.
DEFAULT_VENDOR_ID = 0x1A86
DEFAULT_PRODUCT_ID = 0x1305
DEFAULT_INTERFACE = 2
KNOWN_DEVICES = [
(0x1A86, 0x1305, 2),
(0x1A2C, 0xB001, 0),
]
PACKET_SIZE = 64
BRIGHTNESS_LEVELS = {
"off": [0x07, 0xFF, 0xFD, 0x00, 0x05, 0x01],
@ -52,6 +59,21 @@ def find_hidraw_path(vendor_id: int, product_id: int, interface: int) -> Path:
)
def find_known_hidraw_path() -> tuple[Path, int, int, int]:
for vendor_id, product_id, interface in KNOWN_DEVICES:
try:
return find_hidraw_path(vendor_id, product_id, interface), vendor_id, product_id, interface
except RuntimeError:
continue
raise RuntimeError(
"known RGB hidraw device not found; tried "
+ ", ".join(
f"0x{vendor:04x}:0x{product:04x}:if{interface}"
for vendor, product, interface in KNOWN_DEVICES
)
)
def pad_packet(prefix: list[int]) -> list[int]:
if len(prefix) > PACKET_SIZE:
raise ValueError(f"packet too long: {len(prefix)}")
@ -99,9 +121,9 @@ def build_preset_packet(preset_id: int) -> list[int]:
def build_parser():
parser = argparse.ArgumentParser(description="Experimental OneXPlayer Super X RGB HID helper")
parser.add_argument("--vendor-id", type=parse_int, default=DEFAULT_VENDOR_ID)
parser.add_argument("--product-id", type=parse_int, default=DEFAULT_PRODUCT_ID)
parser.add_argument("--interface", type=int, default=DEFAULT_INTERFACE)
parser.add_argument("--vendor-id", type=parse_int, default=None)
parser.add_argument("--product-id", type=parse_int, default=None)
parser.add_argument("--interface", type=int, default=None)
parser.add_argument("--dry-run", action="store_true")
parser.add_argument("--json", action="store_true")
@ -151,7 +173,7 @@ def main():
result = {
**action,
"interface": args.interface,
"interface": args.interface if args.interface is not None else "auto",
"packet": packet,
}
if args.dry_run:
@ -159,7 +181,17 @@ def main():
print(json.dumps(result, indent=2) if args.json else result)
return
hidraw_path = find_hidraw_path(args.vendor_id, args.product_id, args.interface)
if args.vendor_id is None and args.product_id is None and args.interface is None:
hidraw_path, args.vendor_id, args.product_id, args.interface = find_known_hidraw_path()
else:
vendor_id = DEFAULT_VENDOR_ID if args.vendor_id is None else args.vendor_id
product_id = DEFAULT_PRODUCT_ID if args.product_id is None else args.product_id
interface = DEFAULT_INTERFACE if args.interface is None else args.interface
hidraw_path = find_hidraw_path(vendor_id, product_id, interface)
args.vendor_id, args.product_id, args.interface = vendor_id, product_id, interface
result["vendor_id"] = f"0x{args.vendor_id:04x}"
result["product_id"] = f"0x{args.product_id:04x}"
result["interface"] = args.interface
with hidraw_path.open("r+b", buffering=0) as device:
written = device.write(bytes(packet))
result["status"] = "ok"

View file

@ -1,2 +1,8 @@
SUBSYSTEM=="usb", ATTR{idVendor}=="1a2c", ATTR{idProduct}=="b001", MODE="0666", TAG+="uaccess"
SUBSYSTEM=="hidraw", ENV{ID_VENDOR_ID}=="1a2c", ENV{ID_MODEL_ID}=="b001", ENV{ID_USB_INTERFACE_NUM}=="00", MODE="0666", TAG+="uaccess"
# OneXPlayer Super X observed on this unit: QinHeng 1a86:1305, vendor-defined
# RGB output on interface 02. Interface 00 is keyboard and returns EPROTO for
# the RGB output reports.
SUBSYSTEM=="usb", ATTR{idVendor}=="1a86", ATTR{idProduct}=="1305", MODE="0666", TAG+="uaccess"
SUBSYSTEM=="hidraw", ENV{ID_VENDOR_ID}=="1a86", ENV{ID_MODEL_ID}=="1305", ENV{ID_USB_INTERFACE_NUM}=="02", MODE="0666", TAG+="uaccess"