This is a CircuitPython-in-the-middle MIDI filter and visualizer project for Fruit Jam. When you plug a USB MIDI controller into the Fruit Jam, MIDI input events will be shown on the DVI visualizer display and echoed on the usb_midi output port to the Fruit Jam's host computer. You could use this as the starting point for a USB MIDI filtering project.
The note visualizer has a grid of channels and notes to cover the range of a full size piano (midi notes 21 to 108). Dots indicate a note that hasn't played yet. Note-On messages turn the dot into a white rectangle. Note-Off messages turn the rectangle black. It works like a 1-bit heat map showing which notes and channels are active. Pitch bend, CC, aftertouch, etc. get shown as text in the bottom left of the visualizer and echoed to the serial console. USB host port connection status gets shown in the bottom right of the visualizer.
This code was developed and tested on CircuitPython 10.0.0-beta.0 with a pre-release revision B Fruit Jam prototype. Keep in mind that things may change by the time CircuitPython 10.0.0 is released.
Related guide: Fruit Jam Gamepad Tester
Transparency note: Adafruit provided the Fruit Jam rev B prototype board I used for this guide (Thanks Adafruit!).
Demo Video
This video shows my code running with a BeatStep Pro plugged into the Fruit Jam and the Fruit Jam plugged into a Mac. The Mac is running VCV Rack 2 with a simple drum and lead patch. To avoid possible power or ground loop troubles, I used the BeatStep Pro's bundled ground loop fixer dongle along with a separate 2.5A USB power supply. A powered USB hub would also work.
Parts
I used a pre-release revision B Fruit Jam prototype board. A Metro RP2350 might also work, but I haven't tested that. To use a Metro RP2350, you would need to add an HSTX to DVI adapter, solder USB host header pins, wire up a USB hub breakout, and so on. For more information on setting up a Metro RP2350 as a pseudo-Fruit Jam, you can check out some of the recent guides by Tim C. and M. LeBlanc-Williams.
You will also need a MIDI controller with USB MIDI output. I tested this code with an Arturia BeatStep Pro and an Arturia Keystep. Many other types of MIDI controllers will probably also work because the code detects USB MIDI devices by USB device class rather than relying on vendor and product ID.
Depending on the current required by your MIDI controller, a powered USB hub may be helpful. Keep in mind that many cheap USB hubs are not suitable for streaming audio devices. For best results, you want a hub with a chipset that supports "Multi-TT". Ideally, the hub should also be built to avoid backfeeding power to its upstream host port, but it's usually kind of difficult to determine that from product marketing descriptions. I've been using a Tripp Lite U223-004-IND industrial USB 2.0 hub and 5VDC power adapter which work well.
Updating CircuitPython
As I write this (July 17, 2025), CircuitPython 10.0.0-beta.0 is the most recent build for Fruit Jam. I'm not sure what will change by the time 10.0.0 is released. To keep your board up to date, you can use the "DOWNLOAD .UF2 NOW" buttons on the appropriate download page of circuitpython.org:
- Metro RP2350 Download page
- Fruit Jam Download page
To install the UF2 File:
- Connect your board to a computer with a USB data cable (charge-only cables won't work! )
- Press and hold the board's boot button (Button 1 on Fruit Jam)
- Press and release the reset button
- Release the boot button
- When the removable drive named RP2350 appears, copy the UF2 file onto it. After the copy finishes, you should see the RP2350 drive disappear and soon after that a new CIRCUITPY drive should appear.
CircuitPython Code
You can view the code at the samblenny/fruit-jam-usb-midi-tester GitHub repository. To download a zip archive project bundle with the code and all the necessary libraries, use the "Download Project Bundle" button:
Install Project Bundle
To copy the project bundle files to your CIRCUITPY drive:
- Download the project bundle .zip file using the "Download Project Bundle" button above.
- Expand the zip file by opening it, or use unzip in a Terminal. The zip archive should expand to a folder. When you open the folder, it should contain a README.txt file and a CircuitPython 10.x folder.
- Open the CircuitPython 10.x folder and copy all of its contents to your CIRCUITPY drive.
To learn more about copying libraries to your CIRCUITPY drive, check out the CircuitPython Libraries section of the Welcome to CircuitPython! learn guide.
Usage
- Install the project bundle files onto your Fruit Jam board or a suitably prepared Metro RP2350. (This will leave your Fruit Jam connected to a computer by USB)
- Connect an HDMI display or capture card to the Fruit Jam's DVI port.
- [Optional] Connect to the Fruit Jam's USB serial console with a serial monitor program such as
tio,screen, or whatever you normally use for CircuitPython coding. - At this point, you should see a "USB Host scanning bus..." message in the lower right pane of the visualizer screen on the DVI display. Connect your USB MIDI controller to one of the Fruit Jam's host ports (use powered hub if needed). After a few seconds, you should see a message like "USB Host MIDI Device vid:pid ..." in the visualizer. If not, check the serial console for debug messages.
- Use the controller to send MIDI to the Fruit Jam. You should see the visualizer's note grid update for note on and note off messages. Other message types will be displayed in the bottom left pane of the visualizer (CC for Control Change, PB for Pitch Bend, PP for Polyphonic key Pressure, CP for Channel key Pressure, etc.). The first number after the message type is the channel (e.g. "CC 1 ..." for channel 1). The remaining numbers are whatever comes next in the standard MIDI message of that type (note or control number, velocity or value, etc). The MIDI messages will also be echoed to the Fruit Jam's usb_device interface (output to the host computer powering the Fruit Jam).
This code uses the Fruit Jam's USB device MIDI output port to act as a MIDI thru (technically there's also some filtering) for input events received from the controller on the USB host port. So, if you start a software synthesizer on your host computer, it should see MIDI events coming through from the Fruit Jam.
NOTE: To maintain reasonably acceptable latency, this code filters out all System Real-Time messages and some aftertouch messages. Check the code for details.
# SPDX-License-Identifier: MIT
# SPDX-FileCopyrightText: Copyright 2025 Sam Blenny
from board import BUTTON1, CKP, CKN, D0P, D0N, D1P, D1N, D2P, D2N
from digitalio import DigitalInOut, Direction, Pull
import bitmaptools
import displayio
from displayio import Bitmap, Group, Palette, TileGrid
import framebufferio
import gc
from micropython import const
import picodvi
import supervisor
import sys
from terminalio import FONT
from time import sleep
from usb.core import USBError, USBTimeoutError
import usb_host
import usb_midi
from adafruit_display_text import bitmap_label
import adafruit_imageload
from sb_usb_midi import find_usb_device, MIDIInputDevice
def init_display(width, height, color_depth):
"""Initialize the picodvi display
Video mode compatibility:
| Video Mode | Fruit Jam | Metro RP2350 No PSRAM |
| -------------- | --------- | ------------------------ |
| (320, 240, 8) | Yes! | Yes! |
| (320, 240, 16) | Yes! | Yes! |
| (320, 240, 32) | Yes! | MemoryError exception :( |
| (640, 480, 8) | Yes! | MemoryError exception :( |
"""
displayio.release_displays()
gc.collect()
fb = picodvi.Framebuffer(width, height, clk_dp=CKP, clk_dn=CKN,
red_dp=D0P, red_dn=D0N, green_dp=D1P, green_dn=D1N,
blue_dp=D2P, blue_dn=D2N, color_depth=color_depth)
display = framebufferio.FramebufferDisplay(fb)
supervisor.runtime.display = display
return display
def main():
# Configure display with requested picodvi video mode
display = init_display(320, 240, 16)
display.auto_refresh = False
grp = Group(scale=1)
display.root_group = grp
# Load background image and palette from file
(bg_bitmap, bg_palette) = adafruit_imageload.load(
"background.bmp", bitmap=Bitmap, palette=Palette)
bg_tg = TileGrid(bg_bitmap, pixel_shader=bg_palette)
grp.append(bg_tg)
# Text label for input event data
event = bitmap_label.Label(FONT, text="", color=0xFFFFFF, scale=1)
event.line_spacing = 1.0
event.anchor_point = (0, 0)
event.anchored_position = (16, 160) # Bottom left rounded rectangle
grp.append(event)
# Text label for status messages
status = bitmap_label.Label(FONT, text="", color=0xFFFFFF, scale=1)
status.line_spacing = 1.0
status.anchor_point = (0, 0)
status.anchored_position = (172, 160) # Bottom right rounded rectangle
grp.append(status)
# Configure button #1 as input to trigger USB bus re-connect
button_1 = DigitalInOut(BUTTON1)
button_1.direction = Direction.INPUT
button_1.pull = Pull.UP
# Nested function to visualize note on/off event
# - chan: midi channel in 1-16
# - num: note number in range 21-108 (midi range of full size piano)
# - note_on: True for note-on event, False for note-off event
def visualize(chan, num, note_on):
if not ((1 <= chan <= 16) and (21 <= num <= 108)):
return
# Calculate coordinates for a rectangle in the background image's grid
# of channels and notes. These formulas come from measuring pixels
# in an image editor (dot grid spacing is 3px per note, 6px per chan)
x1 = 28 + (3 * (num - 21))
y1 = 16 + (6 * (chan - 1))
color_val = 2 if note_on else 0
bitmaptools.fill_region(bg_bitmap, x1, y1, x1+2, y1+5, color_val)
# Nested function to update status label text
def set_status(msg, log_it=False):
status.text = msg
display.refresh()
if log_it:
print(msg)
# Main loop: scan for usb MIDI device, connect, handle input events
prev_b1 = button_1.value
while True:
set_status("USB Host\n scanning bus...", log_it=True)
event.text = ''
display.refresh()
gc.collect()
device_cache = {}
try:
# This loop will end as soon as it finds a ScanResult object (r)
r = None
while r is None:
sleep(0.4)
r = find_usb_device(device_cache)
# Use ScanResult object to check if USB device descriptor info
# matches the class/subclass/protocol pattern for a MIDI device
dev = MIDIInputDevice(r)
set_status(
"USB Host\n MIDI Device\n vid:pid %04X:%04X\n" % (r.vid, r.pid)
)
# Collect garbage to hopefully limit heap fragmentation. If we're
# lucky, this may help to avoid gc pauses during MIDI input loop.
r = None
device_cache = {}
gc.collect()
# Cache fn and obj references (MicroPython performance boost trick)
fast_wr = sys.stdout.write
refresh = display.refresh
port_out = None
for p in usb_midi.ports:
if isinstance(p, usb_midi.PortOut):
port_out = p
break
# Poll for input until Button #1 pressed or USB error.
# CAUTION: This loop needs to be as efficient as possible. Any
# extra work here directly adds time to USB MIDI read latency.
# The pp_skip and cp_skip variables help with thinning out channel
# and polyphonic key pressure (aftertouch) messages.
SKIP = const(6)
pp_skip = SKIP
cp_skip = SKIP
for data in dev.input_event_generator():
# Check for falling edge of button press (triggers usb re-scan)
if not button_1.value:
if prev_b1:
prev_b1 = False
break
else:
prev_b1 = True
# Handle midi packet (should be None or 4-byte memoryview)
if data is None:
continue
# Begin Parsing Packet
# NOTE: The & 0x0f below is a bitwise logical operation for
# masking off the CN (Cable Number) bits that indicate which
# midi port the message arrived from. Ignoring the cable number
# lets us filter more efficiently.
# For example, on my BeatStep Pro, regular sequencer notes
# and CC happen on CN==0, while CN==1 has "MCU" (Mackie
# Control) messages. When set for "Control Mode" with the
# "MCU/HUI" knob sub-mode, the BSP sends relative knob turn
# amount events for use with DAW software. Ignoring CN lets us
# merge all the midi input streams to filter more efficiently.
cin = data[0] & 0x0f
# Filter out all System Real-Time messages. Sequencer playback
# commonly sends start/stop messages along with _many_ timing
# clocks. Dropping real-time messages conserves CPU to spend on
# handling note and cc messages.
if cin == 0x0f and (0xf8 <= data[1] <= 0xff):
continue
# Handle notes, cc, aftertouch, pitchbend, etc.
chan = (data[1] & 0x0f) + 1
num = data[2] # note or control number
if cin == 0x08:
# Note off
msg = 'Off %d %d %d\n' % (chan, num, data[3])
visualize(chan, num, False) # visualize in note grid
elif cin == 0x09:
# Note on
msg = 'On %d %d %d\n' % (chan, num, data[3])
visualize(chan, num, True) # visualize in note grid
elif cin == 0x0a:
# Polyphonic key pressure (aftertouch)
if pp_skip > 0:
# Ignore some of the polyphonic pressure messages
# because processing them all can destroy our latency
pp_skip -= 1
continue
pp_skip = SKIP
msg = 'PP %d %d %d\n' % (chan, num, data[3])
elif cin == 0x0b:
# CC (control change)
msg = 'CC %d %d %d\n' % (chan, num, data[3])
elif cin == 0x0d:
# Channel key pressure (aftertouch)
if cp_skip > 0:
# Ignore some of the channel pressure messages
# because processing them all can destroy our latency
cp_skip -= 1
continue
cp_skip = SKIP
msg = 'CP %d %d\n' % (chan, num)
elif cin == 0x0e:
# Pitch bend
msg = 'PB %d %d %d\n' % (chan, num, data[3])
else:
# Hexdump other messages: SysEx or whatever
msg = '%02x %02x %02x %02x\n' % tuple(data)
# Echo message upstream to host computer (usb midi device)
if port_out:
port_out.write(data)
# Send message to serial console
fast_wr(msg)
# Visualize non-note messages in text box
if cin != 0x08 and cin != 0x09:
event.text = msg
# Draw the picodvi updates
refresh()
except USBError as e:
# This sometimes happens when devices are unplugged. Not always.
print("USBError: '%s' (device unplugged?)" % e)
show_scan_msg = True
except ValueError as e:
# This can happen if an initialization handshake glitches
print(e)
show_scan_msg = True
main()
# SPDX-License-Identifier: MIT
# SPDX-FileCopyrightText: Copyright 2025 Sam Blenny
#
# Driver for USB MIDI devices.
#
# NOTE: USB MIDI is CPU intensive. To help keep latency low, this code uses
# performance boosting tricks with a special focus on limiting the amount of
# heap allocations. Related docs:
# - https://docs.python.org/3/glossary.html#term-generator
# - https://docs.python.org/3/glossary.html#term-iterable
# - https://docs.micropython.org/en/latest/reference/speed_python.html
#
import gc
from micropython import const
from usb import core
from usb.core import USBError, USBTimeoutError
import sb_usb_descriptor
def find_usb_device(device_cache):
# Find a usb midi device by inspecting usb device descriptors
# - device_cache: dictionary of previously checked device descriptors
# - return: ScanResult object for success or None for failure.
# Exceptions: may raise usb.core.USBError or usb.core.USBTimeoutError
#
for device in core.find(find_all=True):
# Read descriptors to identify devices by type
try:
desc = sb_usb_descriptor.Descriptor(device)
k = str(desc.to_bytes())
if k in device_cache:
return None
# Remember this device to avoid repeatedly checking it later
device_cache[k] = True
# Compare descriptor to expected midi device fingerprint
desc.read_configuration(device)
print(desc)
# Get tuples of class/subclass/protocol for device and interfaces
d = desc.dev_class_subclass()
i0 = desc.int_class_subclass(0)
i1 = desc.int_class_subclass(1)
if d == (0, 0) and i0 == (1, 1) and i1 == (1, 3):
print("interface 0 is Audio Control")
print("interface 1 is MIDI Streaming")
return ScanResult(device, desc)
else:
print("IGNORING UNRECOGNIZED DEVICE")
return None
except ValueError as e:
# This can happen if we get a 0 length device descriptor. Usually
# it works fine to ignore the error and try again.
print(e)
except USBError as e:
print("find_usb_device() USBError: '%s'" % e)
return None
class ScanResult:
def __init__(self, device, descriptor):
self.device = device
self.descriptor = descriptor
self.vid = descriptor.idVendor
self.pid = descriptor.idProduct
self.dev_info = descriptor.dev_class_subclass()
self.int0_info = descriptor.int_class_subclass(0)
self.int1_info = descriptor.int_class_subclass(1)
class MIDIInputDevice:
def __init__(self, scan_result):
# Prepare for reading input events from specified device
# - scan_result: a ScanResult instance
# Exceptions: may raise usb.core.USBError
#
device = scan_result.device
self.device = device
# Make sure CircuitPython core is not claiming the device
interface = 1
if device.is_kernel_driver_active(interface):
print('Detaching interface %d from kernel' % interface)
device.detach_kernel_driver(interface)
# Set configuration
device.set_configuration()
# Figure out which endpoints to use
ins = scan_result.descriptor.input_endpoints(interface)
outs = scan_result.descriptor.output_endpoints(interface)
endpoint_in = None if (len(ins) < 1) else ins[0]
endpoint_out = None if (len(outs) < 1) else outs[0]
self.int1_endpoint_in = endpoint_in
self.int1_endpoint_out = endpoint_out
def input_event_generator(self):
# Read USB input events _as efficiently as possible_.
#
# This is a generator that makes an iterable for reading input events.
# The code structure here is weird because it's using MicroPython
# performance boosting tricks to reduce CPU cycles spent on dictionary
# lookups, function calls, and heap allocations. The goal is to read
# input fast enough to avoid audible latency glitches.
#
# - returns: iterable that can be used with a for loop
# - iterable can yield:
# 1. A memoryview(bytearray(...)) with a 4 byte usb midi packet, or
# 2. None (read timeout, filtered out clock timing packet, etc)
# Exceptions: may raise USBError
#
addr = self.int1_endpoint_in.bEndpointAddress
max_packet = min(64, self.int1_endpoint_in.wMaxPacketSize)
data = bytearray(max_packet)
view = memoryview(data) # using memoryview reduces heap allocations
read = self.device.read # caching function avoids dictionary lookups
ms = const(3) # read timeout
while True:
try:
# In theory, using a positional argument for the timeout should
# be faster than using a `timeout=ms` keyword argument
n = read(addr, data, ms)
# Bulk read result will be 0 or more 4-byte midi packets, so
# split that up into 4-byte memoryview slices
for i in range(0, n, 4):
yield view[i:i+4]
# In case of 0 byte bulk read, we still need to yield something
if n == 0:
yield None
except USBTimeoutError as e:
# This is normal. Timeouts happen fairly often.
yield None
except USBError as e:
# This may happen when device is unplugged (not always though)
raise e
# SPDX-License-Identifier: MIT
# SPDX-FileCopyrightText: Copyright 2025 Sam Blenny
#
# Descriptor parser for USB devices
#
# Related Documentation:
# - https://docs.circuitpython.org/en/latest/shared-bindings/usb/core/index.html
#
from usb import core
from usb.core import USBError, USBTimeoutError
def get_desc(device, desc_type, length=256):
# Read USB descriptor of type specified by desc_type (index always 0).
# - device: a usb.core.Device
# - desc_type: uint8 value for the descriptor type field of wValue
# - returns: bytearray with results from ctrl_transfer()
# Exceptions: may raise USBError or USBTimeoutError
data = bytearray(length)
bmRequestType = 0x80
wValue = (desc_type << 8) | 0
wIndex = 0
device.ctrl_transfer(bmRequestType, 6, wValue, wIndex, data, 300)
return data
def split_desc(data):
# Split a combined descriptor into its individual sub-descriptors
# - data: a bytearray of descriptor data from ctrl_transfer()
# - returns: array of bytearrays (first byte of each is length)
slices = []
cursor = 0
limit = len(data)
data_mv = memoryview(data) # use memoryview to reduce heap allocations
for i in range(limit):
if cursor == limit:
break
length = data[cursor]
if length == 0:
break
if cursor + length > limit:
break
slices.append(data_mv[cursor:cursor+length])
cursor += length
return slices
def dump_desc(data, message=None, indent=1):
# Hexdump a descriptor
# - data: bytearray or [bytes, ...] with descriptor from ctrl_transfer()
# - message: header message to print before the hexdump
# - indent: number of spaces to put at the start of each line
# - returns: hexdump string
arr = [message] if message else []
if isinstance(data, list):
for row in data:
arr.append((' ' * indent) + ' '.join(['%02x' % b for b in row]))
elif isinstance(data, bytearray):
# Wrap hexdump to fit in 80 columns
bytes_per_line = (80 - indent) // 3
data_mv = memoryview(data) # use memoryview to reduce heap allocations
for offset in range(0, len(data_mv), bytes_per_line):
chunk = data_mv[offset:offset+bytes_per_line]
arr.append((' ' * indent) + ' '.join(['%02x' % b for b in chunk]))
else:
print("Unexpected dump_desc arg type %s" % type(data))
return "\n".join(arr)
class ConfigDesc:
def __init__(self, d):
# Parse a configuration descriptor
# - d: bytearray containing a 9 byte configuration descriptor
if len(d) != 9 or d[0] != 0x09 or d[1] != 0x02:
raise ValueError("Bad configuration descriptor")
self.bNumInterfaces = d[4]
self.bConfigurationValue = d[5] # for set_configuration()
self.bMaxPower = d[8] # units are 2 mA
def __str__(self):
return ' Config %d: %d mA, %d interfaces' % (
self.bConfigurationValue, self.bMaxPower * 2, self.bNumInterfaces)
class InterfaceDesc:
def __init__(self, d):
# Parse an interface descriptor
# - d: bytearray containing a 9 byte interface descriptor
if len(d) != 9 or d[0] != 0x09 or d[1] != 0x04:
raise ValueError("Bad interface descriptor")
self.bInterfaceNumber = d[2]
self.bNumEndpoints = d[4]
self.bInterfaceClass = d[5]
self.bInterfaceSubClass = d[6]
self.bInterfaceProtocol = d[7]
self.endpoint = []
def add_endpoint_descriptor(self, data):
self.endpoint.append(EndpointDesc(data))
def __str__(self):
fmt = (' Interface %d: '
'(class, subclass, proto): (0x%02x, 0x%02x, 0x%02x)')
chunks = [fmt % (
self.bInterfaceNumber,
self.bInterfaceClass,
self.bInterfaceSubClass,
self.bInterfaceProtocol)]
for e in self.endpoint:
chunks.append(str(e))
return '\n'.join(chunks)
class EndpointDesc:
def __init__(self, d):
# Parse an endpoint descriptor
# - d: bytearray containing a 7-9 byte endpoint descriptor
if len(d) < 7 or d[0] < 0x07 or d[1] != 0x05:
raise ValueError("Bad endpoint descriptor")
self.bEndpointAddress = d[2]
# bmAttributes low 2 bits: 0:control, 1:iso., 2:bulk, 3:interrupt
self.bmAttributes = d[3]
self.wMaxPacketSize = (d[5] << 8) | d[4]
self.bInterval = d[6]
def attribute_str(self):
a = self.bmAttributes & 0x3
if a == 0:
return 'control'
elif a == 1:
return 'iso'
elif a == 2:
return 'bulk'
elif a == 3:
return 'interrupt'
return ''
def __str__(self):
return ' Endpoint 0x%02x: maxPacket: %d, interval: %d, %s' % (
self.bEndpointAddress,
self.wMaxPacketSize,
self.bInterval,
self.attribute_str())
class Descriptor:
def __init__(self, device):
# Read and parse USB device descriptor
# - device: usb.core.Device
#
device_desc = get_desc(device, 0x01, length=18)
length = device_desc[0]
if length != 18:
raise ValueError('Bad Device Descriptor Length: %d' % length)
d = device_desc
self.device_desc_bytes = d
self.bcdUSB = (d[ 3] << 8) | d[ 2]
self.bDeviceClass = d[4]
self.bDeviceSubClass = d[5]
self.bDeviceProtocol = d[6]
self.bMaxPacketSize0 = d[7]
self.idVendor = (d[ 9] << 8) | d[ 8]
self.idProduct = (d[11] << 8) | d[10]
self.bNumConfigurations = d[17]
# Make an empty placeholder configuration
self.config_desc_list = []
self.configs = []
self.interfaces = []
def vid_pid(self):
return (self.idVendor, self.idProduct)
def dev_class_subclass(self):
# Get device descriptor's class and subclass
return (self.bDeviceClass, self.bDeviceSubClass)
def int_class_subclass(self, interface):
# Get requested interface descriptor's class and subclass
for i in self.interfaces:
if i.bInterfaceNumber == interface:
return (i.bInterfaceClass, i.bInterfaceSubClass)
return (None, None)
def output_endpoints(self, interface):
# Get list of output endpoints for requested interface
arr = []
input_mask = 0x80
for i in self.interfaces:
if i.bInterfaceNumber == interface:
for e in i.endpoint:
if not (e.bEndpointAddress & input_mask):
arr.append(e)
return arr
def input_endpoints(self, interface):
# Get list of input endpoints for interface 0
arr = []
input_mask = 0x80
for i in self.interfaces:
if i.bInterfaceNumber == interface:
for e in i.endpoint:
if (e.bEndpointAddress & input_mask):
arr.append(e)
return arr
def read_configuration(self, device):
# Read and parse USB configuration descriptor
# - device: usb.core.Device
config_desc_list = split_desc(get_desc(device, 0x02, length=256))
if len(config_desc_list) == 0:
raise ValueError("Empty Configuration Descriptor")
self.config_desc_list = config_desc_list
self.configs = []
self.interfaces = []
interface_num = -1
for d in config_desc_list:
if len(d) < 2:
continue
bLength = d[0]
bDescriptorType = d[1]
tag = (bLength << 8) | bDescriptorType
if tag == 0x0902:
# Configuration
self.configs.append(ConfigDesc(d))
elif tag == 0x0904:
# Interface
self.interfaces.append(InterfaceDesc(d))
interface_num += 1
elif 7 <= bLength <= 9 and bDescriptorType == 0x05:
# Endpoint
if interface_num >= 0:
self.interfaces[interface_num].add_endpoint_descriptor(d)
else:
raise ValueError("Found endpoint before interface")
def to_bytes(self):
return self.device_desc_bytes
def __str__(self):
# Pretty print the parsed descriptor elements
# ---
# Change this value to pick which output format style to use.
# The hexdumps are helpful for debugging parser code.
skip_hexdump = True
# ---
if skip_hexdump:
# Short format without hexdumps
fmt = """Descriptor:
vid:pid: %04x:%04x
bcdUSB: 0x%04x
(class, subclass, proto): (0x%02x, 0x%02x, 0x%02x)"""
chunks = [fmt % (
self.idVendor,
self.idProduct,
self.bcdUSB,
self.bDeviceClass,
self.bDeviceSubClass,
self.bDeviceProtocol)]
for lst in [self.configs, self.interfaces]:
for item in lst:
chunks.append(str(item))
return "\n".join(chunks)
else:
# Long format including hexdump of each element
fmt = """Descriptor:
Device Descriptor Bytes:
%s
Config Descriptor Bytes:
%s
bcdUSB: 0x%04x
bDeviceClass: 0x%02x
bDeviceSubClass: 0x%02x
bDeviceProtocol: 0x%02x
bMaxPacketSize0: %d
idVendor: 0x%04x
idProduct: 0x%04x
bNumConfigurations: %d"""
chunks = [fmt % (
dump_desc(self.device_desc_bytes, indent=4),
dump_desc(self.config_desc_list, indent=4),
self.bcdUSB,
self.bDeviceClass,
self.bDeviceSubClass,
self.bDeviceProtocol,
self.bMaxPacketSize0,
self.idVendor,
self.idProduct,
self.bNumConfigurations)]
for lst in [self.configs, self.interfaces]:
for item in lst:
chunks.append(str(item))
return "\n".join(chunks)
This page (Fruit Jam USB Host MIDI Tester) was last updated on July 20, 2025.