From b89f5b8e8e13677815562d619884560eef7ca9b3 Mon Sep 17 00:00:00 2001 From: RetiredWizard Date: Sun, 23 Nov 2025 20:48:05 -0500 Subject: [PATCH 1/3] Add support for composite HID devices --- adafruit_usb_host_mouse/__init__.py | 139 +++++++++++++++++++++++++++- 1 file changed, 137 insertions(+), 2 deletions(-) diff --git a/adafruit_usb_host_mouse/__init__.py b/adafruit_usb_host_mouse/__init__.py index ca00977..1432451 100644 --- a/adafruit_usb_host_mouse/__init__.py +++ b/adafruit_usb_host_mouse/__init__.py @@ -58,7 +58,7 @@ def find_and_init_boot_mouse(cursor_image=DEFAULT_CURSOR): # noqa: PLR0912 mouse_device = None # scan for connected USB device and loop over any found - print("scanning usb") + print("scanning usb (boot)") for device in usb.core.find(find_all=True): # print device info try: @@ -122,8 +122,85 @@ def find_and_init_boot_mouse(cursor_image=DEFAULT_CURSOR): # noqa: PLR0912 # if no mouse found return None +def find_and_init_report_mouse(cursor_image=DEFAULT_CURSOR): # noqa: PLR0912 + """ + Scan for an attached report mouse connected via USB host. + If one is found initialize an instance of :class:`ReportMouse` class + and return it. + + :param cursor_image: Provide the absolute path to the desired cursor bitmap image. If set as + `None`, the :class:`ReportMouse` instance will not control a :class:`displayio.TileGrid` object. + :return: The :class:`ReportMouse` instance or None if no mouse was found. + """ + mouse_interface_index, mouse_endpoint_address = None, None + mouse_device = None + + # scan for connected USB device and loop over any found + print("scanning usb (report)") + for device in usb.core.find(find_all=True): + # print device info + try: + try: + print(f"{device.idVendor:04x}:{device.idProduct:04x}") + except usb.core.USBError as e: + print_exception(e, e, None) + try: + print(device.manufacturer, device.product) + except usb.core.USBError as e: + print_exception(e, e, None) + print() + config_descriptor = adafruit_usb_host_descriptors.get_configuration_descriptor( + device, 0 + ) + print(config_descriptor) + + _possible_interface_index, _possible_endpoint_address = ( + adafruit_usb_host_descriptors.find_report_mouse_endpoint(device) + ) + if _possible_interface_index is not None and _possible_endpoint_address is not None: + mouse_device = device + mouse_interface_index = _possible_interface_index + mouse_endpoint_address = _possible_endpoint_address + print( + f"mouse interface: {mouse_interface_index} " + + f"endpoint_address: {hex(mouse_endpoint_address)}" + ) + break + print("was not a report mouse") + except usb.core.USBError as e: + print_exception(e, e, None) + + mouse_was_attached = None + if mouse_device is not None: + # detach the kernel driver if needed + if mouse_device.is_kernel_driver_active(0): + mouse_was_attached = True + mouse_device.detach_kernel_driver(0) + else: + mouse_was_attached = False + + # set configuration on the mouse so we can use it + mouse_device.set_configuration() + + # load the mouse cursor bitmap + if isinstance(cursor_image, str): + mouse_bmp = OnDiskBitmap(cursor_image) + + # make the background pink pixels transparent + mouse_bmp.pixel_shader.make_transparent(0) + + # create a TileGrid for the mouse, using its bitmap and pixel_shader + mouse_tg = TileGrid(mouse_bmp, pixel_shader=mouse_bmp.pixel_shader) + + else: + mouse_tg = None + + return ReportMouse(mouse_device, mouse_endpoint_address, mouse_was_attached, mouse_tg) -class BootMouse: + # if no mouse found + return None + +class BootMouse(): """ Helpler class that encapsulates the objects needed to interact with a boot mouse, show a visible cursor on the display, and determine when buttons @@ -229,6 +306,8 @@ def update(self): # update the mouse x and y coordinates # based on the delta values read from the mouse + # Standard Boot Mouse: 3 bytes [Btn, X, Y] + dx, dy = self.buffer[1:3] dx = int(round((dx / self.sensitivity), 0)) dy = int(round((dy / self.sensitivity), 0)) @@ -253,3 +332,59 @@ def update(self): self.pressed_btns.append(button) return tuple(self.pressed_btns) + +class ReportMouse(BootMouse): + + def __init__(self, device, endpoint_address, was_attached, tilegrid=None, scale=1): # noqa: PLR0913, too many args + super().__init__(device, endpoint_address, was_attached, tilegrid, scale) + + def update(self): + """ + Read data from the USB mouse and update the location of the visible cursor + and check if any buttons are pressed. + + :return: a tuple containing one or more of the strings "left", "right", "middle" + indicating which buttons are pressed. If no buttons are pressed, the tuple will be empty. + If a error occurred while trying to read from the usb device, `None` will be returned. + """ + try: + # attempt to read data from the mouse + # 20ms timeout, so we don't block long if there + # is no data + count = self.device.read(self.endpoint, self.buffer, timeout=20) # noqa: F841, var assigned but not used + except usb.core.USBTimeoutError: + # skip the rest if there is no data + return None + except usb.core.USBError: + return None + + # update the mouse x and y coordinates + # based on the delta values read from the mouse + # Mouse with Report ID: 4 bytes [ID, Btn, X, Y] + offset = 1 + + dx, dy = self.buffer[1 + offset:3 + offset] + dx = int(round((dx / self.sensitivity), 0)) + dy = int(round((dy / self.sensitivity), 0)) + if self.tilegrid: + self.tilegrid.x = max( + 0, min((self.display_size[0] // self.scale) - 1, self.tilegrid.x + dx) + ) + self.tilegrid.y = max( + 0, min((self.display_size[1] // self.scale) - 1, self.tilegrid.y + dy) + ) + else: + self._x += dx + self._y += dy + + self.pressed_btns = [] + for i, button in enumerate(BUTTONS): + # check if each button is pressed using bitwise AND shifted + # to the appropriate index for this button + if self.buffer[0 + offset] & (1 << i) != 0: + # append the button name to the string to show if + # it is being clicked. + self.pressed_btns.append(button) + + return tuple(self.pressed_btns) + From f0283fa9451882dad7042cea044e305c222b8aa2 Mon Sep 17 00:00:00 2001 From: RetiredWizard Date: Sun, 23 Nov 2025 21:22:20 -0500 Subject: [PATCH 2/3] a little help from pre-commit --- adafruit_usb_host_mouse/__init__.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/adafruit_usb_host_mouse/__init__.py b/adafruit_usb_host_mouse/__init__.py index 1432451..d285ff5 100644 --- a/adafruit_usb_host_mouse/__init__.py +++ b/adafruit_usb_host_mouse/__init__.py @@ -122,6 +122,7 @@ def find_and_init_boot_mouse(cursor_image=DEFAULT_CURSOR): # noqa: PLR0912 # if no mouse found return None + def find_and_init_report_mouse(cursor_image=DEFAULT_CURSOR): # noqa: PLR0912 """ Scan for an attached report mouse connected via USB host. @@ -129,7 +130,8 @@ def find_and_init_report_mouse(cursor_image=DEFAULT_CURSOR): # noqa: PLR0912 and return it. :param cursor_image: Provide the absolute path to the desired cursor bitmap image. If set as - `None`, the :class:`ReportMouse` instance will not control a :class:`displayio.TileGrid` object. + `None`, the :class:`ReportMouse` instance will not control + a :class:`displayio.TileGrid` object. :return: The :class:`ReportMouse` instance or None if no mouse was found. """ mouse_interface_index, mouse_endpoint_address = None, None @@ -200,7 +202,8 @@ def find_and_init_report_mouse(cursor_image=DEFAULT_CURSOR): # noqa: PLR0912 # if no mouse found return None -class BootMouse(): + +class BootMouse: """ Helpler class that encapsulates the objects needed to interact with a boot mouse, show a visible cursor on the display, and determine when buttons @@ -307,7 +310,7 @@ def update(self): # update the mouse x and y coordinates # based on the delta values read from the mouse # Standard Boot Mouse: 3 bytes [Btn, X, Y] - + dx, dy = self.buffer[1:3] dx = int(round((dx / self.sensitivity), 0)) dy = int(round((dy / self.sensitivity), 0)) @@ -333,8 +336,8 @@ def update(self): return tuple(self.pressed_btns) -class ReportMouse(BootMouse): +class ReportMouse(BootMouse): def __init__(self, device, endpoint_address, was_attached, tilegrid=None, scale=1): # noqa: PLR0913, too many args super().__init__(device, endpoint_address, was_attached, tilegrid, scale) @@ -363,7 +366,7 @@ def update(self): # Mouse with Report ID: 4 bytes [ID, Btn, X, Y] offset = 1 - dx, dy = self.buffer[1 + offset:3 + offset] + dx, dy = self.buffer[1 + offset : 3 + offset] dx = int(round((dx / self.sensitivity), 0)) dy = int(round((dy / self.sensitivity), 0)) if self.tilegrid: @@ -387,4 +390,3 @@ def update(self): self.pressed_btns.append(button) return tuple(self.pressed_btns) - From 9e849538b67e4aa6eabcfcce2568d6baf231cce7 Mon Sep 17 00:00:00 2001 From: RetiredWizard Date: Sun, 23 Nov 2025 21:27:03 -0500 Subject: [PATCH 3/3] more formatting fixes --- adafruit_usb_host_mouse/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/adafruit_usb_host_mouse/__init__.py b/adafruit_usb_host_mouse/__init__.py index d285ff5..0e68546 100644 --- a/adafruit_usb_host_mouse/__init__.py +++ b/adafruit_usb_host_mouse/__init__.py @@ -130,8 +130,7 @@ def find_and_init_report_mouse(cursor_image=DEFAULT_CURSOR): # noqa: PLR0912 and return it. :param cursor_image: Provide the absolute path to the desired cursor bitmap image. If set as - `None`, the :class:`ReportMouse` instance will not control - a :class:`displayio.TileGrid` object. + `None`, the :class:`ReportMouse` will not control a :class:`displayio.TileGrid` object. :return: The :class:`ReportMouse` instance or None if no mouse was found. """ mouse_interface_index, mouse_endpoint_address = None, None