From 2846a29cc5c46972b1d300f1d071e6a73aa82d75 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Wed, 2 Sep 2026 22:55:20 -0500 Subject: [PATCH 1/2] feat(NimBLEServer): add registerServicesFirst() to place app services at low handles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit By default the standard GAP (0x1800) and GATT (0x1801) services register first and take the low attribute handles (0x0001+), with the application's services placed after them. Some peripherals must instead expose their own services at the low handles — e.g. when emulating a device whose central addresses attributes by hardcoded handle rather than by discovery. Add an opt-in NimBLEServer::registerServicesFirst(bool) setter (default false, so existing behavior is unchanged) that defers GAP/GATT registration until after the application services in resetGATT(), giving the application services the low handles. The device-name/appearance restore that follows ble_gatts_reset() is factored into a small lambda so it stays correct in either order. Co-Authored-By: Claude Opus 4.8 --- src/NimBLEServer.cpp | 37 +++++++++++++++++++++++++++++++++---- src/NimBLEServer.h | 2 ++ 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/src/NimBLEServer.cpp b/src/NimBLEServer.cpp index 42579791..4670ee29 100644 --- a/src/NimBLEServer.cpp +++ b/src/NimBLEServer.cpp @@ -54,6 +54,7 @@ NimBLEServer::NimBLEServer() : m_gattsStarted{false}, m_svcChanged{false}, m_deleteCallbacks{false}, + m_registerServicesFirst{false}, # if !MYNEWT_VAL(BLE_EXT_ADV) m_advertiseOnDisconnect{false}, # endif @@ -366,6 +367,20 @@ void NimBLEServer::advertiseOnDisconnect(bool enable) { } // advertiseOnDisconnect # endif +/** + * @brief Register the application's services before the standard GAP/GATT services. + * @param [in] enable true == register the application services first (they take the + * low attribute handles, starting at 0x0001, and GAP/GATT are placed after them); + * false (default) == the standard behavior, GAP/GATT register first and take the low + * handles. + * @details Must be called before the services are started (i.e. before + * NimBLEServer::start()). Useful when a peripheral must expose a fixed attribute-table + * layout that another device relies on by hardcoded handle rather than discovery. + */ +void NimBLEServer::registerServicesFirst(bool enable) { + m_registerServicesFirst = enable; +} // registerServicesFirst + /** * @brief Return the number of connected clients. * @return The number of connected clients. @@ -897,14 +912,24 @@ bool NimBLEServer::resetGATT() { #endif ble_gatts_reset(); - ble_svc_gap_init(); + // Register the standard GAP (0x1800) and GATT (0x1801) services, restoring the + // device name/appearance that ble_gatts_reset() clears. By default this happens + // first, so GAP/GATT take the low attribute handles (0x0001+). When + // registerServicesFirst() is enabled, it is deferred until after the application + // services below, so those take the low handles instead. + auto initGapGattServices = [&]() { + ble_svc_gap_init(); #ifndef CONFIG_USING_NIMBLE_COMPONENT - ble_svc_gap_device_name_set(name.c_str()); - ble_svc_gap_device_appearance_set(appearance); + ble_svc_gap_device_name_set(name.c_str()); + ble_svc_gap_device_appearance_set(appearance); #endif + ble_svc_gatt_init(); + }; - ble_svc_gatt_init(); + if (!m_registerServicesFirst) { + initGapGattServices(); + } for (auto svcIt = m_svcVec.begin(); svcIt != m_svcVec.end();) { auto* pSvc = *svcIt; @@ -949,6 +974,10 @@ bool NimBLEServer::resetGATT() { ++svcIt; } + if (m_registerServicesFirst) { + initGapGattServices(); + } + return true; } // resetGATT diff --git a/src/NimBLEServer.h b/src/NimBLEServer.h index b2b79e1e..70ff21d9 100644 --- a/src/NimBLEServer.h +++ b/src/NimBLEServer.h @@ -81,6 +81,7 @@ class NimBLEServer { NimBLEConnInfo getPeerInfo(const NimBLEAddress& address) const; NimBLEConnInfo getPeerInfoByHandle(uint16_t connHandle) const; void advertiseOnDisconnect(bool enable); + void registerServicesFirst(bool enable); void setDataLen(uint16_t connHandle, uint16_t tx_octets) const; bool updatePhy(uint16_t connHandle, uint8_t txPhysMask, uint8_t rxPhysMask, uint16_t phyOptions); bool getPhy(uint16_t connHandle, uint8_t* txPhy, uint8_t* rxPhy); @@ -129,6 +130,7 @@ class NimBLEServer { bool m_gattsStarted : 1; bool m_svcChanged : 1; bool m_deleteCallbacks : 1; + bool m_registerServicesFirst : 1; # if !MYNEWT_VAL(BLE_EXT_ADV) && MYNEWT_VAL(BLE_ROLE_BROADCASTER) bool m_advertiseOnDisconnect : 1; # endif From 6a396c7a5da171452249b149a3f8990790a472d3 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Wed, 2 Sep 2026 23:16:33 -0500 Subject: [PATCH 2/2] fix(NimBLEServer): register GAP/GATT on the failure path when deferring them When registerServicesFirst() is enabled, GAP/GATT registration is deferred until after the application-service loop in resetGATT(). If a service fails to start mid-loop and the function returns early, the mandatory GAP/GATT services (and the restored device name/appearance) would be left out of the database. Register them on that failure path too so the GATT database is never left without them. Co-Authored-By: Claude Opus 4.8 --- src/NimBLEServer.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/NimBLEServer.cpp b/src/NimBLEServer.cpp index 4670ee29..a83047ef 100644 --- a/src/NimBLEServer.cpp +++ b/src/NimBLEServer.cpp @@ -966,6 +966,12 @@ bool NimBLEServer::resetGATT() { if (pSvc->getRemoved() == 0) { if (!pSvc->start_internal()) { NIMBLE_LOGE(LOG_TAG, "Failed to start service: %s", pSvc->getUUID().toString().c_str()); + // When deferring GAP/GATT (registerServicesFirst), still register + // them on the failure path so the mandatory GAP/GATT services (and + // the restored name/appearance) are never left out of the database. + if (m_registerServicesFirst) { + initGapGattServices(); + } return false; } }