Skip to content

Commit bd0f233

Browse files
authored
Merge pull request #14 from RetiredWizard/trackpad
Add support for composite HID devices
2 parents a182f5a + 9e84953 commit bd0f233

File tree

1 file changed

+137
-1
lines changed

1 file changed

+137
-1
lines changed

adafruit_usb_host_mouse/__init__.py

Lines changed: 137 additions & 1 deletion
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:
@@ -123,6 +123,85 @@ def find_and_init_boot_mouse(cursor_image=DEFAULT_CURSOR): # noqa: PLR0912
123123
return None
124124

125125

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

230309
# update the mouse x and y coordinates
231310
# based on the delta values read from the mouse
311+
# Standard Boot Mouse: 3 bytes [Btn, X, Y]
312+
232313
dx, dy = self.buffer[1:3]
233314
dx = int(round((dx / self.sensitivity), 0))
234315
dy = int(round((dy / self.sensitivity), 0))
@@ -253,3 +334,58 @@ def update(self):
253334
self.pressed_btns.append(button)
254335

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

0 commit comments

Comments
 (0)