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. The Fruit Jam, or the Metro RP2350 with its HSTX connection for DVI display output are two supported devices.
Metro RP2350 USB Host 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 USB Host breakout cable.
- GND to Black wire
- D+ to Green wire
- D- to White wire
- 5V to Red red
Connect to a DVI compatible display with an HDMI cable from the HSTX breakout to the display.
Fruit Jam Mini Computer
Connecting to the Fruit Jam requires plugging in a USB keyboard to one of the Fruit Jam's USB Host ports.
Connect to a DVI compatible display with an HDMI cable from the Fruit Jam's DVI output to the display.
As of CircuitPython version 10.0.0-beta.3, the wired USB keyboard works on CircuitPython when connected directly to the USB Host pins or port. Prior releases of CircuitPython required connecting 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 September 12, 2025
Text editor powered by tinymce.