Connecting Nintendo Switch 2 Controllers to iPhone over BLE

The full Switch 2 BLE protocol: pairing, input reports, rumble, and a working iOS implementation for all four controller types.

GameCube Controller with Bluetooth

The Short Version

Nintendo's Switch 2 controllers use Bluetooth Low Energy. iOS fully supports BLE through the public CoreBluetooth framework. Nintendo's proprietary pairing protocol happens entirely at the application layer: just bytes written to standard GATT characteristics. iOS doesn't know or care that those bytes represent a Nintendo pairing sequence. No private APIs, no jailbreak, no special entitlements.

We built a working iOS app that connects to all four Switch 2 BLE controller types: Joy-Con 2 (L and R), Pro Controller 2, and the NSO GameCube controller. Full input (buttons, sticks, triggers, motion, mouse sensor) and output (HD Rumble, simple motor rumble, LEDs) all working over CoreBluetooth. The source is available on GitHub.

The Controller Family

All four Switch 2 controllers share the same BLE protocol. They advertise with Nintendo's manufacturer ID 0x0553 and are distinguished by product ID in the advertisement data:

ControllerProduct IDFeatures
Joy-Con 2 (R)0x2066Buttons, stick, motion, magnetometer, mouse sensor, HD Rumble
Joy-Con 2 (L)0x2067Buttons, stick, motion, magnetometer, mouse sensor, HD Rumble
Pro Controller 20x2069Buttons, sticks, motion, magnetometer, HD Rumble
NSO GameCube0x2073Buttons, sticks, analog triggers, motion, simple rumble

The pairing protocol, command structure, and 63-byte input report format are identical across all four. The differences are in feature flags, rumble type, and which GATT characteristic is used for commands.

GATT Characteristics

Each controller exposes a set of GATT characteristics. Some are shared across all types, others are controller-specific with unique UUIDs per device:

HandlePurposeShared?
0x0014Command output (init, pairing, features)Yes
0x000AInput reports (63-byte packets via notify)Yes
0x001ACommand response / ACKYes
0x0016Vibration + command combined (rumble, LED)Per-controller UUID
0x001ECommand response #2Per-controller UUID
0x0012HD Rumble output (LRA packets)Per-controller UUID

Joy-Con uses 0x0014 for init and pairing commands. Pro Controller and GameCube use 0x0016 (the combined vibration+command characteristic) for most commands, falling back to 0x0014 if needed. Notifications on 0x000A must be enabled immediately at characteristic discovery.

The Pairing Protocol

Nintendo uses a proprietary application-layer pairing protocol on top of BLE. This is separate from BLE's own pairing and bonding. The protocol uses command 0x15 with four sequential steps, all written to the command characteristic.

Every command follows an 8-byte header:

[cmd] [0x91] [0x01] [subcmd] [0x00] [data_len] [0x00] [0x00] [data...]

0x91 is host-to-device direction. 0x01 is BLE transport.

Step 1: Exchange Addresses (subcmd 0x01)

The host sends two Bluetooth addresses. On a real Switch, these are the console's BT MAC. On iOS, Apple hides the device's Bluetooth MAC address entirely. We discovered that the controller does not validate this address against the BLE link-layer address, so a fixed placeholder works.

15 91 01 01 00 0E 00 00
00 02
[6 bytes: host address, big endian]
[6 bytes: same address, first byte decremented by 1]

Step 2: Exchange Public Keys (subcmd 0x04)

The host sends a fixed 16-byte public key. The controller responds with its own. Both keys are documented in the BlueRetro firmware source.

15 91 01 04 00 11 00 00 00 [16 bytes: public key]

Step 3: Confirm LTK (subcmd 0x02)

The host sends a 16-byte challenge. The Long Term Key is the XOR of both public keys.

15 91 01 02 00 11 00 00 00 [16 bytes: challenge]

Step 4: Finalize (subcmd 0x03)

15 91 01 03 00 01 00 00 00

The full handshake takes about 800ms. After this, the controller is paired.

Joy-Con Store Pairing (0x03)

Joy-Con 2 has an additional step after pairing: the host writes its address and LTK to the controller's storage using command 0x03 subcommand 0x07, followed by 0x03 0x09. Pro Controller and GameCube skip this.

Post-Pairing Initialization

After pairing completes, several commands configure the controller:

Feature Flags (cmd 0x0C)

Sets which input features are enabled. The value varies by controller type:

ControllerFlagsNotes
Joy-Con 20xFFAll features including mouse sensor. Must be 0xFF or the mouse sensor stays off.
Pro Controller 20x2FButtons, sticks, IMU, current
NSO GameCube0x27Buttons, sticks, IMU

A 500ms delay after the feature mask command is critical. Without it, features may not initialize correctly.

LED (cmd 0x09)

Sets the player indicator LEDs using a successive bit pattern: player 1 = 0x01, player 2 = 0x03, player 3 = 0x07, player 4 = 0x0F.

Joy-Con sends this directly on 0x0014. Pro Controller and GameCube embed the LED command in a combined rumble+command packet on 0x0016. The Pro packet is 41 bytes (16 L rumble + 16 R rumble + command). The GameCube packet is 21 bytes (5 rumble state + command).

The 63-Byte Input Report

All four controllers stream the same 63-byte input report on characteristic 0x000A:

OffsetSizeContent
0x003Packet ID (24-bit LE)
0x034Button bitmask (32-bit LE)
0x0A3Left stick (two packed 12-bit values)
0x0D3Right stick (two packed 12-bit values)
0x104Mouse sensor X/Y (int16 each, Joy-Con only)
0x186Magnetometer X/Y/Z (int16 each)
0x1F2Battery voltage
0x282Battery current
0x2E2Temperature
0x306Accelerometer X/Y/Z (int16 each)
0x366Gyroscope X/Y/Z (int16 each)
0x3C1Left trigger (analog, 0-255)
0x3D1Right trigger (analog, 0-255)

