Skip to content

caqlayan/procon2-mac

Repository files navigation

procon2-mac

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.

Why this exists

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.

Quick start

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/emulators

Wireless 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 sweep

Usage

procon2 <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 enableprocon2 monitor to verify → play.

Protocol notes

All knowledge comes from community reverse engineering: ikz87/NSW2-controller-enabler, Nohzockt/Switch2-Controllers, dannydarvish/Switch2ProMac.

  • USB IDs: VID 057E, PID 2069 (Pro 2), 2073 (NSO GameCube), 2066/2067 (Joy-Con 2)
  • Interface 0 = HID (silent until enabled), interface 1 = vendor bulk (EP 0x02 OUT / 0x82 IN), 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 at data[16..21] for the right; streamed every ~4ms, all-zero frame stops. (Format from HandHeldLegend's procon2tool.)
  • macOS gotcha: IOHIDDeviceSetReport needs 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

Auto-enable at login

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.

Wireless (Phases 2+3) — how it works

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 an IOUserHIDDevice with the real controller's descriptor + one extra vendor feature report 0x7F used 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), copy ProCon2VirtualPad.app to /Applications, run once, approve in System Settings. Then procon2 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 via ioreg.

Roadmap

  • 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

Credits & disclaimer

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 0x91 request 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): …7FD2 input (notify) · 7492866C… secondary status (notify) · CC483F51…B05 rumble out · 649D4AC9… command channel · 3DACBC7E…B379 rumble+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 BlueRetro sw2.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.

About

Nintendo Switch 2 Pro Controller support for macOS — wired & wireless (Bluetooth LE + DriverKit virtual gamepad), HD rumble, Steam/cloud-streaming ready

Topics

Resources

License

Contributing

Stars

1 star

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors