To use a mouse to draw and move a visible cursor on a display in CircuitPython requires a device that supports both USB Host and displayio. One such device is the Metro RP2350 with its HSTX connection for DVI display output.
Metro RP2350 Wiring
Connecting to the Metro RP2350 USB Host port requires soldering pins to the broken out USB Host connections as shown in this guide page. Make the following connections between the Metro USB Host pins and CH334F host connection opposite the USB C connector.
- GND to GND with Black or Blue
- D+ to D+ with Green
- D- to D- with White
- 5V to 5V with Red
Note that the data pins are swapped on the Metro compared to the CH334F breakout. On the Metro D- is next to 5V, whereas on the CH334F D- is next to GND.
Be sure to connect D- on the breakout to D- on the Metro, and D+ on the breakout to D+ on the Metro.
USB Host is under active development. As of CircuitPython version 10.0.0-alpha.2, the wired USB Mouse only works on CircuitPython when connected through a USB hub such as the CH334F.
# SPDX-FileCopyrightText: 2025 Tim Cocks for Adafruit Industries # SPDX-License-Identifier: MIT """ This example is made for a basic boot mouse with two buttons and a wheel that can be pressed. It assumes there is a single mouse connected to USB Host, and no other devices connected. """ import array from displayio import Group, OnDiskBitmap, TileGrid import supervisor import terminalio import usb.core from adafruit_display_text.bitmap_label import Label import adafruit_usb_host_descriptors display = supervisor.runtime.display # group to hold visual elements main_group = Group() # make the group visible on the display display.root_group = main_group # load the mouse cursor bitmap mouse_bmp = OnDiskBitmap("mouse_cursor.bmp") # make the background pink pixels transparent mouse_bmp.pixel_shader.make_transparent(0) # create a TileGrid for the mouse, using its bitmap and pixel_shader mouse_tg = TileGrid(mouse_bmp, pixel_shader=mouse_bmp.pixel_shader) # move it to the center of the display mouse_tg.x = display.width // 2 mouse_tg.y = display.height // 2 # text label to show the x, y coordinates on the screen output_lbl = Label( terminalio.FONT, text=f"{mouse_tg.x},{mouse_tg.y}", color=0xFFFFFF, scale=1 ) # move it to the upper left corner output_lbl.anchor_point = (0, 0) output_lbl.anchored_position = (1, 1) # add it to the main group main_group.append(output_lbl) # add the mouse tile grid to the main group main_group.append(mouse_tg) # button names # This is ordered by bit position. BUTTONS = ["left", "right", "middle"] # scan for connected USB device and loop over any found for device in usb.core.find(find_all=True): # print device info print(f"{device.idVendor:04x}:{device.idProduct:04x}") print(device.manufacturer, device.product) print(device.serial_number) # try to find mouse endpoint on the current device. mouse_interface_index, mouse_endpoint_address = ( adafruit_usb_host_descriptors.find_boot_mouse_endpoint(device) ) if mouse_interface_index is not None and mouse_endpoint_address is not None: mouse = device print( f"mouse interface: {mouse_interface_index} " + f"endpoint_address: {hex(mouse_endpoint_address)}" ) # detach the kernel driver if needed if mouse.is_kernel_driver_active(0): mouse.detach_kernel_driver(0) # set configuration on the mouse so we can use it mouse.set_configuration() break # buffer to hold mouse data buf = array.array("b", [0] * 8) # main loop while True: try: # attempt to read data from the mouse # 20ms timeout, so we don't block long if there # is no data count = mouse.read(0x81, buf, timeout=20) except usb.core.USBTimeoutError: # skip the rest of the loop if there is no data continue # update the mouse tilegrid x and y coordinates # based on the delta values read from the mouse mouse_tg.x = max(0, min(display.width - 1, mouse_tg.x + buf[1])) mouse_tg.y = max(0, min(display.height - 1, mouse_tg.y + buf[2])) # string with updated coordinates for the text label out_str = f"{mouse_tg.x},{mouse_tg.y}" # loop over the button names for i, button in enumerate(BUTTONS): # check if each button is pressed using bitwise AND shifted # to the appropriate index for this button if buf[0] & (1 << i) != 0: # append the button name to the string to show if # it is being clicked. out_str += f" {button}" # update the text label with the new coordinates # and buttons being pressed output_lbl.text = out_str
Code Explanation
The example code contains comments for the line or section of code that details its purpose. Read the comments along with a summary below to understand how the demo works.
This demo scans for and reads data from the USB mouse in the same way as the basic demo on the previous page. The code uses the default built-in display with supervisor.runtime.display
. For the Metro RP2350, that is the HSTX / DVI connected display.
Visual Elements Setup
The code creates a displayio main_group
to put all visual elements into and sets it as the root_group
on the display so it is shown on the screen. Then it creates an OnDiskBitmap
to load the mouse_cursor.bmp file.
This files contains a pink color in the palette index 0 which is treated as transparency by calling mouse_bmp.pixel_shader.make_transparent(0)
. A TileGrid is created and stored in the variable mouse_tg
. Later, when reading mouse data, the program will use the data to move mouse_tg
around the screen. The mouse cursor is put into the center of the screen to start with.
A Label
named output_lbl
is created and added to the main_group
after being placed in the top left corner of the screen. This will be used to show the current mouse coordinates and any buttons that are pressed.
The mouse_tg
is added to main_group
last so that it will be visually in front of everything else.
Main Loop
Inside the main loop, mouse.read(0x81, buf, timeout=20)
is called to read data from the mouse. 0x81
is the default endpoint address for basic HID mice, buf
is the 8-byte buffer array that will get filled with the data that is read. timeout
is how many milliseconds to wait before raising a USBTimeoutError
if there is no data to read. This code uses a low value of 20
milliseconds to illustrate how the main loop can do other things if the timeout is kept low. Higher timeout values result in the read()
call blocking other code execution for longer times.
If there is no data, the USBTimeoutError
is raised, and the code skips to the next iteration with continue
.
If there is data, the code reads the delta x and y values from buffer indexes 1
and 2
. These delta values represent how far the mouse has moved in each direction. It will have negative values for up/left, and positive values for right/down.
The delta values are used to move the mouse_tg
to a new location. min()
and max()
are used to clamp the mouse cursor to the bounds of the display. The mouse itself is not aware of these bounds, the code enforces staying on the display after reading raw data from the mouse.
The out_str
is updated with the current x and y coordinates of the mouse_tg
.
Next, the code checks for button presses by looping over the BUTTONS
list and checking the bits in the respective positions within the byte at an index 0
of the buffer. Any buttons that are pressed have their name added to the out_str
.
Finally out_str
is set as the text on the output_lbl
to show the current values on the screen.
Page last edited May 06, 2025
Text editor powered by tinymce.