Skip to content
Merged
Show file tree
Hide file tree
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
86 changes: 53 additions & 33 deletions adafruit_usb_host_mouse.py → adafruit_usb_host_mouse/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,18 @@
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_USB_Host_Mouse.git"

BUTTONS = ["left", "right", "middle"]
DEFAULT_CURSOR = "/".join(__file__.split("/")[:-1]) + "/mouse_cursor.bmp"


def find_and_init_boot_mouse(cursor_image="/launcher_assets/mouse_cursor.bmp"):
def find_and_init_boot_mouse(cursor_image=DEFAULT_CURSOR): # noqa: PLR0912
"""
Scan for an attached boot mouse connected via USB host.
If one is found initialize an instance of BootMouse class
If one is found initialize an instance of :class:`BootMouse` class
and return it.
:return: The BootMouse instance or None if no mouse was found.

:param cursor_image: Provide the absolute path to the desired cursor bitmap image. If set as
`None`, the :class:`BootMouse` instance will not control a :class:`displayio.TileGrid` object.
:return: The :class:`BootMouse` instance or None if no mouse was found.
"""
mouse_interface_index, mouse_endpoint_address = None, None
mouse_device = None
Expand Down Expand Up @@ -101,17 +105,19 @@ def find_and_init_boot_mouse(cursor_image="/launcher_assets/mouse_cursor.bmp"):
mouse_device.set_configuration()

# load the mouse cursor bitmap
if not isinstance(cursor_image, str):
raise TypeError("cursor_image must be a string")
mouse_bmp = OnDiskBitmap(cursor_image)
if isinstance(cursor_image, str):
mouse_bmp = OnDiskBitmap(cursor_image)

# make the background pink pixels transparent
mouse_bmp.pixel_shader.make_transparent(0)

# 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)

# 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 BootMouse(mouse_device, mouse_endpoint_address, mouse_tg, mouse_was_attached)
return BootMouse(mouse_device, mouse_endpoint_address, mouse_was_attached, mouse_tg)

# if no mouse found
return None
Expand All @@ -125,13 +131,13 @@ class BootMouse:

:param device: The usb device instance for the mouse
:param endpoint_address: The address of the mouse endpoint
:param tilegrid: The TileGrid that holds the visible mouse cursor
:param was_attached: Whether the usb device was attached to the kernel
:param tilegrid: The TileGrid that holds the visible mouse cursor
:param scale: The scale of the group that the Mouse TileGrid will be put into.
Needed in order to properly clamp the mouse to the display bounds
"""

def __init__(self, device, endpoint_address, tilegrid, was_attached, scale=1): # noqa: PLR0913, too many args
def __init__(self, device, endpoint_address, was_attached, tilegrid=None, scale=1): # noqa: PLR0913, too many args
self.device = device

self.tilegrid = tilegrid
Expand All @@ -154,29 +160,44 @@ def __init__(self, device, endpoint_address, tilegrid, was_attached, scale=1):
If there's no new mouse data (nothing changes) this property can be checked to see
which buttons are currently pressed."""

self.display_size = (supervisor.runtime.display.width, supervisor.runtime.display.height)
if tilegrid is not None:
self.display_size = (
supervisor.runtime.display.width,
supervisor.runtime.display.height,
)
self.tilegrid.x, self.tilegrid.y = (
x // 2 for x in self.display_size
) # center cursor in display
else:
self._x, self._y = 0, 0

@property
def x(self) -> int:
"""
The x coordinate of the mouse cursor
"""
return self.tilegrid.x
return self.tilegrid.x if self.tilegrid else self._x

@x.setter
def x(self, new_x: int) -> None:
self.tilegrid.x = new_x
if self.tilegrid:
self.tilegrid.x = new_x
else:
self._x = new_x

@property
def y(self) -> int:
"""
The y coordinate of the mouse cursor
"""
return self.tilegrid.y
return self.tilegrid.y if self.tilegrid else self._y

@y.setter
def y(self, new_y: int) -> None:
self.tilegrid.y = new_y
if self.tilegrid:
self.tilegrid.y = new_y
else:
self._y = new_y

def release(self):
"""
Expand Down Expand Up @@ -206,22 +227,21 @@ def update(self):
except usb.core.USBError:
return None

# update the mouse tilegrid x and y coordinates
# update the mouse x and y coordinates
# based on the delta values read from the mouse
self.tilegrid.x = max(
0,
min(
(self.display_size[0] // self.scale) - 1,
self.tilegrid.x + int(round((self.buffer[1] / self.sensitivity), 0)),
),
)
self.tilegrid.y = max(
0,
min(
(self.display_size[1] // self.scale) - 1,
self.tilegrid.y + int(round((self.buffer[2] / self.sensitivity), 0)),
),
)
dx, dy = self.buffer[1:3]
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):
Expand Down
Binary file added adafruit_usb_host_mouse/mouse_cursor.bmp
Binary file not shown.
3 changes: 3 additions & 0 deletions adafruit_usb_host_mouse/mouse_cursor.bmp.license
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
SPDX-FileCopyrightText: 2025 Tim Cocks for Adafruit Industries

SPDX-License-Identifier: MIT
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ dynamic = ["dependencies", "optional-dependencies"]
[tool.setuptools]
# TODO: IF LIBRARY FILES ARE A PACKAGE FOLDER,
# CHANGE `py_modules = ['...']` TO `packages = ['...']`
py-modules = ["adafruit_usb_host_mouse"]
packages = ["adafruit_usb_host_mouse"]

[tool.setuptools.dynamic]
dependencies = {file = ["requirements.txt"]}
Expand Down