Repository navigation
Show a hidden service to connected centrals and announce the change #1202
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
base: master
Are you sure you want to change the base?
Changes from all commits
307b927
35cf457
de52f03
4a11d65
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -53,6 +53,7 @@ struct gattRegisterCallbackArgs { | |
| NimBLEServer::NimBLEServer() | ||
| : m_gattsStarted{false}, | ||
| m_svcChanged{false}, | ||
| m_svcChangedSent{false}, | ||
| # if !MYNEWT_VAL(BLE_EXT_ADV) | ||
| m_advertiseOnDisconnect{false}, | ||
| # endif | ||
|
|
@@ -192,6 +193,7 @@ void NimBLEServer::setServiceChanged() { | |
| * @details This should be called when services are added, removed or modified after the server has been started. | ||
| */ | ||
| void NimBLEServer::sendServiceChangedIndication() const { | ||
| m_svcChangedSent = true; | ||
| ble_svc_gatt_changed(0x0001, 0xffff); | ||
| } | ||
|
|
||
|
|
@@ -209,7 +211,7 @@ void NimBLEServer::gattRegisterCallback(ble_gatt_register_ctxt* ctxt, void* arg) | |
| NimBLEUUID uuid(ctxt->svc.svc_def->uuid); | ||
| args->pSvc = nullptr; | ||
| for (auto pSvc : NimBLEDevice::getServer()->m_svcVec) { | ||
| if (!pSvc->getRemoved() && pSvc->m_handle == 0 && pSvc->getUUID() == uuid) { | ||
| if (pSvc->getRemoved() != NIMBLE_ATT_REMOVE_DELETE && pSvc->m_handle == 0 && pSvc->getUUID() == uuid) { | ||
| pSvc->m_handle = ctxt->svc.handle; | ||
| NIMBLE_LOGD(LOG_TAG, "Service registered: %s, handle=%d", uuid.toString().c_str(), ctxt->svc.handle); | ||
| // Set the arg to the service so we know that the following | ||
|
|
@@ -295,6 +297,13 @@ bool NimBLEServer::start() { | |
| return false; | ||
| } | ||
|
|
||
| // resetGATT() registered the hidden services too; hide them again now that they have handles. | ||
| for (const auto& svc : m_svcVec) { | ||
| if (svc->getRemoved() == NIMBLE_ATT_REMOVE_HIDE) { | ||
| ble_gatts_svc_set_visibility(svc->getHandle(), 0); | ||
| } | ||
| } | ||
|
|
||
| # if MYNEWT_VAL(NIMBLE_CPP_LOG_LEVEL) >= 4 | ||
| ble_gatts_show_local(); | ||
|
|
||
|
|
@@ -559,6 +568,28 @@ int NimBLEServer::handleGapEvent(ble_gap_event* event, void* arg) { | |
| uint8_t subVal = event->subscribe.cur_notify + (event->subscribe.cur_indicate << 1); | ||
| NIMBLE_LOGI(LOG_TAG, "subscribe event; attr_handle=%d, subscribed: %d", event->subscribe.attr_handle, subVal); | ||
|
|
||
| static const ble_uuid16_t gattSvcUuid = BLE_UUID16_INIT(BLE_GATT_SVC_UUID16); | ||
| static const ble_uuid16_t svcChangedUuid = BLE_UUID16_INIT(BLE_SVC_GATT_CHR_SERVICE_CHANGED_UUID16); | ||
| uint16_t svcChangedHandle = 0; | ||
| if (ble_gatts_find_chr(&gattSvcUuid.u, &svcChangedUuid.u, nullptr, &svcChangedHandle) == 0 && | ||
| svcChangedHandle == event->subscribe.attr_handle) { | ||
| // GATT database changes can leave clients with stale attribute caches. | ||
| // NimBLE sends Service Changed indications to connected clients but preserves | ||
| // pending indications across disconnections only for bonded clients. | ||
| // | ||
| // Because unbonded clients cannot be recognized across connections, once any | ||
| // Service Changed has been sent the server indicates it to every client that | ||
| // enables it. A server may indicate only after the client enables it in the CCCD, | ||
| // so this is the earliest opportunity, and it stops a caching client | ||
| // (CoreBluetooth) from serving a stale table. A subscription restored for a bonded | ||
| // client is skipped: the host sends that client's pending indication itself. | ||
| if (pServer->m_svcChangedSent && event->subscribe.cur_indicate && | ||
| event->subscribe.reason == BLE_GAP_SUBSCRIBE_REASON_WRITE) { | ||
| ble_gatts_indicate(event->subscribe.conn_handle, svcChangedHandle); | ||
| } | ||
| break; | ||
| } | ||
|
|
||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think this is right, clients should not cache the database unless bonded, this seems to be fixing a client side issue. Also this will trigger all clients to refresh the database instead of just the current one.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, an unbonded client is expected to rediscover services on each connection (Core 4.2, Vol 3, Part G, 2.5.2). However, macOS CoreBluetooth seems to use a cached table instead, so this is a workaround for CoreBluetooth rather than a regression in the PR. Sorry for the confusion! Just one small clarification: the indication is sent only to the subscribing connection (ble_gatts_indicate(conn_handle, …)), not to all connected clients. Any client subscribing after a change will still receive it, so a compliant client would simply perform one extra rediscovery. Would you be open to keeping this in the PR as a CoreBluetooth workaround? I think it would be useful for macOS users (such as I'm). CoreBluetooth(macOS) gives a client app very little control here: there is no API to clear its attribute cache or force a fresh discovery, so the only lever I found is a Service Changed indication from the server. UPD In case extra rediscovery for compliant clients is a concern, it can be opt-in: off by default, and enabled explicitly for peripheral devices that need macOS clients, e.g. NimBLEServer::setServiceChangedOnSubscribe(true).
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry I read the call differently and thought you were calling The other issue when looking at this is how to deal with multiple clients that are not bonded? This will only send the indication to the first one. Would be nice if everyone just followed the spec lol.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As I understand it, it should reach every unbonded client, not just the first. Each connection has its own CCCD state (conn->bhc_gatt_svr.clt_cfgs), and ble_gatts_clt_cfg_access() raises BLE_GAP_SUBSCRIBE_REASON_WRITE with that client's conn_handle, so the indication goes only to that connection. Also, unlike m_svcChanged, m_svcChangedSent is never cleared, so each client subscribing after the first Service Changed gets an indication. The downside is that clients with an already up-to-date table will get it too, even if they reconnect hours later, and may end up rediscovering unnecessarily. I didn't find a way to recognize a previously connected, unbonded client. I tested this on my bench, and every new connection after a change received the indication. I couldn't properly test multiple simultaneous connections, though. CoreBluetooth combines multiple apps on the same Mac into a single BLE connection, so the board saw only one connection and received one indication, even with two clients running. |
||
| auto pChar = pServer->getCharacteristicByHandle(event->subscribe.attr_handle); | ||
| if (!pChar) { | ||
| NIMBLE_LOGE(LOG_TAG, "subscribe event; attr_handle=%d, not found", event->subscribe.attr_handle); | ||
|
|
@@ -838,8 +869,9 @@ void NimBLEServer::resetCallbacks() { | |
| * available and can be re-added in the future. If desired a removed but not deleted service can | ||
| * be deleted later by calling this method with deleteSvc set to true. | ||
| * | ||
| * @note The service will not be removed from the database until all open connections are closed | ||
| * as it requires resetting the GATT server. In the interim the service will have it's visibility disabled. | ||
| * @note A service removed but not deleted stays registered with its visibility disabled, through any | ||
| * GATT reset, so addService() can show it again while clients are connected. A deleted service | ||
| * leaves the database at the next reset, which waits until all open connections are closed. | ||
| * | ||
| * @note Advertising will need to be restarted by the user after calling this as we must stop | ||
| * advertising in order to remove the service. | ||
|
|
@@ -853,13 +885,10 @@ void NimBLEServer::removeService(NimBLEService* service, bool deleteSvc) { | |
| // Otherwise, ignore the call and return. | ||
| if (service->getRemoved() > 0) { | ||
| if (deleteSvc) { | ||
| for (auto it = m_svcVec.begin(); it != m_svcVec.end(); ++it) { | ||
| if ((*it) == service) { | ||
| delete *it; | ||
| m_svcVec.erase(it); | ||
| break; | ||
| } | ||
| } | ||
| // A hidden service is still registered and the stack keeps a pointer to its definition, | ||
| // so resetGATT() frees it once the reset has dropped it from the database. | ||
| service->setRemoved(NIMBLE_ATT_REMOVE_DELETE); | ||
| setServiceChanged(); | ||
| } | ||
|
|
||
| return; | ||
|
|
@@ -871,7 +900,11 @@ void NimBLEServer::removeService(NimBLEService* service, bool deleteSvc) { | |
| } | ||
|
|
||
| service->setRemoved(deleteSvc ? NIMBLE_ATT_REMOVE_DELETE : NIMBLE_ATT_REMOVE_HIDE); | ||
| setServiceChanged(); | ||
| if (deleteSvc) { | ||
| setServiceChanged(); | ||
| } else { | ||
| sendServiceChangedIndication(); | ||
| } | ||
| # if !MYNEWT_VAL(BLE_EXT_ADV) && MYNEWT_VAL(BLE_ROLE_BROADCASTER) | ||
| NimBLEDevice::getAdvertising()->removeServiceUUID(service->getUUID()); | ||
| # endif | ||
|
|
@@ -897,15 +930,24 @@ void NimBLEServer::addService(NimBLEService* service) { | |
| return; | ||
| } | ||
|
|
||
| // Hidden but still registered: shown in place, no reset needed. A pending reset may still change | ||
| // the database under it, so the service waits for that reset instead. | ||
| if (service->getRemoved() == NIMBLE_ATT_REMOVE_HIDE && service->getHandle() != 0 && !m_svcChanged && | ||
| ble_gatts_svc_set_visibility(service->getHandle(), 1) == 0) { | ||
|
coderabbitai[bot] marked this conversation as resolved.
Resolved
|
||
| service->setRemoved(0); | ||
| sendServiceChangedIndication(); | ||
| return; | ||
| } | ||
|
|
||
| service->setRemoved(0); | ||
| setServiceChanged(); | ||
| } // addService | ||
|
|
||
| /** | ||
| * @brief Resets the GATT server, used when services are added/removed after initialization. | ||
| * @return True if successful. | ||
| * @details This will reset the GATT server and re-register all services, characteristics, and | ||
| * descriptors that have not been removed. Services, characteristics, and descriptors that have been | ||
| * @details This will reset the GATT server and re-register every service that has not been deleted, | ||
| * hidden ones included; start() hides those again. Characteristics and descriptors that have been | ||
| * removed but not deleted will be skipped and have their handles cleared, and those that have been | ||
| * deleted will be removed from the server's service vector. | ||
| */ | ||
|
|
@@ -914,7 +956,13 @@ bool NimBLEServer::resetGATT() { | |
| NimBLEDevice::stopAdvertising(); | ||
| # endif | ||
|
|
||
| ble_gatts_reset(); | ||
| // 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(); | ||
| if (rc != 0) { | ||
| NIMBLE_LOGE(LOG_TAG, "ble_gatts_reset; rc=%d, %s", rc, NimBLEUtils::returnCodeToString(rc)); | ||
| return false; | ||
| } | ||
| ble_svc_gap_init(); | ||
| ble_svc_gatt_init(); | ||
|
|
||
|
|
@@ -950,11 +998,11 @@ bool NimBLEServer::resetGATT() { | |
| ++chrIt; | ||
| } | ||
|
|
||
| if (pSvc->getRemoved() == 0) { | ||
| if (!pSvc->start_internal()) { | ||
| NIMBLE_LOGE(LOG_TAG, "Failed to start service: %s", pSvc->getUUID().toString().c_str()); | ||
| return false; | ||
| } | ||
| // A hidden service stays registered, so addService() can show it again while a client is | ||
| // 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()); | ||
| return false; | ||
|
coderabbitai[bot] marked this conversation as resolved.
Resolved
|
||
| } | ||
|
|
||
| pSvc->m_handle = 0; | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.