diff --git a/adafruit_usb_host_mouse/__init__.py b/adafruit_usb_host_mouse/__init__.py index ca00977..0e68546 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: @@ -123,6 +123,85 @@ def find_and_init_boot_mouse(cursor_image=DEFAULT_CURSOR): # noqa: PLR0912 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` 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) + + # if no mouse found + return None + + class BootMouse: """ Helpler class that encapsulates the objects needed to interact with a boot @@ -229,6 +308,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 +334,58 @@ 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)