The Match3 game uses two different USB mice for input from the players, each player gets their own mouse. This builds on top of the basic code for handling a single mouse. If you are unfamiliar with the concepts involved, first read the basic single Mouse Input guide page then return here.
To use two mice, start with with the example below. It illustrates finding the connected mice, initializing them, reading data, and moving two cursors around the display.
The basic mouse example is embedded below. Click the 'Download Project Bundle' button to download a zip containing this example and the associated image file and required libraries. Unzip and copy them to the CIRCUITPY drive on the Metro which shows up in your computer file application (Explorer or Finder) to run the example.
There is active development work underway for USB Host support. If you are having trouble with your mice, try upgrading your device to CircuitPython 10.0.0-alpha.6 or newer.
# SPDX-FileCopyrightText: Copyright (c) 2025 Tim Cocks for Adafruit Industries # # SPDX-License-Identifier: MIT import array import supervisor import terminalio import usb.core from adafruit_display_text.bitmap_label import Label from displayio import Group, OnDiskBitmap, TileGrid, Palette import adafruit_usb_host_descriptors # use the default built-in display, # the HSTX / PicoDVI display for the Metro RP2350 display = supervisor.runtime.display # a group to hold all other visual elements main_group = Group() # set the main group to show on the display display.root_group = main_group # load the cursor bitmap file mouse_bmp = OnDiskBitmap("mouse_cursor.bmp") # lists for labels, mouse tilegrids, and palettes. # each mouse will get 1 of each item. All lists # will end up with length 2. output_lbls = [] mouse_tgs = [] palettes = [] # the different colors to use for each mouse cursor # and labels colors = [0xFF00FF, 0x00FF00] for i in range(2): # create a palette for this mouse mouse_palette = Palette(3) # index zero is used for transparency mouse_palette.make_transparent(0) # add the palette to the list of palettes palettes.append(mouse_palette) # copy the first two colors from mouse palette for palette_color_index in range(2): mouse_palette[palette_color_index] = mouse_bmp.pixel_shader[palette_color_index] # replace the last color with different color for each mouse mouse_palette[2] = colors[i] # create a TileGrid for this mouse cursor. # use the palette created above mouse_tg = TileGrid(mouse_bmp, pixel_shader=mouse_palette) # move the mouse tilegrid to near the center of the display mouse_tg.x = display.width // 2 - (i * 12) mouse_tg.y = display.height // 2 # add this mouse tilegrid to the list of mouse tilegrids mouse_tgs.append(mouse_tg) # add this mouse tilegrid to the main group so it will show # on the display main_group.append(mouse_tg) # create a label for this mouse output_lbl = Label(terminalio.FONT, text=f"{mouse_tg.x},{mouse_tg.y}", color=colors[i], scale=1) # anchored to the top left corner of the label output_lbl.anchor_point = (0, 0) # move to op left corner of the display, moving # down by a static amount to static the two labels # one below the other output_lbl.anchored_position = (1, 1 + i * 13) # add the label to the list of labels output_lbls.append(output_lbl) # add the label to the main group so it will show # on the display main_group.append(output_lbl) # lists for mouse interface indexes, endpoint addresses, and USB Device instances # each of these will end up with length 2 once we find both mice mouse_interface_indexes = [] mouse_endpoint_addresses = [] mice = [] # scan for connected USB devices for device in usb.core.find(find_all=True): # check for boot mouse endpoints on this device mouse_interface_index, mouse_endpoint_address = ( adafruit_usb_host_descriptors.find_boot_mouse_endpoint(device) ) # if a boot mouse interface index and endpoint address were found if mouse_interface_index is not None and mouse_endpoint_address is not None: # add the interface index to the list of indexes mouse_interface_indexes.append(mouse_interface_index) # add the endpoint address to the list of addresses mouse_endpoint_addresses.append(mouse_endpoint_address) # add the device instance to the list of mice mice.append(device) # print details to the console print(f"mouse interface: {mouse_interface_index} ", end="") print(f"endpoint_address: {hex(mouse_endpoint_address)}") # detach device from kernel if needed if device.is_kernel_driver_active(0): device.detach_kernel_driver(0) # set the mouse configuration so it can be used device.set_configuration() # This is ordered by bit position. BUTTONS = ["left", "right", "middle"] # list of buffers, will hold one buffer for each mouse mouse_bufs = [] for i in range(2): # Buffer to hold data read from the mouse mouse_bufs.append(array.array("b", [0] * 8)) def get_mouse_deltas(buffer, read_count): """ Given a buffer and read_count return the x and y delta values :param buffer: A buffer containing data read from the mouse :param read_count: How many bytes of data were read from the mouse :return: tuple x,y delta values """ if read_count == 4: delta_x = buffer[1] delta_y = buffer[2] elif read_count == 8: delta_x = buffer[2] delta_y = buffer[4] else: raise ValueError(f"Unsupported mouse packet size: {read_count}, must be 4 or 8") return delta_x, delta_y # main loop while True: # for each mouse instance for mouse_index, mouse in enumerate(mice): # try to read data from the mouse try: count = mouse.read( mouse_endpoint_addresses[mouse_index], mouse_bufs[mouse_index], timeout=10 ) # if there is no data it will raise USBTimeoutError except usb.core.USBTimeoutError: # Nothing to do if there is no data for this mouse continue # there was mouse data, so get the delta x and y values from it mouse_deltas = get_mouse_deltas(mouse_bufs[mouse_index], count) # update the x position of this mouse cursor using the delta value # clamped to the display size mouse_tgs[mouse_index].x = max( 0, min(display.width - 1, mouse_tgs[mouse_index].x + mouse_deltas[0]) ) # update the y position of this mouse cursor using the delta value # clamped to the display size mouse_tgs[mouse_index].y = max( 0, min(display.height - 1, mouse_tgs[mouse_index].y + mouse_deltas[1]) ) # output string with the new cursor position out_str = f"{mouse_tgs[mouse_index].x},{mouse_tgs[mouse_index].y}" # loop over possible button bit indexes for i, button in enumerate(BUTTONS): # check each bit index to determin if the button was pressed if mouse_bufs[mouse_index][0] & (1 << i) != 0: # if it was pressed, add the button to the output string out_str += f" {button}" # set the output string into text of the label # to show it on the display output_lbls[mouse_index].text = out_str
The code contains comments describing the purpose of each section. An overview of the code functionality can be found below. Reading both will give you a good understanding of one of the core principals that will get used by the Match3 game.
Default Display
This demo code uses the built-in display which is the HSTX ribbon connector PicoDVI display for the Metro RP2350. In older versions of CircuitPython, the display was not automatically initialized. If you get errors dealing with None
trying to access the display size or setting its root_group
, then update your Metro RP2350 to the latest stable release of CircuitPython listed on the downloads page.
Visual Elements Setup
main_group
is a Group
created to hold all of the other visual elements, it gets set as display.root_display
. mouse_bmp
holds the loaded mouse cursor bitmap image file. For the rest of the display related objects there will be two of each type, one for each of the mice. The objects are held in lists that will contain 2 elements each after the setup is complete. output_lbls
list holds the Label
objects that are used to display the mouse positions and buttons pressed. mouse_tgs
list holds the TileGrid
objects that will draw the visual mouse cursors. palettes
list holds a Palette
object for each cursor. The palettes both contain the same colors as the mouse cursor bitmap for indexes 0
and 1
, and each has their own color at index 2
which is the color that the outline of the cursor will be drawn with. In the demo code the colors are pink and green.
Finding Connected Mice
There are three additional lists declared that will hold two elements, one for each of the USB mice found connected via the USB Host API. mouse_interface_indexes
list will hold the interface index within connected USB device. mouse_endpoint_addresses
will hold the address of the mouse endpoint within the USB device. Finally, the mice
list will hold a USB Device
object for each mouse which will be used to read data from that device during the main loop.
The code scans for connected USB devices with usb.core.find(find_all=True)
and iterates over the returned device objects. Each device object is passed to adafruit_usb_host_descriptors.find_boot_mouse_endpoint()
to check if it represents a boot mouse and find its interface index and endpoint address if so. If the device is found to be a mouse the appropriate values and object will get added to the respective lists.
For each mouse device object it is also detached from the kernel if needed, and its configuration is set so that data can be read from it.
Mouse Deltas Helper Function
The code defines a helper function get_mouse_deltas()
which accepts a buffer
and a read_count
as arguments. This function will check the appropriate indexes within the buffer based on the read_count
and return the delta x and y values contained within the buffer. These values represent how far the mouse has moved, and in which directions.
Main Loop
Inside the main loop is a for
loop that iterates over each mouse device object. Inside of that, the code attempts to read data from the mouse device object. If there is no data to read, the read()
call will raise the USBTimeoutError
which is caught by the code and causes it to skip the rest of the loop and go on to check the next mouse object.
If there was data read, the x and y delta values will be found using the helper function and then the mouse TileGrid
has its x
and y
properties updated based on the delta values, clamped to the display size.
Page last edited May 20, 2025
Text editor powered by tinymce.