From 56a8173f4838e00c6af55c6e37c01c04845e2375 Mon Sep 17 00:00:00 2001 From: dano <81797108+dwaycik@users.noreply.github.com> Date: Fri, 7 Aug 2026 14:35:29 +0200 Subject: [PATCH 1/2] Make the HID interface selectable, defaulting away from interface 0 _open_device() opens with hid.device().open(vendor_id, product_id), which takes whichever interface the backend enumerates first. On an RT100 that is interface 0 -- the one carrying key input, which the README warns against using. 0.0.8 did not have this problem: _find_device_path() matched DEVICE_DESCRIPTION_REGEX against /sys/class/input and opened by path, which landed on interface 1. That lookup was removed in 0.0.9. Adds an INTERFACE config key, defaulting to 1 to restore the pre-0.0.9 behaviour rather than pick a new number. Setting it to null falls back to opening by vendor/product id, which is what Windows needs -- hidapi reports interface_number as -1 there, so path-based selection cannot work. If the configured interface is not present the code logs a warning and falls back rather than failing. Measured on an RT100 (3151:4010), for whoever picks the default later: interface 0 carries the keyboard, 1 carries Consumer Control -- holding it takes the volume knob and media keys offline until released -- and 2 carries no input collections at all. 2 may therefore suit start-daemon better, which holds the device continuously, but that is one board and the default here deliberately restores what 0.0.8 did rather than generalising from it. --- src/epomakercontroller/configs/default.json | 1 + src/epomakercontroller/epomakercontroller.py | 30 +++++++++++++++++++- tests/data/config/config.json | 1 + 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/epomakercontroller/configs/default.json b/src/epomakercontroller/configs/default.json index e7f783d..7c1091a 100644 --- a/src/epomakercontroller/configs/default.json +++ b/src/epomakercontroller/configs/default.json @@ -10,6 +10,7 @@ 16406 ], "USE_WIRELESS": false, + "INTERFACE": 1, "DEVICE_DESCRIPTION_REGEX": "ROYUAN .* System Control", "CONF_LAYOUT_PATH": "EpomakerRT100-UK-ISO.json", "CONF_KEYMAP_PATH": "EpomakerRT100.json" diff --git a/src/epomakercontroller/epomakercontroller.py b/src/epomakercontroller/epomakercontroller.py index de08f8d..53109cc 100644 --- a/src/epomakercontroller/epomakercontroller.py +++ b/src/epomakercontroller/epomakercontroller.py @@ -58,6 +58,10 @@ def __init__(self, config_main: Config) -> None: self.vendor_id = config_main["VENDOR_ID"] self.use_wireless = config_main["USE_WIRELESS"] + # Which HID interface to open. None falls back to opening by + # vendor/product id, which takes whichever the backend enumerates + # first -- on Windows that is the only option available. + self.interface: Optional[int] = config_main["INTERFACE"] self.product_ids: list[int] = ( config_main["PRODUCT_IDS_WIRED"] @@ -170,6 +174,26 @@ def _find_product_id(self) -> Optional[int]: return None + def _find_interface_path(self, product_id: int) -> Optional[bytes]: + """Path of the configured HID interface, or None to open by id. + + Returns None when no interface is configured, or when the backend does + not report interface numbers -- notably on Windows, where hidapi + reports -1 and opening by vendor/product id is the only option. + """ + if self.config.interface is None: + return None + + for entry in hid.enumerate(self.config.vendor_id, product_id): + if entry.get("interface_number") == self.config.interface: + return entry["path"] + + Logger.log_warning( + f"Interface {self.config.interface} not found on device " + f"{self.config.vendor_id:04x}:{product_id:04x}, opening by id instead" + ) + return None + def _open_device(self, product_id: int) -> None: """Opens the USB HID device. @@ -177,7 +201,11 @@ def _open_device(self, product_id: int) -> None: product_id (int): The product ID. """ try: - self.device.open(self.config.vendor_id, product_id) + path = self._find_interface_path(product_id) + if path is not None: + self.device.open_path(path) + else: + self.device.open(self.config.vendor_id, product_id) except IOError as e: Logger.log_error( f"Failed to open device: {e}\n" diff --git a/tests/data/config/config.json b/tests/data/config/config.json index f709122..312d654 100644 --- a/tests/data/config/config.json +++ b/tests/data/config/config.json @@ -10,6 +10,7 @@ 16406 ], "USE_WIRELESS": false, + "INTERFACE": 1, "DEVICE_DESCRIPTION_REGEX": "ROYUAN .* System Control", "CONF_LAYOUT_PATH": "EpomakerRT100-UK-ISO.json", "CONF_KEYMAP_PATH": "EpomakerRT100.json" From aa143d7dc30b805dd7fcbfe070b11b5f0fa87144 Mon Sep 17 00:00:00 2001 From: dano <81797108+dwaycik@users.noreply.github.com> Date: Fri, 7 Aug 2026 14:35:29 +0200 Subject: [PATCH 2/2] Add --interface to the CLI Not part of the fix, and kept as its own commit so it is easy to leave out. Which interface is correct turned out to be device-specific, and editing config.json to try one is awkward, so this exposes the same setting as a flag for troubleshooting. The config key remains the way to set it. --- src/epomakercontroller/cli.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/epomakercontroller/cli.py b/src/epomakercontroller/cli.py index e8ed064..f5e5f1e 100644 --- a/src/epomakercontroller/cli.py +++ b/src/epomakercontroller/cli.py @@ -34,8 +34,17 @@ def wrapper(*args, **kwargs): @click.group() @click.version_option(retrieve_app_version(), prog_name="EpomakerController") -def cli() -> None: +@click.option( + "--interface", + type=int, + default=None, + help="HID interface to open, overriding INTERFACE in config.json. " + "Try another value if the keyboard misbehaves while a command runs.", +) +def cli(interface: int | None) -> None: """A simple CLI for the EpomakerController.""" + if interface is not None: + CONFIG_MAIN.data["INTERFACE"] = interface @cli.command()