Skip to content

Latest commit

ย 

History

25 Commits

Folders and files

NameName
Last commit message
Last commit date
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

๐ŸŽฎ Switch 2 Pro Controller โ€” macOS BLE Bridge

The first working Bluetooth LE client for the Nintendo Switch 2 Pro Controller on macOS.

A Python menubar app that connects to the Switch 2 Pro Controller via BLE and translates inputs to keyboard presses for use with emulators like Ryujinx.

CI macOS Python License


โš ๏ธ What This Is (and Isn't)

This is NOT a system driver. It won't make your controller appear in System Preferences or work natively with games.

This IS:

  • โœ… A BLE client that reads controller inputs via Bluetooth Low Energy
  • โœ… A keyboard bridge that converts inputs to key presses for Ryujinx
  • โœ… A reference implementation for the Switch 2 Pro Controller BLE protocol

๐Ÿš€ Features

  • โœ… Full button mapping โ€” all buttons, triggers, D-pad working
  • โœ… Analog sticks โ€” read with 12-bit precision (converted to 8 directions, see Limitations)
  • โœ… Grip buttons โ€” Switch 2 exclusive GL/GR buttons supported
  • โœ… Ryujinx compatible โ€” keyboard bridge for emulator support
  • โœ… No pairing required โ€” bypasses macOS Bluetooth limitations
  • โœ… Auto-reconnect โ€” if the controller sleeps or drops, the bridge retries for 60 s
  • โœ… C button โ€” the Switch 2's new C button can be mapped (experimental)
  • โœ… DSU server (cemuhook) โ€” true analog sticks in Dolphin, Cemu & other DSU clients, no driver needed
  • โœ… Start at Login โ€” one click in the menubar (bundled .app, macOS 13+)

๐Ÿค” Why This Exists

The Nintendo Switch 2 Pro Controller (Product ID: 0x2069) doesn't work with macOS natively:

Method Status Problem
USB โŒ Firmware blocks non-Switch connections
Bluetooth Classic โŒ macOS can't discover/pair with it
Bluetooth LE โœ… Works with custom BLE client (this project)

This bridge connects via BLE using the bleak library, reads the raw input data, and converts it to keyboard presses that Ryujinx can use.

๐Ÿ“‹ Requirements

  • macOS Ventura (13.0) or later
  • Python 3.9+
  • Nintendo Switch 2 Pro Controller

๐Ÿ”ง Run from source

git clone https://github.com/mlstr0m/switch2bridge-macos.git
cd switch2bridge-macos

python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt

python Switch2Bridge.py

macOS will ask for two permissions:

  1. Accessibility โ€” prompted at first launch (to simulate keyboard input)
  2. Bluetooth โ€” prompted the first time you click Connect Controller (not at launch!)

โš ๏ธ When running from source, the Bluetooth permission belongs to Terminal (or your Python interpreter), not to the app. If no prompt ever appears, add/enable Terminal manually in System Settings โ†’ Privacy & Security โ†’ Bluetooth, then relaunch. The app detects a denied permission and offers to open the right settings pane.

๐Ÿ“ฆ Build a standalone .app + DMG

chmod +x build_dmg.sh
./build_dmg.sh

The installer lands at dist/Switch2Bridge-Installer.dmg. Manual build:

python setup_app.py py2app
# โ†’ dist/Switch2 Bridge.app

๐ŸŽฏ Usage

  1. Launch the app โ€” a ๐ŸŽฎ appears in the menu bar
  2. Click โ†’ Connect Controller
  3. Wait for ๐ŸŸข (connected)
  4. Open Ryujinx โ†’ Options โ†’ Settings โ†’ Input
    • Input Device: Keyboard
    • Controller Type: Pro Controller
    • Map keys using the table below

๐ŸŽฎ Button Mapping

Default mapping:

Button Key Button Key
A Z L Q
B X R E
X C ZL 1
Y V ZR 3
+ P LS (click) F
- M RS (click) G
Home H GL (grip) 9
Capture O GR (grip) 0
D-Pad Key Stick Keys
Up โ†‘ Left Stick WASD
Down โ†“ Right Stick IJKL
Left โ†
Right โ†’

Custom mappings

The first time you launch the app it writes a JSON config to:

~/Library/Application Support/Switch2Bridge/mappings.json

Edit it to remap any button or stick direction, then Reload mappings from the menubar (or restart the app). The menubar also has Edit mappings fileโ€ฆ which reveals the file in Finder.

Each value is either a single character ("a", "5", "."), null to leave a button unmapped, or a named key in angle brackets: <up>, <down>, <left>, <right>, <space>, <enter>, <esc>, <tab>, <backspace>, <delete>, <home>, <end>, <pageup>, <pagedown>, <shift>, <ctrl>, <alt>, <cmd>, and <f1> โ€ฆ <f20>.

The Switch 2's new C button is supported as "C" (unmapped by default โ€” set it to any key to use it).

The "ble" section holds one advanced setting, input_char: leave it null to auto-detect the controller's input characteristic (the bridge fills it in itself once detected), or set a 128-bit UUID to force one. See BLE Characteristics.

Invalid JSON falls back to defaults and the menubar surfaces the parse error. Unknown button names or stick directions (typos) are reported via a notification instead of being silently ignored. Two inputs may share the same key: the key is only released once both are released.

๐Ÿ•น๏ธ DSU server โ€” true analog sticks (no driver)

The app runs a cemuhook/DSU server (default 127.0.0.1:26760), which exposes the controller as a full gamepad over UDP โ€” analog sticks included, bypassing the keyboard bridge's 8-direction limitation.

  • Dolphin โ€” Options โ†’ Controller Settings โ†’ Alternate Input Sources โ†’ enable DSU Client, add 127.0.0.1:26760. The pad then appears as an input device with analog axes.
  • Cemu โ€” Input settings โ†’ add a DSUController with the same address.
  • Ryujinx โ€” uses DSU for motion only (Settings โ†’ Input โ†’ enable Motion โ†’ Use CemuHook compatible motion). Buttons/sticks still go through the keyboard bridge. Motion data itself is not decoded yet (sent as zeros).

Configure in mappings.json:

"dsu": { "enabled": true, "host": "127.0.0.1", "port": 26760 }

or toggle it from the menubar (DSU server item โ€” the checkmark shows it's listening). Button mapping on the DSU side is positional: Aโ†’Circle, Bโ†’Cross, Xโ†’Triangle, Yโ†’Square, โˆ’โ†’Share, +โ†’Options, Homeโ†’PS, Captureโ†’Touch. GL/GR/C have no DSU equivalent.

๐Ÿ“ Project Structure

switch2bridge-macos/
โ”œโ”€โ”€ Switch2Bridge.py    # Menubar app (BLE client + keyboard bridge)
โ”œโ”€โ”€ dsu_server.py       # DSU (cemuhook) server โ€” analog output for emulators
โ”œโ”€โ”€ setup_app.py        # py2app configuration
โ”œโ”€โ”€ build_dmg.sh        # Automated build script (.app + DMG)
โ”œโ”€โ”€ requirements.txt    # Python dependencies
โ”œโ”€โ”€ tests/
โ”‚   โ””โ”€โ”€ test_bridge.py  # Headless tests (mappings, key dispatch, BLE lifecycle)
โ”œโ”€โ”€ AppIcon.icns        # Application icon (used by py2app)
โ”œโ”€โ”€ LICENSE
โ””โ”€โ”€ README.md

๐Ÿ”ฌ Technical Details

How It Works

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”     BLE      โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    pynput    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Switch 2 Pro   โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ถ  โ”‚  Python Bridge  โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ถ  โ”‚    Ryujinx      โ”‚
โ”‚   Controller    โ”‚   (bleak)    โ”‚                 โ”‚  (keyboard)  โ”‚   (Keyboard)    โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜              โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜              โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
  1. BLE Connection โ€” uses bleak to connect directly via Bluetooth LE
  2. Input Parsing โ€” decodes the proprietary Nintendo protocol
  3. Keyboard Simulation โ€” uses pynput to simulate key presses
  4. Ryujinx โ€” reads keyboard input as if from a physical keyboard

BLE Characteristics

UUID Purpose
7492866c-ec3e-4619-8258-32755ffcc0f9 Input reports (notifications)
7492866c-ec3e-4619-8258-32755ffcc0f8 Output (LED, rumble โ€” not working)

Not every controller exposes that input UUID (see #15), so the bridge treats it as a first guess only:

  1. the UUID pinned in mappings.json (ble.input_char), if it is present on the device;
  2. otherwise the documented UUID above;
  3. otherwise it probes every other notifiable characteristic โ€” vendor UUIDs first, SIG-assigned ones last โ€” subscribing for 3 s each and keeping the first one that streams reports of at least 11 bytes (enough to decode buttons and both sticks). The winner is written back to ble.input_char, so later connections skip the probing.

Every connection also logs the full GATT table (GATT: N characteristic(s): โ€ฆ) to ~/Library/Logs/Switch2Bridge/bridge.log โ€” that line is what a bug report needs when a controller can't be identified.

๐Ÿฉบ Troubleshooting

  • The controller never appears in System Settings โ†’ Bluetooth โ€” that's expected, and not a failure. This bridge is a BLE client: there is no system-level pairing, so macOS will never list the controller. The only place to watch is the app's menubar icon (๐Ÿ” โ†’ ๐ŸŸข).
  • "Controller not found" โ€” make sure the controller is not paired with a console nearby (unpair it or put the console to sleep far away). Click Connect Controller first โ€” the search now runs for 30 s โ€” then hold the small pair button on the back until the LEDs sweep back and forth.
  • No Bluetooth prompt ever appeared (run-from-source) โ€” the permission belongs to Terminal/Python, not the app. Check System Settings โ†’ Privacy & Security โ†’ Bluetooth and enable Terminal, then relaunch. Without it, scans silently find nothing.
  • "Characteristic โ€ฆ was not found" / "no readable input characteristic" โ€” the controller connected but its input-report characteristic isn't where the bridge expects it. Since v1.2.4 the bridge probes the alternatives automatically and remembers what worked, so retry once โ€” and move the sticks while it says it is identifying the controller, in case that revision only reports on change. If it still gives up, the log now contains a GATT: line listing every service and characteristic of your controller โ€” attach it to an issue. You can also pin a UUID yourself: "ble": { "input_char": "โ€ฆ" } in mappings.json.
  • Menubar says ๐ŸŸข connected but inputs don't reach the emulator โ€” macOS Accessibility permission is missing. Grant it in System Settings โ†’ Privacy & Security โ†’ Accessibility, then relaunch the app. (The app should also pop an alert about this on first launch.)
  • Logs โ€” written to ~/Library/Logs/Switch2Bridge/bridge.log. Open a terminal and tail -f it to watch what's happening in real time.

๐Ÿšง Limitations

Feature Status Notes
Buttons โœ… Working All buttons mapped
C button ๐Ÿงช Experimental Parsed as byte 4, bit 0x02 โ€” please report if it doesn't fire
Analog Sticks โœ… Analog via DSU Full 12-bit analog through the DSU server (Dolphin/Cemu). The keyboard bridge remains digital: thresholded (with hysteresis) to 8 directions (WASD/IJKL).
LED Control โŒ Not working Output characteristic doesn't respond
Rumble โŒ Not working Same issue
Motion/Gyro โš ๏ธ Plumbing ready DSU motion fields are sent (as zeros) โ€” the gyro bytes in the BLE report are not decoded yet
Native HID โŒ Not possible Would require DriverKit (kernel-level)

๐Ÿค Contributing

Contributions welcome! Areas that need work:

  1. LED/Rumble โ€” figure out the output protocol (likely a Joy-Con-style handshake)
  2. Motion controls โ€” decode gyro/accelerometer data
  3. True analog โ€” virtual HID device via DriverKit
  4. Cross-platform โ€” Linux/Windows ports

๐Ÿ“œ Credits

๐Ÿ“„ License

MIT License โ€” see LICENSE for details.


โญ Star this repo if it helped you!
First macOS BLE bridge for Switch 2 Pro Controller โ€” January 2026

About

Bluetooth bridge for the Nintendo Switch 2 Pro Controller on macOS.

Topics

Resources

Stars

27 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages