Nintendo's Switch 2 Pro Controllers require special handling to use on Linux. At some point I or somebody else will condense this down into a nicer one-button tool.
# Requirements
Controller *must* be connected via USB. It **does not** work via Bluetooth.
Can *only* be detected by Steam. Will **not work** with non-Steam games or emulators that do not have special built-in support for it.
# Setup
First give the controller access:[^1]
```sh
sudo su
echo 'SUBSYSTEM=="usb", ATTR{idVendor}=="057e", ATTR{idProduct}=="2069", MODE="0666"' > /etc/udev/rules.d/99-nintendo-pro-controller.rules
udevadm control --reload-rules
udevadm trigger
exit
```
Then create this script as `joycon_hid.py`:[^2]
```python
import usb.core # install pyusb first: pip install pyusb
import usb.util
import time
import sys
VENDOR_ID = 0x057E
PRODUCT_IDS = {
0x2066: "Joy-Con (L)",
0x2067: "Joy-Con (R)",
0x2069: "Pro Controller",
0x2073: "GCN Controller"
}
USB_INTERFACE_NUMBER = 1
INIT_COMMAND_0x03 = bytes([0x03, 0x91, 0x00, 0x0d, 0x00, 0x08, 0x00, 0x00, 0x01, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF])
UNKNOWN_COMMAND_0x07 = bytes([0x07, 0x91, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00])
UNKNOWN_COMMAND_0x16 = bytes([0x16, 0x91, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00])
REQUEST_CONTROLLER_MAC = bytes([0x15, 0x91, 0x00, 0x01, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x02, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF])
LTK_REQUEST = bytes([0x15, 0x91, 0x00, 0x02, 0x00, 0x11, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF])
UNKNOWN_COMMAND_0x15_ARG_0x03 = bytes([0x15, 0x91, 0x00, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00])
UNKNOWN_COMMAND_0x09 = bytes([0x09, 0x91, 0x00, 0x07, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])
IMU_COMMAND_0x02 = bytes([0x0c, 0x91, 0x00, 0x02, 0x00, 0x04, 0x00, 0x00, 0x27, 0x00, 0x00, 0x00])
OUT_UNKNOWN_COMMAND_0x11 = bytes([0x11, 0x91, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00])
UNKNOWN_COMMAND_0x0A = bytes([0x0a, 0x91, 0x00, 0x08, 0x00, 0x14, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x35, 0x00, 0x46, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])
IMU_COMMAND_0x04 = bytes([0x0c, 0x91, 0x00, 0x04, 0x00, 0x04, 0x00, 0x00, 0x27, 0x00, 0x00, 0x00])
ENABLE_HAPTICS = bytes([0x03, 0x91, 0x00, 0x0a, 0x00, 0x04, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00])
OUT_UNKNOWN_COMMAND_0x10 = bytes([0x10, 0x91, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00])
OUT_UNKNOWN_COMMAND_0x01 = bytes([0x01, 0x91, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x00])
OUT_UNKNOWN_COMMAND_0x03 = bytes([0x03, 0x91, 0x00, 0x01, 0x00, 0x00, 0x00])
OUT_UNKNOWN_COMMAND_0x0A_ALT = bytes([0x0a, 0x91, 0x00, 0x02, 0x00, 0x04, 0x00, 0x00, 0x03, 0x00, 0x00])
def send_usb_data(ep_out, ep_in, data, description=""):
try:
ep_out.write(data)
time.sleep(0.01)
try:
response = ep_in.read(32, timeout=100)
hex_resp = " ".join([f"{x:02x}" for x in response])
print(f"[{description}] Response: {hex_resp}")
except usb.core.USBError as e:
if e.errno == 110:
print(f"[{description}] No response (Timeout)")
else:
print(f"[{description}] Read Error: {e}")
except usb.core.USBError as e:
print(f"[{description}] Write Error: {e}")
raise
def set_player_leds(ep_out, ep_in, led_mask):
command = [
0x09, 0x91, 0x00, 0x07, 0x00, 0x08, 0x00, 0x00,
led_mask,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
]
send_usb_data(ep_out, ep_in, bytes(command), f"Set LED Mask: 0x{led_mask:02x}")
def connect_usb():
print("Searching for Nintendo Switch Controllers...")
def match_device(dev):
return dev.idVendor == VENDOR_ID and dev.idProduct in PRODUCT_IDS
dev = usb.core.find(custom_match=match_device)
if dev is None:
raise ValueError("Device not found")
product_name = PRODUCT_IDS.get(dev.idProduct, "Unknown Device")
print(f"Found {product_name} (ID: {dev.idProduct:04x})")
if dev.is_kernel_driver_active(USB_INTERFACE_NUMBER):
try:
print("Detaching kernel driver...")
dev.detach_kernel_driver(USB_INTERFACE_NUMBER)
except usb.core.USBError as e:
sys.exit(f"Could not detach kernel driver: {e}")
try:
dev.set_configuration()
print("Configuration set.")
except usb.core.USBError as e:
print(f"Error setting configuration: {e}")
try:
usb.util.claim_interface(dev, USB_INTERFACE_NUMBER)
print(f"Interface {USB_INTERFACE_NUMBER} claimed.")
except usb.core.USBError as e:
sys.exit(f"Could not claim interface: {e}")
cfg = dev.get_active_configuration()
intf = cfg[(USB_INTERFACE_NUMBER,0)]
ep_out = usb.util.find_descriptor(
intf,
custom_match = lambda e: usb.util.endpoint_direction(e.bEndpointAddress) == usb.util.ENDPOINT_OUT)
ep_in = usb.util.find_descriptor(
intf,
custom_match =
lambda e: usb.util.endpoint_direction(e.bEndpointAddress) == usb.util.ENDPOINT_IN
)
if not ep_out:
sys.exit("Could not find OUT endpoint")
print(f"Found Endpoint OUT: 0x{ep_out.bEndpointAddress:02x}")
print("Starting Initialization Sequence...")
try:
send_usb_data(ep_out, ep_in, INIT_COMMAND_0x03, "Init 0x03")
send_usb_data(ep_out, ep_in, UNKNOWN_COMMAND_0x07, "Unknown 0x07")
send_usb_data(ep_out, ep_in, UNKNOWN_COMMAND_0x16, "Unknown 0x16")
send_usb_data(ep_out, ep_in, REQUEST_CONTROLLER_MAC, "Req MAC")
send_usb_data(ep_out, ep_in, LTK_REQUEST, "Req LTK")
send_usb_data(ep_out, ep_in, UNKNOWN_COMMAND_0x15_ARG_0x03, "Unknown 0x15")
send_usb_data(ep_out, ep_in, UNKNOWN_COMMAND_0x09, "Unknown 0x09")
send_usb_data(ep_out, ep_in, IMU_COMMAND_0x02, "IMU 0x02")
send_usb_data(ep_out, ep_in, OUT_UNKNOWN_COMMAND_0x11, "OUT Unknown 0x11")
send_usb_data(ep_out, ep_in, UNKNOWN_COMMAND_0x0A, "Unknown 0x0A")
send_usb_data(ep_out, ep_in, IMU_COMMAND_0x04, "IMU 0x04")
send_usb_data(ep_out, ep_in, ENABLE_HAPTICS, "Enable Haptics")
send_usb_data(ep_out, ep_in, OUT_UNKNOWN_COMMAND_0x10, "OUT Unknown 0x10")
send_usb_data(ep_out, ep_in, OUT_UNKNOWN_COMMAND_0x01, "OUT Unknown 0x01")
send_usb_data(ep_out, ep_in, OUT_UNKNOWN_COMMAND_0x03, "OUT Unknown 0x03")
send_usb_data(ep_out, ep_in, OUT_UNKNOWN_COMMAND_0x0A_ALT, "OUT Unknown 0x0A Alt")
set_player_leds(ep_out, ep_in, 0x0F)
print("Controller initialization sequence complete! All LEDs should be on.")
except Exception as e:
print(f"Error during sequence: {e}")
if __name__ == "__main__":
try:
connect_usb()
except ValueError as e:
print(e)
except Exception as e:
print(f"Unexpected error: {e}")
```
Install its dependencies:
```sh
python3 -m venv venv
./venv/bin/pip install pyusb
```
And run it like this *every time* you reboot of reconnect the controller:
```sh
./venv/bin/python3 joycon_hid.py
```
# References
[^1]: https://nerdburglars.net/question/anyone-figured-out-how-to-connect-switch-2-pro-controllers-to-linux/
[^2]: https://micro.pinapelz.moe/posts/2025-12-04-procon2-hid-tool/