From de3596e318c357ee3424d574cd8f65f0a5df9030 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Fri, 18 Sep 2026 17:28:15 -0500 Subject: [PATCH] feat(NimBLEServer): add registerServicesFirst() to place app services at low handles (#443) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(NimBLEServer): add registerServicesFirst() to place app services at low handles 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. --- src/NimBLEServer.cpp | 54 ++++++++++++++++++++++++++++++++++++++++---- src/NimBLEServer.h | 2 ++ 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/src/NimBLEServer.cpp b/src/NimBLEServer.cpp index db473c2d..fbbb0129 100644 --- a/src/NimBLEServer.cpp +++ b/src/NimBLEServer.cpp @@ -53,8 +53,9 @@ struct gattRegisterCallbackArgs { NimBLEServer::NimBLEServer() : m_gattsStarted{false}, m_svcChanged{false}, + m_registerAppServicesFirst{false}, m_svcChangedSent{false}, -# if !MYNEWT_VAL(BLE_EXT_ADV) +# if !MYNEWT_VAL(BLE_EXT_ADV) && MYNEWT_VAL(BLE_ROLE_BROADCASTER) m_advertiseOnDisconnect{false}, # endif m_pCallbacks{&defaultCallbacks}, @@ -397,6 +398,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::registerAppServicesFirst(bool enable) { + m_registerAppServicesFirst = enable; +} // registerAppServicesFirst + /** * @brief Return the number of connected clients. * @return The number of connected clients. @@ -954,7 +969,12 @@ bool NimBLEServer::resetGATT() { # if MYNEWT_VAL(BLE_ROLE_BROADCASTER) NimBLEDevice::stopAdvertising(); # endif - + // esp-idf nimble clears the device name and + // appearance during ble_gatts_reset() +#ifndef CONFIG_USING_NIMBLE_COMPONENT + std::string name(ble_svc_gap_device_name()); + uint16_t appearance = ble_svc_gap_device_appearance(); +#endif // A refused reset leaves every attribute registered, so nothing may be freed below; the next // start() retries, since services stay marked changed. int rc = ble_gatts_reset(); @@ -962,8 +982,24 @@ bool NimBLEServer::resetGATT() { NIMBLE_LOGE(LOG_TAG, "ble_gatts_reset; rc=%d, %s", rc, NimBLEUtils::returnCodeToString(rc)); return false; } - ble_svc_gap_init(); - ble_svc_gatt_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 + // registerAppServicesFirst() 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); +#endif + ble_svc_gatt_init(); + }; + + if (!m_registerAppServicesFirst) { + initGapGattServices(); + } for (auto svcIt = m_svcVec.begin(); svcIt != m_svcVec.end();) { auto* pSvc = *svcIt; @@ -1001,6 +1037,12 @@ bool NimBLEServer::resetGATT() { // connected — a reset, which needs none connected, is the only way to register one. if (!pSvc->start_internal()) { NIMBLE_LOGE(LOG_TAG, "Failed to start service: %s", pSvc->getUUID().toString().c_str()); + // When deferring GAP/GATT (registerAppServicesFirst), 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_registerAppServicesFirst) { + initGapGattServices(); + } return false; } @@ -1008,6 +1050,10 @@ bool NimBLEServer::resetGATT() { ++svcIt; } + if (m_registerAppServicesFirst) { + initGapGattServices(); + } + return true; } // resetGATT diff --git a/src/NimBLEServer.h b/src/NimBLEServer.h index ad872d93..37fab98d 100644 --- a/src/NimBLEServer.h +++ b/src/NimBLEServer.h @@ -83,6 +83,7 @@ class NimBLEServer { NimBLEConnInfo getPeerInfo(const NimBLEAddress& address) const; NimBLEConnInfo getPeerInfoByHandle(uint16_t connHandle) const; void advertiseOnDisconnect(bool enable); + void registerAppServicesFirst(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); @@ -130,6 +131,7 @@ class NimBLEServer { bool m_gattsStarted : 1; bool m_svcChanged : 1; + bool m_registerAppServicesFirst : 1; mutable bool m_svcChangedSent : 1; # if !MYNEWT_VAL(BLE_EXT_ADV) && MYNEWT_VAL(BLE_ROLE_BROADCASTER) bool m_advertiseOnDisconnect : 1;