Skip to content

Commit f94718e

Browse files
authored
Merge pull request #3015 from FoamyGuy/usb_snespad_circuitpython
adding circuitpython example for USB Host with SNES like gamepad
2 parents 071c95b + 0ad935f commit f94718e

File tree

1 file changed

+172
-0
lines changed
  • USB_SNES_Gamepad/CircuitPython_USB_Host

1 file changed

+172
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2025 Tim Cocks for Adafruit Industries
2+
#
3+
# SPDX-License-Identifier: MIT
4+
import array
5+
import time
6+
import usb.core
7+
import adafruit_usb_host_descriptors
8+
9+
# Set to true to print detailed information about all devices found
10+
VERBOSE_SCAN = True
11+
12+
BTN_DPAD_UPDOWN_INDEX = 1
13+
BTN_DPAD_RIGHTLEFT_INDEX = 0
14+
BTN_ABXY_INDEX = 5
15+
BTN_OTHER_INDEX = 6
16+
17+
DIR_IN = 0x80
18+
controller = None
19+
20+
if VERBOSE_SCAN:
21+
for device in usb.core.find(find_all=True):
22+
controller = device
23+
print("pid", hex(device.idProduct))
24+
print("vid", hex(device.idVendor))
25+
print("man", device.manufacturer)
26+
print("product", device.product)
27+
print("serial", device.serial_number)
28+
print("config[0]:")
29+
config_descriptor = adafruit_usb_host_descriptors.get_configuration_descriptor(
30+
device, 0
31+
)
32+
33+
i = 0
34+
while i < len(config_descriptor):
35+
descriptor_len = config_descriptor[i]
36+
descriptor_type = config_descriptor[i + 1]
37+
if descriptor_type == adafruit_usb_host_descriptors.DESC_CONFIGURATION:
38+
config_value = config_descriptor[i + 5]
39+
print(f" value {config_value:d}")
40+
elif descriptor_type == adafruit_usb_host_descriptors.DESC_INTERFACE:
41+
interface_number = config_descriptor[i + 2]
42+
interface_class = config_descriptor[i + 5]
43+
interface_subclass = config_descriptor[i + 6]
44+
print(f" interface[{interface_number:d}]")
45+
print(
46+
f" class {interface_class:02x} subclass {interface_subclass:02x}"
47+
)
48+
elif descriptor_type == adafruit_usb_host_descriptors.DESC_ENDPOINT:
49+
endpoint_address = config_descriptor[i + 2]
50+
if endpoint_address & DIR_IN:
51+
print(f" IN {endpoint_address:02x}")
52+
else:
53+
print(f" OUT {endpoint_address:02x}")
54+
i += descriptor_len
55+
56+
# get the first device found
57+
device = None
58+
while device is None:
59+
for d in usb.core.find(find_all=True):
60+
device = d
61+
break
62+
time.sleep(0.1)
63+
64+
# set configuration so we can read data from it
65+
device.set_configuration()
66+
print(
67+
f"configuration set for {device.manufacturer}, {device.product}, {device.serial_number}"
68+
)
69+
70+
# Test to see if the kernel is using the device and detach it.
71+
if device.is_kernel_driver_active(0):
72+
device.detach_kernel_driver(0)
73+
74+
# buffer to hold 64 bytes
75+
buf = array.array("B", [0] * 64)
76+
77+
78+
def print_array(arr, max_index=None, fmt="hex"):
79+
"""
80+
Print the values of an array
81+
:param arr: The array to print
82+
:param max_index: The maximum index to print. None means print all.
83+
:param fmt: The format to use, either "hex" or "bin"
84+
:return: None
85+
"""
86+
out_str = ""
87+
if max_index is None or max_index >= len(arr):
88+
length = len(arr)
89+
else:
90+
length = max_index
91+
92+
for _ in range(length):
93+
if fmt == "hex":
94+
out_str += f"{int(arr[_]):02x} "
95+
elif fmt == "bin":
96+
out_str += f"{int(arr[_]):08b} "
97+
print(out_str)
98+
99+
100+
def reports_equal(report_a, report_b, check_length=None):
101+
"""
102+
Test if two reports are equal. If check_length is provided then
103+
check for equality in only the first check_length number of bytes.
104+
105+
:param report_a: First report data
106+
:param report_b: Second report data
107+
:param check_length: How many bytes to check
108+
:return: True if the reports are equal, otherwise False.
109+
"""
110+
if (
111+
report_a is None
112+
and report_b is not None
113+
or report_b is None
114+
and report_a is not None
115+
):
116+
return False
117+
118+
length = len(report_a) if check_length is None else check_length
119+
for _ in range(length):
120+
if report_a[_] != report_b[_]:
121+
return False
122+
return True
123+
124+
125+
idle_state = None
126+
prev_state = None
127+
128+
while True:
129+
try:
130+
count = device.read(0x81, buf)
131+
# print(f"read size: {count}")
132+
except usb.core.USBTimeoutError:
133+
continue
134+
135+
if idle_state is None:
136+
idle_state = buf[:]
137+
print("Idle state:")
138+
print_array(idle_state[:8], max_index=count)
139+
print()
140+
141+
if not reports_equal(buf, prev_state, 8) and not reports_equal(buf, idle_state, 8):
142+
if buf[BTN_DPAD_UPDOWN_INDEX] == 0x0:
143+
print("D-Pad UP pressed")
144+
elif buf[BTN_DPAD_UPDOWN_INDEX] == 0xFF:
145+
print("D-Pad DOWN pressed")
146+
147+
if buf[BTN_DPAD_RIGHTLEFT_INDEX] == 0:
148+
print("D-Pad LEFT pressed")
149+
elif buf[BTN_DPAD_RIGHTLEFT_INDEX] == 0xFF:
150+
print("D-Pad RIGHT pressed")
151+
152+
if buf[BTN_ABXY_INDEX] == 0x2F:
153+
print("A pressed")
154+
elif buf[BTN_ABXY_INDEX] == 0x4F:
155+
print("B pressed")
156+
elif buf[BTN_ABXY_INDEX] == 0x1F:
157+
print("X pressed")
158+
elif buf[BTN_ABXY_INDEX] == 0x8F:
159+
print("Y pressed")
160+
161+
if buf[BTN_OTHER_INDEX] == 0x01:
162+
print("L shoulder pressed")
163+
elif buf[BTN_OTHER_INDEX] == 0x02:
164+
print("R shoulder pressed")
165+
elif buf[BTN_OTHER_INDEX] == 0x10:
166+
print("SELECT pressed")
167+
elif buf[BTN_OTHER_INDEX] == 0x20:
168+
print("START pressed")
169+
170+
# print_array(buf[:8])
171+
172+
prev_state = buf[:]

0 commit comments

Comments
 (0)