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