Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 137 additions & 1 deletion adafruit_usb_host_mouse/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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))
Expand All @@ -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)