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.