1+ # SPDX-FileCopyrightText: Copyright (c) 2025 Tim Cocks for Adafruit Industries
2+ #
3+ # SPDX-License-Identifier: MIT
4+ import array
5+ import supervisor
6+ import terminalio
7+ import usb .core
8+ from adafruit_display_text .bitmap_label import Label
9+ from displayio import Group , OnDiskBitmap , TileGrid , Palette , ColorConverter
10+
11+ import adafruit_usb_host_descriptors
12+
13+ # use the default built-in display,
14+ # the HSTX / PicoDVI display for the Metro RP2350
15+ display = supervisor .runtime .display
16+
17+ # a group to hold all other visual elements
18+ main_group = Group ()
19+
20+ # set the main group to show on the display
21+ display .root_group = main_group
22+
23+ # load the cursor bitmap file
24+ mouse_bmp = OnDiskBitmap ("mouse_cursor.bmp" )
25+
26+ # lists for labels, mouse tilegrids, and palettes.
27+ # each mouse will get 1 of each item. All lists
28+ # will end up with length 2.
29+ output_lbls = []
30+ mouse_tgs = []
31+ palettes = []
32+
33+ # the different colors to use for each mouse cursor
34+ # and labels
35+ colors = [0xFF00FF , 0x00FF00 ]
36+
37+ for i in range (2 ):
38+ # create a palette for this mouse
39+ mouse_palette = Palette (3 )
40+ # index zero is used for transparency
41+ mouse_palette .make_transparent (0 )
42+ # add the palette to the list of palettes
43+ palettes .append (mouse_palette )
44+
45+ # copy the first two colors from mouse palette
46+ for palette_color_index in range (2 ):
47+ mouse_palette [palette_color_index ] = mouse_bmp .pixel_shader [palette_color_index ]
48+
49+ # replace the last color with different color for each mouse
50+ mouse_palette [2 ] = colors [i ]
51+
52+ # create a TileGrid for this mouse cursor.
53+ # use the palette created above
54+ mouse_tg = TileGrid (mouse_bmp , pixel_shader = mouse_palette )
55+
56+ # move the mouse tilegrid to near the center of the display
57+ mouse_tg .x = display .width // 2 - (i * 12 )
58+ mouse_tg .y = display .height // 2
59+
60+ # add this mouse tilegrid to the list of mouse tilegrids
61+ mouse_tgs .append (mouse_tg )
62+
63+ # add this mouse tilegrid to the main group so it will show
64+ # on the display
65+ main_group .append (mouse_tg )
66+
67+ # create a label for this mouse
68+ output_lbl = Label (terminalio .FONT , text = f"{ mouse_tg .x } ,{ mouse_tg .y } " , color = colors [i ], scale = 1 )
69+ # anchored to the top left corner of the label
70+ output_lbl .anchor_point = (0 , 0 )
71+
72+ # move to op left corner of the display, moving
73+ # down by a static amount to static the two labels
74+ # one below the other
75+ output_lbl .anchored_position = (1 , 1 + i * 13 )
76+
77+ # add the label to the list of labels
78+ output_lbls .append (output_lbl )
79+
80+ # add the label to the main group so it will show
81+ # on the display
82+ main_group .append (output_lbl )
83+
84+ # lists for mouse interface indexes, endpoint addresses, and USB Device instances
85+ # each of these will end up with length 2 once we find both mice
86+ mouse_interface_indexes = []
87+ mouse_endpoint_addresses = []
88+ mice = []
89+
90+ # scan for connected USB devices
91+ for device in usb .core .find (find_all = True ):
92+ # check for boot mouse endpoints on this device
93+ mouse_interface_index , mouse_endpoint_address = (
94+ adafruit_usb_host_descriptors .find_boot_mouse_endpoint (device )
95+ )
96+ # if a boot mouse interface index and endpoint address were found
97+ if mouse_interface_index is not None and mouse_endpoint_address is not None :
98+ # add the interface index to the list of indexes
99+ mouse_interface_indexes .append (mouse_interface_index )
100+ # add the endpoint address to the list of addresses
101+ mouse_endpoint_addresses .append (mouse_endpoint_address )
102+ # add the device instance to the list of mice
103+ mice .append (device )
104+
105+ # print details to the console
106+ print (f"mouse interface: { mouse_interface_index } " , end = "" )
107+ print (f"endpoint_address: { hex (mouse_endpoint_address )} " )
108+
109+ # detach device from kernel if needed
110+ if device .is_kernel_driver_active (0 ):
111+ device .detach_kernel_driver (0 )
112+
113+ # set the mouse configuration so it can be used
114+ device .set_configuration ()
115+
116+ # This is ordered by bit position.
117+ BUTTONS = ["left" , "right" , "middle" ]
118+
119+ # list of buffers, will hold one buffer for each mouse
120+ mouse_bufs = []
121+ for i in range (2 ):
122+ # Buffer to hold data read from the mouse
123+ mouse_bufs .append (array .array ("b" , [0 ] * 8 ))
124+
125+
126+ def get_mouse_deltas (buffer , read_count ):
127+ """
128+ Given a buffer and read_count return the x and y delta values
129+ :param buffer: A buffer containing data read from the mouse
130+ :param read_count: How many bytes of data were read from the mouse
131+ :return: tuple x,y delta values
132+ """
133+ if read_count == 4 :
134+ delta_x = buffer [1 ]
135+ delta_y = buffer [2 ]
136+ elif read_count == 8 :
137+ delta_x = buffer [2 ]
138+ delta_y = buffer [4 ]
139+ else :
140+ raise ValueError (f"Unsupported mouse packet size: { read_count } , must be 4 or 8" )
141+ return delta_x , delta_y
142+
143+ # main loop
144+ while True :
145+ # for each mouse instance
146+ for mouse_index , mouse in enumerate (mice ):
147+ # try to read data from the mouse
148+ try :
149+ count = mouse .read (
150+ mouse_endpoint_addresses [mouse_index ], mouse_bufs [mouse_index ], timeout = 10
151+ )
152+
153+ # if there is no data it will raise USBTimeoutError
154+ except usb .core .USBTimeoutError :
155+ # Nothing to do if there is no data for this mouse
156+ continue
157+
158+ # there was mouse data, so get the delta x and y values from it
159+ mouse_deltas = get_mouse_deltas (mouse_bufs [mouse_index ], count )
160+
161+ # update the x position of this mouse cursor using the delta value
162+ # clamped to the display size
163+ mouse_tgs [mouse_index ].x = max (
164+ 0 , min (display .width - 1 , mouse_tgs [mouse_index ].x + mouse_deltas [0 ])
165+ )
166+ # update the y position of this mouse cursor using the delta value
167+ # clamped to the display size
168+ mouse_tgs [mouse_index ].y = max (
169+ 0 , min (display .height - 1 , mouse_tgs [mouse_index ].y + mouse_deltas [1 ])
170+ )
171+
172+ # output string with the new cursor position
173+ out_str = f"{ mouse_tgs [mouse_index ].x } ,{ mouse_tgs [mouse_index ].y } "
174+
175+ # loop over possible button bit indexes
176+ for i , button in enumerate (BUTTONS ):
177+ # check each bit index to determin if the button was pressed
178+ if mouse_bufs [mouse_index ][0 ] & (1 << i ) != 0 :
179+ # if it was pressed, add the button to the output string
180+ out_str += f" { button } "
181+
182+ # set the output string into text of the label
183+ # to show it on the display
184+ output_lbls [mouse_index ].text = out_str
0 commit comments