Skip to content

Commit b89f5b8

Browse files
committed
Add support for composite HID devices
1 parent a182f5a commit b89f5b8

File tree

1 file changed

+137
-2
lines changed

1 file changed

+137
-2
lines changed

adafruit_usb_host_mouse/__init__.py

Lines changed: 137 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ def find_and_init_boot_mouse(cursor_image=DEFAULT_CURSOR): # noqa: PLR0912
5858
mouse_device = None
5959

6060
# scan for connected USB device and loop over any found
61-
print("scanning usb")
61+
print("scanning usb (boot)")
6262
for device in usb.core.find(find_all=True):
6363
# print device info
6464
try:
@@ -122,8 +122,85 @@ def find_and_init_boot_mouse(cursor_image=DEFAULT_CURSOR): # noqa: PLR0912
122122
# if no mouse found
123123
return None
124124

125+
def find_and_init_report_mouse(cursor_image=DEFAULT_CURSOR): # noqa: PLR0912
126+
"""
127+
Scan for an attached report mouse connected via USB host.
128+
If one is found initialize an instance of :class:`ReportMouse` class
129+
and return it.
130+
131+
:param cursor_image: Provide the absolute path to the desired cursor bitmap image. If set as
132+
`None`, the :class:`ReportMouse` instance will not control a :class:`displayio.TileGrid` object.
133+
:return: The :class:`ReportMouse` instance or None if no mouse was found.
134+
"""
135+
mouse_interface_index, mouse_endpoint_address = None, None
136+
mouse_device = None
137+
138+
# scan for connected USB device and loop over any found
139+
print("scanning usb (report)")
140+
for device in usb.core.find(find_all=True):
141+
# print device info
142+
try:
143+
try:
144+
print(f"{device.idVendor:04x}:{device.idProduct:04x}")
145+
except usb.core.USBError as e:
146+
print_exception(e, e, None)
147+
try:
148+
print(device.manufacturer, device.product)
149+
except usb.core.USBError as e:
150+
print_exception(e, e, None)
151+
print()
152+
config_descriptor = adafruit_usb_host_descriptors.get_configuration_descriptor(
153+
device, 0
154+
)
155+
print(config_descriptor)
156+
157+
_possible_interface_index, _possible_endpoint_address = (
158+
adafruit_usb_host_descriptors.find_report_mouse_endpoint(device)
159+
)
160+
if _possible_interface_index is not None and _possible_endpoint_address is not None:
161+
mouse_device = device
162+
mouse_interface_index = _possible_interface_index
163+
mouse_endpoint_address = _possible_endpoint_address
164+
print(
165+
f"mouse interface: {mouse_interface_index} "
166+
+ f"endpoint_address: {hex(mouse_endpoint_address)}"
167+
)
168+
break
169+
print("was not a report mouse")
170+
except usb.core.USBError as e:
171+
print_exception(e, e, None)
172+
173+
mouse_was_attached = None
174+
if mouse_device is not None:
175+
# detach the kernel driver if needed
176+
if mouse_device.is_kernel_driver_active(0):
177+
mouse_was_attached = True
178+
mouse_device.detach_kernel_driver(0)
179+
else:
180+
mouse_was_attached = False
181+
182+
# set configuration on the mouse so we can use it
183+
mouse_device.set_configuration()
184+
185+
# load the mouse cursor bitmap
186+
if isinstance(cursor_image, str):
187+
mouse_bmp = OnDiskBitmap(cursor_image)
188+
189+
# make the background pink pixels transparent
190+
mouse_bmp.pixel_shader.make_transparent(0)
191+
192+
# create a TileGrid for the mouse, using its bitmap and pixel_shader
193+
mouse_tg = TileGrid(mouse_bmp, pixel_shader=mouse_bmp.pixel_shader)
194+
195+
else:
196+
mouse_tg = None
197+
198+
return ReportMouse(mouse_device, mouse_endpoint_address, mouse_was_attached, mouse_tg)
125199

126-
class BootMouse:
200+
# if no mouse found
201+
return None
202+
203+
class BootMouse():
127204
"""
128205
Helpler class that encapsulates the objects needed to interact with a boot
129206
mouse, show a visible cursor on the display, and determine when buttons
@@ -229,6 +306,8 @@ def update(self):
229306

230307
# update the mouse x and y coordinates
231308
# based on the delta values read from the mouse
309+
# Standard Boot Mouse: 3 bytes [Btn, X, Y]
310+
232311
dx, dy = self.buffer[1:3]
233312
dx = int(round((dx / self.sensitivity), 0))
234313
dy = int(round((dy / self.sensitivity), 0))
@@ -253,3 +332,59 @@ def update(self):
253332
self.pressed_btns.append(button)
254333

255334
return tuple(self.pressed_btns)
335+
336+
class ReportMouse(BootMouse):
337+
338+
def __init__(self, device, endpoint_address, was_attached, tilegrid=None, scale=1): # noqa: PLR0913, too many args
339+
super().__init__(device, endpoint_address, was_attached, tilegrid, scale)
340+
341+
def update(self):
342+
"""
343+
Read data from the USB mouse and update the location of the visible cursor
344+
and check if any buttons are pressed.
345+
346+
:return: a tuple containing one or more of the strings "left", "right", "middle"
347+
indicating which buttons are pressed. If no buttons are pressed, the tuple will be empty.
348+
If a error occurred while trying to read from the usb device, `None` will be returned.
349+
"""
350+
try:
351+
# attempt to read data from the mouse
352+
# 20ms timeout, so we don't block long if there
353+
# is no data
354+
count = self.device.read(self.endpoint, self.buffer, timeout=20) # noqa: F841, var assigned but not used
355+
except usb.core.USBTimeoutError:
356+
# skip the rest if there is no data
357+
return None
358+
except usb.core.USBError:
359+
return None
360+
361+
# update the mouse x and y coordinates
362+
# based on the delta values read from the mouse
363+
# Mouse with Report ID: 4 bytes [ID, Btn, X, Y]
364+
offset = 1
365+
366+
dx, dy = self.buffer[1 + offset:3 + offset]
367+
dx = int(round((dx / self.sensitivity), 0))
368+
dy = int(round((dy / self.sensitivity), 0))
369+
if self.tilegrid:
370+
self.tilegrid.x = max(
371+
0, min((self.display_size[0] // self.scale) - 1, self.tilegrid.x + dx)
372+
)
373+
self.tilegrid.y = max(
374+
0, min((self.display_size[1] // self.scale) - 1, self.tilegrid.y + dy)
375+
)
376+
else:
377+
self._x += dx
378+
self._y += dy
379+
380+
self.pressed_btns = []
381+
for i, button in enumerate(BUTTONS):
382+
# check if each button is pressed using bitwise AND shifted
383+
# to the appropriate index for this button
384+
if self.buffer[0 + offset] & (1 << i) != 0:
385+
# append the button name to the string to show if
386+
# it is being clicked.
387+
self.pressed_btns.append(button)
388+
389+
return tuple(self.pressed_btns)
390+

0 commit comments

Comments
 (0)