Sticks are packed as two 12-bit integers in 3 bytes:

X = byte[0] | ((byte[1] & 0x0F) << 8)
Y = (byte[1] >> 4) | (byte[2] << 4)

Neutral position is 0x800 (2048). Effective ranges differ by controller: the Pro Controller main stick reaches about 1610 units from center, the GameCube main stick about 1225, and the GameCube C-stick about 1120.

The analog triggers at 0x3C-0x3D are 0-255 on the GameCube controller (true analog). On the Pro Controller, the triggers are digital.

The mouse sensor at 0x10-0x13 is active only on Joy-Con 2 when feature flags are set to 0xFF. It reports signed 16-bit X/Y position data when the Joy-Con is held sideways as an air mouse.

Button Bitmask

BitButtonBitButton
31ZL15ZR
30L14R
29SL (L)13SL (R)
28SR (L)12SR (R)
27D-Pad Left11A
26D-Pad Right10B
25D-Pad Up9X
24D-Pad Down8Y
22Chat / C20Home
21Capture19L Stick Click
18R Stick Click17Start / +
16Select / -

Rumble

The Switch 2 controller family has two distinct rumble systems: HD Rumble with LRA motors on the Joy-Con 2 and Pro Controller 2, and a simple on/off ERM motor on the NSO GameCube controller.

HD Rumble Packet Layout (Joy-Con 2, Pro Controller 2)

HD Rumble uses Linear Resonant Actuators with independent left and right motors. Packets are 42 bytes, written to the 0x0012 characteristic:

0x00 1 byte pad Left LRA Motor 16 bytes (1 state + 3 ops) bytes 1-16 Right LRA Motor 16 bytes (1 state + 3 ops) bytes 17-32 pad 9 bytes State 1 byte Op 0 5 bytes Op 1 5 bytes Op 2 5 bytes

State Byte

Each motor block starts with a state byte that controls the transaction and enable state:

tbd en ops_cnt tid (0-F) bit 7 bit 6 bits 5-4 bits 3-0

Active rumble: 0x70 | tid (enable=1, ops_cnt=3). Stop: 0x30 | tid (enable=0, ops_cnt=3). The transaction ID increments 0 through F and wraps.

LRA Operation (5 bytes)

Each operation controls both the low-frequency and high-frequency components of the LRA motor:

LF freq 9 bits (0-511) en LF amplitude 10 bits (0-1023) HF freq 9 bits (0-511) en rsv en HF amplitude 8 bits (0-255) bytes 0-3 (32-bit LE word) byte 4

Default Frequencies

BlueRetro defines per-motor default frequencies that match the physical resonance of each LRA:

MotorLF FrequencyHF Frequency
Left0x100 (256)0x0E1 (225)
Right0x180 (384)0x1E1 (481)

The left and right motors have different resonant frequencies. Sending the same frequency to both produces a richer combined vibration than tuning them identically.

Amplitude Scaling

BlueRetro converts a generic power value (0-255) to LRA amplitudes using these ratios:

The LF component carries most of the perceived vibration intensity. HF adds texture on top.

Idle State

When rumble is inactive, ops should be set to the idle value 0x1e100000 (32-bit) with HF amplitude 0x00. This puts the LRA in a known safe state rather than leaving the previous values in place.

Simple Rumble (NSO GameCube)

The GameCube controller uses a basic on/off ERM motor. No frequency or amplitude control. A 21-byte packet on 0x0016:

byte[0]  = 0x00          // padding
byte[1]  = 0x50 | tid    // state byte (enable + ops_cnt=3 + tid)
byte[2]  = 0x01 or 0x00  // motor on / off
byte[3-20] = 0x00        // padding

Sustained Vibration

Both rumble types require continuous packets. A single packet produces only a brief pulse before the motor spins down. Packets must be sent every 5ms from a dedicated background thread using a high-priority dispatch timer. Using the main thread or a lower-priority timer introduces jitter that causes audible stuttering in the vibration.

iOS Implementation

CoreBluetooth handles all standard BLE operations. The flow is:

  1. Scan for peripherals, filtering by manufacturer ID 0x0553 and product ID in the advertisement data.
  2. Connect and discover services and characteristics. Subscribe to 0x000A and 0x001A notifications immediately at discovery.
  3. Pair using the four-step 0x15 command sequence.
  4. Configure feature flags, vibration sample, and LEDs.
  5. Read input from notification callbacks on 0x000A.

The Fake Address

The biggest unknown was whether the controller would validate the host Bluetooth address against the BLE link-layer address. iOS uses randomized private addresses at the link layer, and Apple provides no API to read the real Bluetooth MAC.

Testing confirmed the controllers do not cross-reference these. A fixed placeholder address passes through the pairing handshake on all four controller types. The controller stores it for reconnection, but since iOS identifies peripherals by a stable CBPeripheral UUID, there's no practical impact.

iOS-Specific Notes

Source Code

The full implementation is available at github.com/bitaxislabs/Switch2BLE. It's a self-contained iOS app written in Swift using CoreBluetooth. Supports all four controller types with a debug UI showing live input state, HD Rumble controls, and a BLE log.

Acknowledgments

This work builds on the protocol documentation from BlueRetro by Jacques Gagnon, which provided the pairing keys, command structures, and controller mappings for the Switch 2 family.

Found an error or have additional findings? Let us know.

Back to Blog