Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
623e7c0
feat(switch2_pro): GATT + pairing skeleton for Switch 2 Pro Controlle…
finger563 Aug 12, 2026
d9df47b
feat(switch2_pro): send pairing responses + init command replies (mil…
finger563 Aug 12, 2026
2f03c1d
feat(switch2_pro): target S3 + trace the handshake for on-hardware pa…
finger563 Aug 13, 2026
d9c26b6
fix(switch2_pro): keep Nintendo manufacturer data in the primary adve…
finger563 Aug 14, 2026
1a181ae
debug(switch2_pro): trace all characteristic reads/writes/subscriptions
finger563 Aug 14, 2026
7c42fb8
debug(switch2_pro): dump GATT handle map + enable NimBLE stack logging
finger563 Aug 14, 2026
5bc83a4
debug(switch2_pro): fix handle-map timing, NimBLE DEBUG log, auth trace
finger563 Aug 14, 2026
1a0f2ea
debug(switch2_pro): actually set NimBLE log to DEBUG (was INFO)
finger563 Aug 14, 2026
451e6ec
fix(switch2_pro): disable BLE bonding + clear stale bonds
finger563 Aug 14, 2026
40f7cd8
fix(switch2_pro): patcher uses GNU ar (macOS BSD ar can't read the ar…
finger563 Aug 14, 2026
eac8765
chore(esp-nimble-cpp): bump submodule for NimBLEServer::registerServi…
finger563 Sep 3, 2026
4c7326b
feat(ble_gatt_server): add conn_params_update_callback
finger563 Sep 3, 2026
b14ba2f
feat(switch2_pro): working Switch 2 Pro Controller BLE emulation (C6)…
finger563 Sep 3, 2026
8e31b14
fix(switch2_pro): address PR review (Copilot + cppcheck)
finger563 Sep 3, 2026
b1c63ee
fix(switch2_pro): address 2nd PR review round (Copilot)
finger563 Sep 3, 2026
e362ede
fix(switch2_pro): address 3rd PR review round + self-review
finger563 Sep 4, 2026
0279fca
fix(switch2_pro): address follow-up PR review (wake guard + patcher a…
finger563 Sep 4, 2026
7fa5f6f
fix(switch2_pro): make paired_/reconnect_mode_ atomic (cross-thread r…
finger563 Sep 4, 2026
d9f65da
docs(switch2_pro): document official ESP-IDF S3/C3 5 ms support
finger563 Sep 4, 2026
c179242
docs(switch2_pro): promote ESP32-S3 to a verified target (ESP-IDF v6.1)
finger563 Sep 4, 2026
f421a1f
fix(switch2_pro): address PR review comments
finger563 Sep 4, 2026
0134b23
fix(switch2_pro): finish 5 ms narrative correction + drop unverified …
finger563 Sep 4, 2026
7405d33
fix(switch2_pro): address Copilot suppressed-comment findings
finger563 Sep 4, 2026
816dd8d
fix(switch2_pro): address latest review round
finger563 Sep 4, 2026
69b8c21
fix(switch2_pro): propagate advertising failure + doc fixes
finger563 Sep 4, 2026
bb9b74d
fix(switch2_pro): wake-timer lifecycle + NOTIFY_TX status accounting
finger563 Sep 4, 2026
8c2ca5e
Start wake timer only if boot wake is pending
finger563 Sep 4, 2026
88b264a
fix(switch2_pro): wake-latch timing + ChannelCallbacks ownership
finger563 Sep 5, 2026
8ec67d1
style(switch2_pro): fix comment indentation (clang-format)
finger563 Sep 5, 2026
c2fff46
update to latest naming following requested name change in esp-nimble…
finger563 Sep 7, 2026
0b4c46e
feat(switch2_pro): clear_bond() API + start()/restore safety
finger563 Sep 8, 2026
c5abbc9
refactor(switch2_pro): use the espp nvs component instead of raw ESP-…
finger563 Sep 8, 2026
65c28f9
fix(switch2_pro): address latest review round
finger563 Sep 8, 2026
601f4cc
fix(switch2_pro): gate streaming on encryption + complete factory reset
finger563 Sep 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,10 @@ jobs:
target: esp32
- path: 'components/stream_frame/example'
target: esp32
- path: 'components/switch2_pro/example'
target: esp32c6
- path: 'components/switch2_pro/example'
target: esp32s3
- path: 'components/sx126x/example'
target: esp32s3
- path: 'components/t-deck/example'
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/upload_components.yml
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ jobs:
components/st25dv
components/st7123touch
components/state_machine
components/switch2_pro
components/sx126x
components/t_keyboard
components/t-deck
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@

# build folder for ESP-IDF
build/
# alternate/per-target ESP-IDF build dirs (e.g. build_s3, build_c6)
build_*/


# we only version sdkconfig.defaults
Expand Down
54 changes: 44 additions & 10 deletions components/ble_gatt_server/include/ble_gatt_server.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,13 @@ class BleGattServer : public BaseComponent {
/// @param conn_info The connection information for the device.
typedef std::function<void(const NimBLEConnInfo &)> authentication_complete_callback_t;

/// @brief Callback for when the connection parameters are updated (fires on
/// completion of any connection-parameter-update procedure — whether
/// peer- or self-initiated, accepted or rejected; read the live
/// parameters from conn_info to see the outcome).
/// @param conn_info The connection information for the device.
typedef std::function<void(const NimBLEConnInfo &)> conn_params_update_callback_t;

/// @brief Callback to retrieve the passkey for the device.
/// @return The passkey for the device.
typedef std::function<uint32_t(void)> get_passkey_callback_t;
Expand Down Expand Up @@ -131,6 +138,8 @@ class BleGattServer : public BaseComponent {
nullptr; ///< Callback for when a device disconnects from the GATT server.
authentication_complete_callback_t authentication_complete_callback =
nullptr; ///< Callback for when a device completes authentication.
conn_params_update_callback_t conn_params_update_callback =
nullptr; ///< Callback for when the connection parameters are updated.
Comment thread
finger563 marked this conversation as resolved.
get_passkey_callback_t get_passkey_callback =
nullptr; ///< Callback for getting the passkey.
/// @note If not provided, will simply return
Expand Down Expand Up @@ -259,15 +268,35 @@ class BleGattServer : public BaseComponent {
// set the server callbacks
server_->setCallbacks(new BleGattServerCallbacks(this));

// create the device info service
device_info_service_.init(server_);
if (builtin_info_services_) {
// create the device info service
device_info_service_.init(server_);

// create the battery service
battery_service_.init(server_);
// create the battery service
battery_service_.init(server_);
}

return true;
}

/// Enable or disable the built-in Device Information and Battery services.
/// @param enabled Whether init()/start_services() create and start the
/// built-in Device Information (0x180A) and Battery (0x180F) services.
/// Defaults to true. Set to false BEFORE init() for peripherals that
/// must expose only their own services (e.g. emulating a device whose
/// GATT layout must match a specific attribute table).
/// @note Must be called before init(). Calling it after init() has no effect
/// (the built-in services are created/skipped during init) and is ignored
/// with a warning, since honoring it would leave services created but never
/// started or torn down.
void set_builtin_info_services_enabled(bool enabled) {
if (server_) {
logger_.warn("set_builtin_info_services_enabled() ignored: must be called before init()");
return;
}
builtin_info_services_ = enabled;
}

/// Deinitialize the GATT server
/// This method deletes the server and all associated objects.
/// It also invalidates any references/pointers to the server.
Expand All @@ -283,8 +312,10 @@ class BleGattServer : public BaseComponent {
}

// deinitialize the services
device_info_service_.deinit();
battery_service_.deinit();
if (builtin_info_services_) {
device_info_service_.deinit();
battery_service_.deinit();
}
// if true, deletes all server/advertising/scan/client objects which
// invalidates any references/pointers to them
bool clear_all = true;
Expand All @@ -296,8 +327,10 @@ class BleGattServer : public BaseComponent {
/// Start the services
/// This method starts the device info and battery services.
void start_services() {
device_info_service_.start();
battery_service_.start();
if (builtin_info_services_) {
device_info_service_.start();
battery_service_.start();
}
}

/// Start the server
Expand All @@ -309,8 +342,7 @@ class BleGattServer : public BaseComponent {
if (!server_) {
return false;
}
server_->start();
return true;
return server_->start();
}

#if !CONFIG_BT_NIMBLE_EXT_ADV || defined(_DOXYGEN_)
Expand Down Expand Up @@ -806,6 +838,8 @@ class BleGattServer : public BaseComponent {
NimBLEServer *server_{nullptr}; ///< The GATT server.
DeviceInfoService device_info_service_; ///< The device info service.
BatteryService battery_service_; ///< The battery service.
bool builtin_info_services_{
true}; ///< Whether to create/start the built-in DIS + battery services.
};
} // namespace espp

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class BleGattServerCallbacks : public NimBLEServerCallbacks {
virtual void onConnect(NimBLEServer *server, NimBLEConnInfo &conn_info) override;
virtual void onDisconnect(NimBLEServer *server, NimBLEConnInfo &conn_info, int reason) override;
virtual void onAuthenticationComplete(NimBLEConnInfo &conn_info) override;
virtual void onConnParamsUpdate(NimBLEConnInfo &conn_info) override;
virtual uint32_t onPassKeyDisplay() override;
virtual void onConfirmPassKey(NimBLEConnInfo &conn_info, uint32_t pass_key) override;

Expand Down
7 changes: 7 additions & 0 deletions components/ble_gatt_server/src/ble_gatt_server_callbacks.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,13 @@ void BleGattServerCallbacks::onAuthenticationComplete(NimBLEConnInfo &conn_info)
}
}
}
void BleGattServerCallbacks::onConnParamsUpdate(NimBLEConnInfo &conn_info) {
if (server_) {
if (server_->callbacks_.conn_params_update_callback) {
server_->callbacks_.conn_params_update_callback(conn_info);
}
}
}
uint32_t BleGattServerCallbacks::onPassKeyDisplay() {
if (server_ && server_->callbacks_.get_passkey_callback) {
return server_->callbacks_.get_passkey_callback();
Expand Down
19 changes: 19 additions & 0 deletions components/nvs/example/main/nvs_example.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,25 @@ extern "C" void app_main(void) {
}
ec.clear();

// test setting + getting a binary blob (arbitrary bytes, incl. embedded nulls)
std::vector<uint8_t> blob = {0xDE, 0xAD, 0x00, 0xBE, 0xEF};
nvs.set_var("system", "blob", blob, ec);
if (ec) {
fmt::print("Error: {}\n", ec.message());
} else {
fmt::print("Blob set ({} bytes)\n", blob.size());
}
ec.clear();

std::vector<uint8_t> read_blob;
nvs.get_var("system", "blob", read_blob, ec);
if (ec) {
fmt::print("Error: {}\n", ec.message());
} else {
fmt::print("Blob read back ({} bytes), matches = {}\n", read_blob.size(), read_blob == blob);
}
ec.clear();

counter++;

if (counter > 10) {
Expand Down
73 changes: 73 additions & 0 deletions components/nvs/include/nvs_handle_espp.hpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#pragma once

#include <cstdint>
#include <string>
#include <vector>

#include <nvs.h>
#include <nvs.hpp>
Expand Down Expand Up @@ -191,6 +193,49 @@ class NvsHandle : public BaseComponent {
return;
}

/// @brief Reads a binary blob from the NVS
/// @param[in] key NVS Key of the blob to read
/// @param[out] value Vector resized to the stored blob and filled with its bytes
/// @param[out] ec Saves a std::error_code representing success or failure
/// @details Use for arbitrary binary data / structs (unlike set/get<string>,
/// embedded null bytes are preserved).
void get(const char *key, std::vector<uint8_t> &value, std::error_code &ec) {
if (!check_handle_initialized(ec))
return;

if (!check_key(key, ec))
return;

std::size_t len = 0;
esp_err_t err = handle_->get_item_size(nvs::ItemType::BLOB, key, len);
if (err != ESP_OK) {
if (err == ESP_ERR_NVS_NOT_FOUND) {
ec = make_error_code(NvsErrc::Key_Not_Found);
logger_.error("The value is not initialized in NVS, key = '{}'", key);
} else {
ec = make_error_code(NvsErrc::Read_NVS_Failed);
logger_.error("Error {} reading blob size!", esp_err_to_name(err));
}
return;
}
value.resize(len);
if (len == 0)
return;
err = handle_->get_blob(key, value.data(), len);
if (err != ESP_OK) {
ec = make_error_code(NvsErrc::Read_NVS_Failed);
logger_.error("Error {} reading blob from NVS!", esp_err_to_name(err));
}
}

/// @brief Reads a binary blob from the NVS
/// @param[in] key NVS Key of the blob to read
/// @param[out] value Vector resized to the stored blob and filled with its bytes
/// @param[out] ec Saves a std::error_code representing success or failure
void get(std::string_view key, std::vector<uint8_t> &value, std::error_code &ec) {
get(key.data(), value, ec);
}

/// @brief Reads a variable from the NVS
/// @param[in] key NVS Key of the variable to read
/// @param[out] value Variable to read
Expand Down Expand Up @@ -415,6 +460,34 @@ class NvsHandle : public BaseComponent {
return;
}

/// @brief Save a binary blob in the NVS
/// @param[in] key NVS Key of the blob to set
/// @param[in] value Bytes to store
/// @param[out] ec Saves a std::error_code representing success or failure
/// @details Use for arbitrary binary data / structs (unlike set/get<string>,
/// embedded null bytes are preserved). Does not commit.
void set(const char *key, const std::vector<uint8_t> &value, std::error_code &ec) {
if (!check_handle_initialized(ec))
return;

if (!check_key(key, ec))
return;

esp_err_t err = handle_->set_blob(key, value.data(), value.size());
if (err != ESP_OK) {
ec = make_error_code(NvsErrc::Write_NVS_Failed);
logger_.error("Error {} writing blob to NVS!", esp_err_to_name(err));
}
}

/// @brief Save a binary blob in the NVS
/// @param[in] key NVS Key of the blob to set
/// @param[in] value Bytes to store
/// @param[out] ec Saves a std::error_code representing success or failure
void set(std::string_view key, const std::vector<uint8_t> &value, std::error_code &ec) {
set(key.data(), value, ec);
}

/// @brief Commit changes
/// @param[out] ec Saves a std::error_code representing success or failure
/// @details Commits changes to the NVS
Expand Down
3 changes: 3 additions & 0 deletions components/switch2_pro/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
example/build/
example/sdkconfig
example/sdkconfig.old
65 changes: 65 additions & 0 deletions components/switch2_pro/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
idf_component_register(
Comment thread
finger563 marked this conversation as resolved.
INCLUDE_DIRS "include"
SRC_DIRS "src"
REQUIRES base_component ble_gatt_server esp-nimble-cpp nvs 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()
Loading
Loading