The Fruit Jam has a built-in USB hub with two exposed USB Host ports which makes it easy to take input from USB keyboard, mice, and game controllers.
There are two possible ways to read data from a keyboard in CircuitPython. The less complex way is utilizing sys.stdin. Python has the concept of standard input and output streams, similar to those in Linux/Unix and other operating systems. CircuitPython has this capability and through a lot of behind the scenes code, presents a USB keyboard as a sys.stdin input device. Due to the way this works, it is only possible to know when a key is pressed, not how long it remains pressed, nor when it is released.
The sys.stdin method defaults to using the standard US layout for the keyboard. You can set a custom layout using the set_user_keymap()function documented here.
The code to get USB Host Keyboard characters and echo them to serial out is as follows:
# SPDX-FileCopyrightText: 2025 Tim Cocks for Adafruit Industries
#
# SPDX-License-Identifier: MIT
import sys
import supervisor
# main loop
while True:
# check how many bytes are available
available = supervisor.runtime.serial_bytes_available
# if there are some bytes available
if available:
# read data from the keyboard input
c = sys.stdin.read(available)
# print the data that was read
print(c, end="")
For the more advanced and manual method for reading from USB keyboards see the Demo Code: USB Host section of USB Host keyboard Learn Guide.
This example uses the USB Host API to read data from the mouse and prints out the relevant values to the serial console. The CircuitPython USB Host API is made to mimic PyUSB from CPython.
To read data from the mouse, the code must first scan the connected devices and find one with a boot mouse endpoint. Once a mouse device and endpoint are found, the code will try to read data from them in the main loop. If no data is sent, it will timeout and simply keep trying again until data is present. Once it reads data from the mouse, it will print any buttons that are pressed along with the delta X and delta Y values that represent how far the mouse has been moved and in which directions.
The list BUTTONS is created with values "left", "right", and "middle". The order and indexes of these values align with the mouse protocol, which will use bits in the same positions to denote whether each button is being pressed or not.
# SPDX-FileCopyrightText: 2025 Tim Cocks for Adafruit Industries
# SPDX-License-Identifier: MIT
"""
This example is made for a basic mouse with
two buttons and a wheel that can be pressed.
It assumes there is a single mouse connected to USB Host.
"""
import array
import usb.core
import adafruit_usb_host_descriptors
# 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
# string with delta x, y values to print
out_str = f"{buf[1]},{buf[2]}"
# 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}"
print(out_str)
Here is a sample output using the Adafruit mouse. If you have a different mouse, the identification information will likely be different, but the functionality should be the same.
Connect the USB game controller while the microcontroller is unplugged from power. Only power up the microcontroller once the USB host connections are made securely.
Press the 'Download Project Bundle' button below to download a zip file containing the demo code. Connect your computer to your Fruit Jam via a known good data+power USB C cable. Copy code.py and the required libraries to the CIRCUITPY drive on your device which appears when the board is connected to your computer via good USB cable.
The code will scan for connected USB devices, printing out information about each one it finds.
Then it will start a loop reading from the first device it found and printing messages when each of the gamepad buttons are pressed. Check the serial console to see the output.
# SPDX-FileCopyrightText: Copyright (c) 2025 Tim Cocks for Adafruit Industries
#
# SPDX-License-Identifier: MIT
import array
import time
import usb.core
import adafruit_usb_host_descriptors
# Set to true to print detailed information about all devices found
VERBOSE_SCAN = True
BTN_DPAD_UPDOWN_INDEX = 1
BTN_DPAD_RIGHTLEFT_INDEX = 0
BTN_ABXY_INDEX = 5
BTN_OTHER_INDEX = 6
DIR_IN = 0x80
controller = None
if VERBOSE_SCAN:
for device in usb.core.find(find_all=True):
controller = device
print("pid", hex(device.idProduct))
print("vid", hex(device.idVendor))
print("man", device.manufacturer)
print("product", device.product)
print("serial", device.serial_number)
print("config[0]:")
config_descriptor = adafruit_usb_host_descriptors.get_configuration_descriptor(
device, 0
)
i = 0
while i < len(config_descriptor):
descriptor_len = config_descriptor[i]
descriptor_type = config_descriptor[i + 1]
if descriptor_type == adafruit_usb_host_descriptors.DESC_CONFIGURATION:
config_value = config_descriptor[i + 5]
print(f" value {config_value:d}")
elif descriptor_type == adafruit_usb_host_descriptors.DESC_INTERFACE:
interface_number = config_descriptor[i + 2]
interface_class = config_descriptor[i + 5]
interface_subclass = config_descriptor[i + 6]
print(f" interface[{interface_number:d}]")
print(
f" class {interface_class:02x} subclass {interface_subclass:02x}"
)
elif descriptor_type == adafruit_usb_host_descriptors.DESC_ENDPOINT:
endpoint_address = config_descriptor[i + 2]
if endpoint_address & DIR_IN:
print(f" IN {endpoint_address:02x}")
else:
print(f" OUT {endpoint_address:02x}")
i += descriptor_len
# get the first device found
device = None
while device is None:
for d in usb.core.find(find_all=True):
device = d
break
time.sleep(0.1)
# set configuration so we can read data from it
device.set_configuration()
print(
f"configuration set for {device.manufacturer}, {device.product}, {device.serial_number}"
)
# Test to see if the kernel is using the device and detach it.
if device.is_kernel_driver_active(0):
device.detach_kernel_driver(0)
# buffer to hold 64 bytes
buf = array.array("B", [0] * 64)
def print_array(arr, max_index=None, fmt="hex"):
"""
Print the values of an array
:param arr: The array to print
:param max_index: The maximum index to print. None means print all.
:param fmt: The format to use, either "hex" or "bin"
:return: None
"""
out_str = ""
if max_index is None or max_index >= len(arr):
length = len(arr)
else:
length = max_index
for _ in range(length):
if fmt == "hex":
out_str += f"{int(arr[_]):02x} "
elif fmt == "bin":
out_str += f"{int(arr[_]):08b} "
print(out_str)
def reports_equal(report_a, report_b, check_length=None):
"""
Test if two reports are equal. If check_length is provided then
check for equality in only the first check_length number of bytes.
:param report_a: First report data
:param report_b: Second report data
:param check_length: How many bytes to check
:return: True if the reports are equal, otherwise False.
"""
if (
report_a is None
and report_b is not None
or report_b is None
and report_a is not None
):
return False
length = len(report_a) if check_length is None else check_length
for _ in range(length):
if report_a[_] != report_b[_]:
return False
return True
idle_state = None
prev_state = None
while True:
try:
count = device.read(0x81, buf)
# print(f"read size: {count}")
except usb.core.USBTimeoutError:
continue
if idle_state is None:
idle_state = buf[:]
print("Idle state:")
print_array(idle_state[:8], max_index=count)
print()
if not reports_equal(buf, prev_state, 8) and not reports_equal(buf, idle_state, 8):
if buf[BTN_DPAD_UPDOWN_INDEX] == 0x0:
print("D-Pad UP pressed")
elif buf[BTN_DPAD_UPDOWN_INDEX] == 0xFF:
print("D-Pad DOWN pressed")
if buf[BTN_DPAD_RIGHTLEFT_INDEX] == 0:
print("D-Pad LEFT pressed")
elif buf[BTN_DPAD_RIGHTLEFT_INDEX] == 0xFF:
print("D-Pad RIGHT pressed")
if buf[BTN_ABXY_INDEX] == 0x2F:
print("A pressed")
elif buf[BTN_ABXY_INDEX] == 0x4F:
print("B pressed")
elif buf[BTN_ABXY_INDEX] == 0x1F:
print("X pressed")
elif buf[BTN_ABXY_INDEX] == 0x8F:
print("Y pressed")
if buf[BTN_OTHER_INDEX] == 0x01:
print("L shoulder pressed")
elif buf[BTN_OTHER_INDEX] == 0x02:
print("R shoulder pressed")
elif buf[BTN_OTHER_INDEX] == 0x10:
print("SELECT pressed")
elif buf[BTN_OTHER_INDEX] == 0x20:
print("START pressed")
# print_array(buf[:8])
prev_state = buf[:]
Page last edited August 06, 2025
Text editor powered by tinymce.