196 lines
6.6 KiB
Python
196 lines
6.6 KiB
Python
# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries
|
|
# SPDX-FileCopyrightText: Copyright (c) 2025 Tim Cocks for Adafruit Industries
|
|
#
|
|
# SPDX-License-Identifier: MIT
|
|
"""
|
|
`adafruit_usb_host_mouse`
|
|
================================================================================
|
|
|
|
Helper class that encapsulates the objects needed for user code to interact with
|
|
a USB mouse, draw a visible cursor, and determine when buttons are pressed.
|
|
|
|
|
|
* Author(s): Tim Cocks
|
|
|
|
Implementation Notes
|
|
--------------------
|
|
|
|
**Hardware:**
|
|
|
|
* `USB Wired Mouse - Two Buttons plus Wheel <https://www.adafruit.com/product/2025>`_
|
|
|
|
**Software and Dependencies:**
|
|
|
|
* Adafruit CircuitPython firmware for the supported boards:
|
|
https://circuitpython.org/downloads
|
|
|
|
|
|
# * Adafruit's Bus Device library: https://github.com/adafruit/Adafruit_CircuitPython_BusDevice
|
|
# * Adafruit's Register library: https://github.com/adafruit/Adafruit_CircuitPython_Register
|
|
"""
|
|
|
|
import array
|
|
|
|
import adafruit_usb_host_descriptors
|
|
import supervisor
|
|
import usb
|
|
from displayio import OnDiskBitmap, TileGrid
|
|
|
|
__version__ = "0.0.0+auto.0"
|
|
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_USB_Host_Mouse.git"
|
|
|
|
BUTTONS = ["left", "right", "middle"]
|
|
|
|
|
|
def find_and_init_boot_mouse(cursor_image="/launcher_assets/mouse_cursor.bmp"):
|
|
"""
|
|
Scan for an attached boot mouse connected via USB host.
|
|
If one is found initialize an instance of BootMouse class
|
|
and return it.
|
|
:return: The BootMouse 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")
|
|
for device in usb.core.find(find_all=True):
|
|
# print device info
|
|
print(f"{device.idVendor:04x}:{device.idProduct:04x}")
|
|
print(device.manufacturer, device.product)
|
|
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_boot_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)}"
|
|
)
|
|
|
|
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 not isinstance(cursor_image, str):
|
|
raise TypeError("cursor_image must be a string")
|
|
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)
|
|
|
|
return BootMouse(mouse_device, mouse_endpoint_address, mouse_tg, mouse_was_attached)
|
|
|
|
# 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
|
|
were pressed.
|
|
|
|
: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
|
|
"""
|
|
|
|
def __init__(self, device, endpoint_address, tilegrid, was_attached, scale=1): # noqa: PLR0913, too many args
|
|
self.device = device
|
|
self.tilegrid = tilegrid
|
|
self.endpoint = endpoint_address
|
|
self.buffer = array.array("b", [0] * 4)
|
|
self.was_attached = was_attached
|
|
self.scale = scale
|
|
|
|
self.display_size = (supervisor.runtime.display.width, supervisor.runtime.display.height)
|
|
|
|
@property
|
|
def x(self):
|
|
"""
|
|
The x coordinate of the mouse cursor
|
|
"""
|
|
return self.tilegrid.x
|
|
|
|
@x.setter
|
|
def x(self, new_x):
|
|
self.tilegrid.x = new_x
|
|
|
|
@property
|
|
def y(self):
|
|
"""
|
|
The y coordinate of the mouse cursor
|
|
"""
|
|
return self.tilegrid.y
|
|
|
|
@y.setter
|
|
def y(self, new_y):
|
|
self.tilegrid.y = new_y
|
|
|
|
def release(self):
|
|
"""
|
|
Release the mouse cursor and re-attach it to the kernel
|
|
if it was attached previously.
|
|
"""
|
|
if self.was_attached and not self.device.is_kernel_driver_active(0):
|
|
self.device.attach_kernel_driver(0)
|
|
|
|
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 List containing one or more of the strings "left", "right", "middle"
|
|
indicating which buttons are pressed.
|
|
"""
|
|
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 tilegrid 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 + self.buffer[1])
|
|
)
|
|
self.tilegrid.y = max(
|
|
0, min((self.display_size[1] // self.scale) - 1, self.tilegrid.y + self.buffer[2])
|
|
)
|
|
|
|
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] & (1 << i) != 0:
|
|
# append the button name to the string to show if
|
|
# it is being clicked.
|
|
pressed_btns.append(button)
|
|
|
|
if len(pressed_btns) > 0:
|
|
return pressed_btns
|