@@ -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