Nintendo Switch 2 BLE Protocol Breakdown

GATT services, pairing, input reports, SPI calibration, IMU, charging detection, HD Rumble, and everything else in the Switch 2 BLE protocol.

GameCube Controller with Bluetooth

The Short Version

Nintendo's Switch 2 controllers use Bluetooth Low Energy with a proprietary application-layer pairing protocol. The pairing, command structure, and input report format all happen over standard GATT characteristics. There's nothing OS-specific about it. Any platform with a BLE stack can connect to these controllers.

This post documents the full protocol: GATT service and characteristic UUIDs, the four-step pairing handshake, the 63-byte input report format, SPI flash calibration, IMU data, charging detection, HD Rumble, and analog trigger normalization. We've verified everything against all four controller types: Joy-Con 2 (L and R), Pro Controller 2, and the NSO GameCube controller.

A working implementation is available as a web app that connects directly in Chrome via the Web Bluetooth API. No native code, no extensions. Source 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 Services and Characteristics

All four controller types expose the same two proprietary GATT service UUIDs:

00c5af5d-1964-4e30-8f51-1956f96bd280
ab7de9be-89fe-49ad-828f-118f09df7fd0

These must be known ahead of time on platforms that require service UUID whitelisting (e.g., Web Bluetooth's optionalServices). The characteristics within these services are:

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 GameCube0x2FButtons, sticks, IMU, analog triggers. Using 0x27 disables IMU (bit 3).

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 (uint16 LE, millivolts)
0x211Charging indicator (>0 = charging)
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.

Battery and Charging

Battery voltage at 0x1F is in millivolts. To calculate percentage: (voltage/1000 - 3.0) / 1.2 * 100, clamped to 0-100. The 3.0V floor and 4.2V ceiling represent the typical Li-ion discharge curve.

Byte 0x21 indicates charging state. A value greater than zero means the controller is currently charging. This byte is zero when running on battery.

IMU Orientation

The accelerometer reports gravity-relative orientation. With the controller held normally in front of you:

The gyroscope reports angular velocity:

Raw gyro values of roughly +-4000 correspond to moderate rotation speed. All six IMU values are signed 16-bit integers.

SPI Flash Calibration

The controllers store stick and trigger calibration data in SPI flash. This data can be read during initialization using command 0x02 (SPI read) via the write characteristic (0x0014):

02 91 01 04 00 08 00 00 [len] 7E 00 00 [addr LE 4 bytes]

The response arrives on the command response characteristic with command ID 0x02, payload starting at byte 4.

Stick Calibration Addresses

AddressSizeContent
0x000130800x40Factory calibration, left stick (data at offset 52)
0x000130C00x40Factory calibration, right stick (data at offset 52)
0x001FC0400x40User calibration (left at offset 14, right at offset 46). Overrides factory if present (first byte != 0xFF).

Calibration Data Format

Each stick axis is packed as 9 bytes containing three 12-bit values for X and Y:

x.neutral  = d[0] | ((d[1] & 0x0F) << 8)
y.neutral  = (d[1] >> 4) | (d[2] << 4)
x.rel_max  = d[3] | ((d[4] & 0x0F) << 8)
y.rel_max  = (d[4] >> 4) | (d[5] << 4)
x.rel_min  = d[6] | ((d[7] & 0x0F) << 8)
y.rel_min  = (d[7] >> 4) | (d[8] << 4)

Dead zone is at offset 19 of the calibration block, packed as a 12-bit value: d[19] | ((d[20] & 0x0F) << 8).

To apply calibration:

centered = raw - neutral
range = (centered >= 0) ? rel_max : rel_min
if (abs(centered) < deadzone) return 0
scaled = centered * 32767 / range    // clamp to +-32767

Joy-Con Calibration Quirk

Solo Joy-Cons have only one stick, but the stick data in the input report is always in the correct field (left stick for Joy-Con L, right stick for Joy-Con R). However, the calibration data is stored at the left stick SPI address (0x13080) on both Joy-Con L and Joy-Con R. When applying calibration for a Joy-Con R, use the left SPI calibration data but apply it to the right stick input.

Trigger Calibration

The GameCube analog triggers use default calibration values (from BlueRetro): neutral = 30, max = 195. Normalization:

if (raw <= neutral) return 0
calibrated = (raw - neutral) * 255 / (max - neutral)  // clamp to 0-255

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.

Implementation Notes

The connection flow on any platform is:

  1. Scan for BLE devices with manufacturer ID 0x0553.
  2. Connect and discover the two GATT services. Subscribe to input (0x000A) and command response (0x001A) notifications.
  3. Read calibration from SPI flash (left stick, right stick, user overrides).
  4. Pair using the four-step 0x15 command sequence.
  5. Configure feature flags (sent to 0x0014, not 0x0016), vibration, and LEDs.
  6. Read input from 63-byte notifications on 0x000A.

Host Address

The pairing handshake requires a host Bluetooth MAC address, but testing confirmed the controllers do not validate it against the BLE link-layer address. A fixed placeholder (0xAABBCCDDEEFF) works on all four controller types. This is important for platforms that use randomized private addresses (iOS) or don't expose the real MAC (Web Bluetooth).

Feature Flags Target

The feature flags command (0x0C) must be written to 0x0014 (the write characteristic), not 0x0016 (the vibration+command characteristic). Sending it to 0x0016 silently fails and IMU data won't be enabled. This applies to Pro Controller and GameCube, which use 0x0016 for most other commands.

Web Bluetooth

Web Bluetooth requires service UUIDs declared in optionalServices at scan time. The two service UUIDs listed above are identical across all four controller types. Supported in Chrome, Edge, and Chromium-based browsers on desktop and Android. Not supported in Safari or Firefox.

Try It

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