Native macOS support for the Nintendo Switch 2 Pro Controller (and other
Switch 2 controllers) — no Python, no libusb, no kexts. A single Swift binary
that talks to the controller with Apple's IOUSBHost and IOHIDManager
frameworks.
macOS (as of 26.x) does not support the Switch 2 Pro Controller: it uses Bluetooth LE with a custom Nintendo protocol, and over USB-C it stays silent until it receives a proprietary handshake. This tool sends that handshake so the controller becomes a normal HID device that Steam and other apps can read.
Wired play (no Apple Developer account needed):
swift build -c release
sudo cp .build/release/procon2 /usr/local/bin/ # or /opt/homebrew/bin
procon2 enable # with the controller plugged in via USB-C
procon2 monitor # verify input, then play in Steam/Chrome/emulatorsWireless play additionally needs the DriverKit virtual gamepad — see CONTRIBUTING.md for build/signing steps (requires a paid Apple Developer membership). Once the driver is approved:
procon2 bridge # controller in pairing mode: hold sync until LEDs sweepprocon2 <command>| Command | What it does |
|---|---|
procon2 enable |
Send the init handshake to a USB-connected controller (-v for protocol replies) |
procon2 monitor |
Live decoded view of buttons + sticks (--raw for a hex diff view) |
procon2 daemon |
Stay running and auto-enable any Switch 2 controller that gets plugged in |
procon2 rumble [secs] |
Play an HD-haptic test pattern |
procon2 learn |
Interactive wizard that maps the report layout from button presses |
procon2 steam |
Launch Steam with an SDL mapping for the controller |
procon2 status |
Report USB + HID state |
Typical flow: plug the controller in via USB-C → procon2 enable →
procon2 monitor to verify → play.
All knowledge comes from community reverse engineering: ikz87/NSW2-controller-enabler, Nohzockt/Switch2-Controllers, dannydarvish/Switch2ProMac.
- USB IDs: VID
057E, PID2069(Pro 2),2073(NSO GameCube),2066/2067(Joy-Con 2) - Interface 0 = HID (silent until enabled), interface 1 = vendor bulk
(EP
0x02OUT /0x82IN), interfaces 2-4 = USB audio (headphone jack) - Init = 17 commands on the bulk OUT pipe; replies echo the command ID
with status
0x01 - Input report (ID
0x09, ~60 Hz), payload offsets after the ID byte:[0]frame counter,[2..4]button bits (measured on hardware — see below)[5..7]left stick,[8..10]right stick — packed 12-bit X/Y, center ≈ 2048[16..17]timer,[22..]IMU (gyro/accel)
- Output report ID
0x02(63 bytes) on the HID interface carries HD-haptic frames:data[0] = 0x50|counter(4-bit rolling counter),data[1..5]= 5-byte encoded frame for the left actuator, mirrored atdata[16..21]for the right; streamed every ~4ms, all-zero frame stops. (Format from HandHeldLegend's procon2tool.) - macOS gotcha:
IOHIDDeviceSetReportneeds the buffer to INCLUDE the report-ID byte for numbered reports — without it the firmware silently drops the report while the call still returns success. - The HID report descriptor declares a standard Game Pad collection: 21 buttons in bit order + X/Y/Rx/Rz 12-bit axes. The D-pad is buttons, not a hat switch.
Button field, measured with procon2 learn (differs from the BLE layout!):
| Byte | bit0 | bit1 | bit2 | bit3 | bit4 | bit5 | bit6 | bit7 |
|---|---|---|---|---|---|---|---|---|
[2] |
B | A | Y | X | R | ZR | + | RStick |
[3] |
D-Down | D-Right | D-Left | D-Up | L | ZL | − | LStick |
[4] |
Home | Capture | GR | GL | C |
A LaunchAgent (~/Library/LaunchAgents/com.procon2.daemon.plist) runs
procon2 daemon at login so the controller is enabled the moment it is
plugged in. Logs: ~/Library/Logs/procon2.log. Disable with
launchctl bootout gui/$UID/com.procon2.daemon.
ProCon 2 ──BLE──▶ procon2 bridge ──feature report 0x7F──▶ VirtualPadDriver.dext
(controller) (user process) (virtual "Switch 2 Pro
▲ Controller", VID 057E
└──rumble via GetReport(0x7F)◀── PID 2069) ──▶ games
- BLE: the controller's custom Nintendo GATT service
(
AB7DE9BE-…-7FD0); subscribe to the notify characteristic and input streams immediately. BLE report: buttons u32-LE at[4..7](different bit order than USB!), sticks packed 12-bit at[10..15]. - Virtual gamepad: a DriverKit extension (
VirtualPad/) publishes anIOUserHIDDevicewith the real controller's descriptor + one extra vendor feature report0x7Fused as control channel.SetReport(feature 0x7F)wraps an input report to publish;GetReport(feature 0x7F)drains rumble output from games. This avoids DriverKit user clients entirely — both user-client entitlements (allow-any-userclient-access,userclient-access) require Apple approval, but plain HID report I/O needs none. - Setup: build
VirtualPad/VirtualPad.xcodeproj(paid Apple Developer team needed for DriverKit signing; the HID family capabilities auto-provision), copyProCon2VirtualPad.appto/Applications, run once, approve in System Settings. Thenprocon2 bridge— no sudo. - Gotchas discovered: the embedded dext's folder name must equal its bundle
identifier; sysextd's upgrade-replace flow can wedge (fresh bundle ID
sidesteps it); dexts log invisibly to
log show— debug viaioreg.
- Phase 1 — USB: enable handshake, input decoding, Steam (wired)
- Rumble over USB: HD-haptic streaming via output report 0x02
- Phase 2 — Bluetooth LE: CoreBluetooth client, wireless input
- Phase 3 — Virtual gamepad: DriverKit device; wireless play in
Steam, browsers (cloud streaming), emulators via
procon2 bridge - Wireless HD rumble: games' 5-byte LRA haptic frames pass through the virtual pad to the BLE rumble characteristic verbatim (8ms poll cadence, idle-frame safety stop). Feels identical to wired.
- Gyro/IMU forwarding over the bridge
- Battery level reporting
Protocol knowledge builds on
ikz87/NSW2-controller-enabler,
Nohzockt/Switch2-Controllers,
HandheldLegend/procon2tool, and
especially darthcloud/BlueRetro —
plus measurements from real hardware documented here and in
procon2-mapping.json.
This project is not affiliated with or endorsed by Nintendo. "Nintendo Switch" is a trademark of Nintendo. Use at your own risk; poking undocumented firmware commands can change controller state (see CONTRIBUTING.md).
Licensed under the MIT License.
- Player LED over BLE — solved via BlueRetro's
sw2 implementation (the authoritative SW2 BLE reference). Key facts:
- BLE commands use the same IDs as USB and the same
0x91request marker; only the interface byte differs (0x00=USB,0x01=BLE). SET_LED =09 91 01 07 00 08 00 00 <bitmask>. - The Nintendo service channel map (by ATT handle order):
…7FD2input (notify) ·7492866C…secondary status (notify) ·CC483F51…B05rumble out ·649D4AC9…command channel ·3DACBC7E…B379rumble+command combo ·C765A961…command ACKs. - Rumble uses LRA op frames (
sw2_lra_ops_t: freq/amp packed ops, ~4ms cadence) on the rumble characteristic — see BlueRetrosw2.h. - Beware: other community scripts send
30 01 00 30 …as "SET_LED" to the wrong characteristic — that's misread protocol (0x01 is the response marker) and produces undefined behavior.
- BLE commands use the same IDs as USB and the same