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() 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"