-
Notifications
You must be signed in to change notification settings - Fork 29
feat(switch2_pro): Nintendo Switch 2 Pro Controller BLE emulation #765
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
finger563
wants to merge
27
commits into
main
Choose a base branch
from
feat/switch2-pro
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
27 commits
Select commit
Hold shift + click to select a range
623e7c0
feat(switch2_pro): GATT + pairing skeleton for Switch 2 Pro Controlle…
finger563 d9df47b
feat(switch2_pro): send pairing responses + init command replies (mil…
finger563 2f03c1d
feat(switch2_pro): target S3 + trace the handshake for on-hardware pa…
finger563 d9c26b6
fix(switch2_pro): keep Nintendo manufacturer data in the primary adve…
finger563 1a181ae
debug(switch2_pro): trace all characteristic reads/writes/subscriptions
finger563 7c42fb8
debug(switch2_pro): dump GATT handle map + enable NimBLE stack logging
finger563 5bc83a4
debug(switch2_pro): fix handle-map timing, NimBLE DEBUG log, auth trace
finger563 1a0f2ea
debug(switch2_pro): actually set NimBLE log to DEBUG (was INFO)
finger563 451e6ec
fix(switch2_pro): disable BLE bonding + clear stale bonds
finger563 40f7cd8
fix(switch2_pro): patcher uses GNU ar (macOS BSD ar can't read the ar…
finger563 eac8765
chore(esp-nimble-cpp): bump submodule for NimBLEServer::registerServi…
finger563 4c7326b
feat(ble_gatt_server): add conn_params_update_callback
finger563 b14ba2f
feat(switch2_pro): working Switch 2 Pro Controller BLE emulation (C6)…
finger563 8e31b14
fix(switch2_pro): address PR review (Copilot + cppcheck)
finger563 b1c63ee
fix(switch2_pro): address 2nd PR review round (Copilot)
finger563 e362ede
fix(switch2_pro): address 3rd PR review round + self-review
finger563 0279fca
fix(switch2_pro): address follow-up PR review (wake guard + patcher a…
finger563 7fa5f6f
fix(switch2_pro): make paired_/reconnect_mode_ atomic (cross-thread r…
finger563 d9f65da
docs(switch2_pro): document official ESP-IDF S3/C3 5 ms support
finger563 c179242
docs(switch2_pro): promote ESP32-S3 to a verified target (ESP-IDF v6.1)
finger563 f421a1f
fix(switch2_pro): address PR review comments
finger563 0134b23
fix(switch2_pro): finish 5 ms narrative correction + drop unverified …
finger563 7405d33
fix(switch2_pro): address Copilot suppressed-comment findings
finger563 816dd8d
fix(switch2_pro): address latest review round
finger563 69b8c21
fix(switch2_pro): propagate advertising failure + doc fixes
finger563 bb9b74d
fix(switch2_pro): wake-timer lifecycle + NOTIFY_TX status accounting
finger563 8c2ca5e
Start wake timer only if boot wake is pending
finger563 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Submodule esp-nimble-cpp
updated
12 files
| +13 −2 | src/NimBLEAddress.cpp | |
| +7 −3 | src/NimBLEAttValue.cpp | |
| +17 −2 | src/NimBLEDevice.cpp | |
| +10 −8 | src/NimBLEEddystoneTLM.cpp | |
| +2 −2 | src/NimBLEEddystoneTLM.h | |
| +35 −6 | src/NimBLEScan.cpp | |
| +2 −0 | src/NimBLEScan.h | |
| +50 −2 | src/NimBLEServer.cpp | |
| +2 −0 | src/NimBLEServer.h | |
| +2 −2 | src/NimBLEStream.cpp | |
| +2 −1 | src/NimBLEStream.h | |
| +1 −1 | src/NimBLEUtils.cpp |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| example/build/ | ||
| example/sdkconfig | ||
| example/sdkconfig.old |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| idf_component_register( | ||
| INCLUDE_DIRS "include" | ||
| SRC_DIRS "src" | ||
| REQUIRES base_component ble_gatt_server esp-nimble-cpp timer | ||
| PRIV_REQUIRES mbedtls) | ||
|
|
||
| # Opt-in: patch the prebuilt BLE controller library to accept the console's | ||
| # sub-spec 5 ms connection interval. Off by default. Covers ONLY the open RISC-V | ||
| # NimBLE controller (C6/C61/C2/H2, libble_app.a), which has no config option for a | ||
| # sub-spec interval. S3/C3 use ESP-IDF >= v6.1's official | ||
| # CONFIG_BT_CTRL_BLE_MIN_CONN_INTERVAL_ENABLE instead (see below) — they are NOT | ||
| # patch targets. Mutates the global $IDF_PATH install, so it is deliberately | ||
| # explicit and never silent. | ||
| if(CONFIG_SWITCH2_PRO_PATCH_NIMBLE_5MS) | ||
| if(IDF_TARGET STREQUAL "esp32c6" OR IDF_TARGET STREQUAL "esp32c61" | ||
| OR IDF_TARGET STREQUAL "esp32c2" OR IDF_TARGET STREQUAL "esp32h2") | ||
| message(WARNING | ||
| "[switch2_pro] SWITCH2_PRO_PATCH_NIMBLE_5MS is ON: patching the prebuilt " | ||
| "BLE controller library in $ENV{IDF_PATH} for a 5 ms connection interval " | ||
| "(${IDF_TARGET}). This modifies your global ESP-IDF install; run " | ||
| "tools/patch_nimble_5ms.py --target ${IDF_TARGET} --restore to undo.") | ||
| find_package(Python3 COMPONENTS Interpreter REQUIRED) | ||
| execute_process( | ||
| COMMAND ${Python3_EXECUTABLE} | ||
| ${CMAKE_CURRENT_LIST_DIR}/tools/patch_nimble_5ms.py | ||
| --idf-path $ENV{IDF_PATH} --target ${IDF_TARGET} | ||
| RESULT_VARIABLE _switch2_patch_result) | ||
| if(NOT _switch2_patch_result EQUAL 0) | ||
| message(FATAL_ERROR "[switch2_pro] 5 ms controller patch failed (${_switch2_patch_result})") | ||
| endif() | ||
| elseif(IDF_TARGET STREQUAL "esp32s3" OR IDF_TARGET STREQUAL "esp32c3") | ||
| message(FATAL_ERROR | ||
| "[switch2_pro] SWITCH2_PRO_PATCH_NIMBLE_5MS does not support ${IDF_TARGET}. Use " | ||
| "ESP-IDF >= v6.1's official CONFIG_BT_CTRL_BLE_MIN_CONN_INTERVAL_ENABLE (default " | ||
| "on) for S3/C3 5 ms support instead — no binary patch needed. A patch of the " | ||
| "pre-fix BTDM controller was never confirmed to work (espressif/esp-idf#18467). " | ||
| "Disable this option for ${IDF_TARGET}.") | ||
| else() | ||
| message(WARNING | ||
| "[switch2_pro] SWITCH2_PRO_PATCH_NIMBLE_5MS has no effect on ${IDF_TARGET}: " | ||
| "no known controller patch for this target (supported: C6/C61/C2/H2 NimBLE).") | ||
| endif() | ||
| endif() | ||
|
|
||
| # ESP32-S3 / C3: reconnect and wake-from-sleep need the console's sub-spec (5 ms) | ||
| # connection interval to be accepted by the closed BTDM controller. The official, | ||
| # default-on way is CONFIG_BT_CTRL_BLE_MIN_CONN_INTERVAL_ENABLE, which requires | ||
| # ESP-IDF >= v6.1 (or the v6.0.2 / v5.5.x / v5.4.x / v5.3.x backports; | ||
| # espressif/esp-idf#18467). Warn at configure time if neither that option nor the | ||
| # legacy binary patch is enabled — reconnect/wake will otherwise silently fail | ||
| # (fresh pairing and first-session input still work). | ||
| if(IDF_TARGET STREQUAL "esp32s3" OR IDF_TARGET STREQUAL "esp32c3") | ||
| if(NOT CONFIG_BT_CTRL_BLE_MIN_CONN_INTERVAL_ENABLE | ||
| AND NOT CONFIG_SWITCH2_PRO_PATCH_NIMBLE_5MS) | ||
| idf_build_get_property(_switch2_idf_ver IDF_VERSION) | ||
| message(WARNING | ||
| "[switch2_pro] ${IDF_TARGET}: neither CONFIG_BT_CTRL_BLE_MIN_CONN_INTERVAL_ENABLE " | ||
| "(official ESP-IDF sub-spec-interval support) nor SWITCH2_PRO_PATCH_NIMBLE_5MS is " | ||
| "enabled. The console drives the link at 5 ms for SUSTAINED input (it drops even the " | ||
| "fresh session to 5 ms ~1.5 s after subscription) as well as reconnect/wake, so only " | ||
| "the initial pairing handshake (~15 ms) will work here — sustained input, reconnect, " | ||
| "and wake will fail. Update to ESP-IDF >= v6.1 (you have ${_switch2_idf_ver}) for the " | ||
| "default-on option. See espressif/esp-idf#18467.") | ||
| endif() | ||
| endif() | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,138 @@ | ||
| # switch2_pro — design notes | ||
|
|
||
| ## Goal | ||
|
|
||
| Emulate a **Nintendo Switch 2 Pro Controller over BLE** so a real Switch 2 console | ||
| accepts it as a native controller — including waking the console from sleep over BLE. | ||
|
|
||
| This is NOT the Switch 1 protocol. The Switch 2 moved controllers from Bluetooth | ||
| Classic HID to **BLE with a proprietary GATT layer** (not HID-over-GATT), a custom | ||
| pairing scheme (not BLE SMP), and a custom command channel. So espp's existing | ||
| `hid_service` / `hid-rp` (standard HOGP + report descriptors) do **not** apply here; | ||
| this component builds custom GATT services directly on `espp::BleGattServer`. | ||
|
|
||
| ## Sources / prior art | ||
|
|
||
| - **Protocol facts**: `ndeadly/switch2_controller_research` (byte-level GATT map, | ||
| pairing handshake, command set, report formats; decrypted sniffer captures). | ||
| - **Working ESP32 reference** (MIT): `zhantss/ESP32-BLE5-NSController-Emulator` — | ||
| raw-NimBLE C emulator that a real Switch 2 accepts. We adapt its *approach and | ||
| structure* (with attribution) and reimplement on esp-nimble-cpp / `BleGattServer`. | ||
| We do **not** copy ndeadly's prose/tables wholesale, and we do **not** vendor | ||
| Espressif's `libble_app.a`. | ||
|
|
||
| ## Feasibility (verified) | ||
|
|
||
| Not blocked by cryptographic attestation. The pairing "authentication" is weak and | ||
| reproducible: a **fixed controller key** `B1 = 5CF6EE792CDF05E1BA2B6325C41A5F10`, an | ||
| XOR-derived link key `LTK = A1 ⊕ B1`, and a single AES-128-ECB possession proof | ||
| `B2 = AES_ECB(reverse(LTK), reverse(A2))`. Golden vector (host-verified with openssl): | ||
|
|
||
| A1 = 3503e92982877124bea80c664615834b (host public key, from console) | ||
| B1 = 5cf6ee792cdf05e1ba2b6325c41a5f10 (fixed controller key) | ||
| A2 = 6fc6df8ad8fedf15bb8c15e91f320544 (host challenge) | ||
| LTK = 69f50750ae5874c504836f43820fdc5b (= A1 ⊕ B1) | ||
| B2 = 134c97f511b9b6dd4d86fd40f536e9ed (= AES-128-ECB(rev(LTK), rev(A2))) | ||
|
|
||
| `switch2_pro_pairing.*` implements this and self-tests against the golden vector at | ||
| init (logged pass/fail) — verifiable on-device with no console. | ||
|
|
||
| ## The 5 ms connection-interval problem | ||
|
|
||
| The console drives the link at a **5 ms** connection interval — below the 7.5 ms BLE | ||
| spec minimum. The controller stack must accept it or reconnect/wake won't form. | ||
|
|
||
| Two routes by chip family: | ||
|
|
||
| **ESP32-S3 / C3 — official ESP-IDF option (the verified path).** ESP-IDF added | ||
| `CONFIG_BT_CTRL_BLE_MIN_CONN_INTERVAL_ENABLE` (default `y`), which lets the BTDM | ||
| controller and the BLE host accept sub-spec intervals (down to 3.75 ms) with no | ||
| binary patching — in ESP-IDF ≥ v6.0 (`142aea3`) / v5.5 (`cf13345`) / v5.4 | ||
| (`aefcf1c`) / v5.3 (`9831261`), and confirmed on **v6.1**; see | ||
| espressif/esp-idf#18467. The S3 is verified end-to-end on real hardware this way, | ||
| and v6.1's updated controller lib also fixes the sustained-tx stall seen on v6.0.1. | ||
| There is **no binary-patch fallback for S3/C3**: a patch of the pre-fix BTDM | ||
| library (`libbtdm_app.a`, `r_llc_con_upd_param_in_range` — the peripheral-side | ||
| connection-parameter validator) was reverse-engineered but never confirmed to | ||
| enable 5 ms on hardware (patching that min-interval compare alone is reported | ||
| insufficient on the pre-fix S3, esp-idf#18467, matching our own testing where it | ||
| had no effect), so it is not shipped. Those RE notes live in git history. | ||
|
|
||
| **ESP32-C6 / C61 / C2 / H2 — binary patch.** The open RISC-V NimBLE controller has | ||
| no equivalent config option, so `tools/patch_nimble_5ms.py` lowers its 7.5 ms floor | ||
| with a single-instruction edit: patch `$IDF_PATH/.../libble_app.a`, object | ||
| `ble_ll_conn.c.o` — the floor is `addi a5, a4, -6`; flip the immediate to `-4` | ||
| (`93 07 a7 ff` → `93 07 c7 ff`). Adapted from zhantss (MIT). It is the single | ||
| unique occurrence in its object (asserted by the patcher); `tools/smoke_test_5ms.py` | ||
| proves the edit at the disassembly level with no hardware. | ||
|
|
||
| **Build integration (decision: opt-in, never silent).** The patch mutates the user's | ||
| global IDF install and is version-fragile (the byte pattern is not guaranteed across | ||
| IDF versions — the patcher refuses to run if the pattern is missing or non-unique). So | ||
| it is gated behind a component Kconfig option `SWITCH2_PRO_PATCH_NIMBLE_5MS` | ||
| (default **n**). When enabled for a supported target, the component CMake invokes the | ||
| patcher at configure time (idempotent) and prints a loud notice. It is **not required | ||
| for the GATT + pairing skeleton milestone** — pairing runs over the command channel | ||
| independent of the interval. | ||
|
|
||
| ## GATT layout (reproduced from captures) | ||
|
|
||
| Two proprietary primary services; contiguous handles matter for some console | ||
| firmwares (FW 2.0.0+ shifts them +8 for headset audio, so absolute-handle dependence | ||
| is not strict — we reproduce the map but discover by UUID). | ||
|
|
||
| 00c5af5d-1964-4e30-8f51-1956f96bd280 (svc1, purpose unclear; chars …281/282/283) | ||
| ab7de9be-89fe-49ad-828f-118f09df7fd0 (svc2, main) | ||
| ab7de9be-…-fd2 READ/NOTIFY common input report (0x05) | ||
| 7492866c-… READ/NOTIFY Pro Controller 2 input report (0x09) | ||
| cc483f51-… WRITE_NR vibration / HD rumble | ||
| 649d4ac9-… WRITE_NR command (basic) | ||
| 3dacbc7e-… WRITE_NR vibration+command combined (pairing runs here) | ||
| 4147423d-… WRITE_NR firmware update (large) | ||
| c765a961-… NOTIFY command response #1 | ||
| 506d9f7d-… NOTIFY command response #2 | ||
|
|
||
| Security: **no SMP** — the console app-level-pairs over the command channel and will | ||
| disconnect a peer that initiates SMP. We configure NimBLE not to initiate pairing; | ||
| the LTK from the 0x15 exchange is what encrypts the link. Bond (host addr + LTK) | ||
| persists in NVS for reconnect + wake. | ||
|
|
||
| ## Milestones (all implemented; verified end-to-end on ESP32-C6 and ESP32-S3) | ||
|
|
||
| 1. **GATT + pairing skeleton**: custom GATT tree stands up, advertises with | ||
| Nintendo manufacturer data, completes the 0x15 pairing handshake (crypto | ||
| known-answer verified) and the console accepts pairing. | ||
| 2. **Command dispatch + init sequence** (flash/calibration reads, feature-select, | ||
| LEDs, firmware-update-prompt suppression) so the console finishes bring-up. | ||
| 3. **Input report streaming** (report 0x09: buttons incl. C/GL/GR, 12-bit sticks, | ||
| IMU block) streamed continuously with real backpressure. The `CONNECT_IND` and | ||
| pairing run at 15 ms, but ~1.5 s after subscription the console issues an | ||
| `LL_CONNECTION_UPDATE` dropping the link to **5 ms** for the rest of the session | ||
| (observed on real S3 hardware) — so sustained input needs sub-spec-interval | ||
| support (see milestone 4), not just reconnect/wake. | ||
| 4. **Reconnect + wake-from-sleep** (bonded reconnect with the 0x81 wake flag). | ||
| The console connects a bonded controller at 5 ms from the first packet, and | ||
| drops even the fresh session to 5 ms mid-stream, so the link runs at 5 ms in | ||
| every mode. On S3/C3 use ESP-IDF ≥ v6.1's official | ||
| `CONFIG_BT_CTRL_BLE_MIN_CONN_INTERVAL_ENABLE` (default on); on C6/C61/C2/H2 use | ||
| the opt-in `SWITCH2_PRO_PATCH_NIMBLE_5MS` controller patch. | ||
|
|
||
| On ESP-IDF ≥ v6.1 the ESP32-S3 is also fully verified (pairing, continuous input, | ||
| reconnect, wake) — its updated BTDM controller lib both accepts the console's 5 ms | ||
| CONNECT_IND (via `CONFIG_BT_CTRL_BLE_MIN_CONN_INTERVAL_ENABLE`) and sustains the | ||
| encrypted input stream, fixing the ~3 s tx-stall seen on v6.0.1 (see the README | ||
| "Known issues"). On pre-v6.1 IDF, C6-class chips (open NimBLE controller) are the | ||
| supported target. | ||
|
|
||
| ## Component layout | ||
|
|
||
| switch2_pro/ | ||
| include/switch2_pro.hpp Switch2Pro class (over BleGattServer) | ||
| include/switch2_pro_protocol.hpp UUIDs, command/subcommand ids, feature bits, fixed key, golden vector | ||
| include/switch2_pro_report.hpp Pro Controller 2 input report (0x09) packed struct | ||
| src/switch2_pro.cpp GATT setup, advertising, GAP, command dispatch | ||
| src/switch2_pro_pairing.cpp pairing crypto (mbedTLS) + state machine + self-test | ||
| tools/patch_nimble_5ms.py opt-in 5 ms interval patcher (C6/C61/C2/H2 NimBLE; S3/C3 use the official IDF option) | ||
| tools/smoke_test_5ms.py hardware-free verifier (disassembles the controller floor) | ||
| Kconfig SWITCH2_PRO_PATCH_NIMBLE_5MS opt-in | ||
| example/ C6-primary, S3-buildable |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.