Skip to content

Commit 0527f1f

Browse files
committed
Add a timer for scan responses
This ensures that the callback will be called within the configured time (in ms) when devices fail to respond to a scan response request within that time. * Adds stats when debug logging to help tune the scan response timeout/scan parameters.
1 parent b5110a2 commit 0527f1f

4 files changed

Lines changed: 323 additions & 16 deletions

File tree

src/NimBLEAdvertisedDevice.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ NimBLEAdvertisedDevice::NimBLEAdvertisedDevice(const ble_gap_event* event, uint8
5252
m_advLength{event->disc.length_data},
5353
m_payload(event->disc.data, event->disc.data + event->disc.length_data) {
5454
# endif
55+
m_pNextWaiting = this; // initialize sentinel: self-pointer means "not in list"
5556
} // NimBLEAdvertisedDevice
5657

5758
/**

src/NimBLEAdvertisedDevice.h

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -158,11 +158,13 @@ class NimBLEAdvertisedDevice {
158158
uint8_t findAdvField(uint8_t type, uint8_t index = 0, size_t* data_loc = nullptr) const;
159159
size_t findServiceData(uint8_t index, uint8_t* bytes) const;
160160

161-
NimBLEAddress m_address{};
162-
uint8_t m_advType{};
163-
int8_t m_rssi{};
164-
uint8_t m_callbackSent{};
165-
uint16_t m_advLength{};
161+
NimBLEAddress m_address{};
162+
uint8_t m_advType{};
163+
int8_t m_rssi{};
164+
uint8_t m_callbackSent{};
165+
uint16_t m_advLength{};
166+
ble_npl_time_t m_time{};
167+
NimBLEAdvertisedDevice* m_pNextWaiting{}; // intrusive list node; self-pointer means "not in list", set in ctor
166168

167169
# if MYNEWT_VAL(BLE_EXT_ADV)
168170
bool m_isLegacyAdv{};

src/NimBLEScan.cpp

Lines changed: 213 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,70 @@
2020

2121
# include "NimBLEDevice.h"
2222
# include "NimBLELog.h"
23+
# if defined(CONFIG_NIMBLE_CPP_IDF)
24+
# include "nimble/nimble_port.h"
25+
# else
26+
# include "nimble/porting/nimble/include/nimble/nimble_port.h"
27+
# endif
2328

2429
# include <string>
2530
# include <climits>
2631

32+
# define DEFAULT_SCAN_RESP_TIMEOUT_MS 10240 // max advertising interval (10.24s)
33+
2734
static const char* LOG_TAG = "NimBLEScan";
2835
static NimBLEScanCallbacks defaultScanCallbacks;
2936

37+
/**
38+
* @brief Calls the onResult callback with the given device,
39+
* this is used to provide a scan result to the callbacks when a device hasn't responded to
40+
* the scan request in time. This is called by the host task from the default event queue.
41+
*/
42+
void NimBLEScan::forceResultCallback(ble_npl_event* ev) {
43+
auto pScan = NimBLEDevice::getScan();
44+
pScan->m_stats.incMissedSrCount();
45+
NimBLEAdvertisedDevice* pDev = static_cast<NimBLEAdvertisedDevice*>(ble_npl_event_get_arg(ev));
46+
pDev->m_callbackSent = 2;
47+
pScan->m_pScanCallbacks->onResult(pDev);
48+
if (pScan->m_maxResults == 0) {
49+
pScan->erase(pDev);
50+
}
51+
}
52+
53+
/**
54+
* @brief This will schedule an event to run in the host task that will call forceResultCallback
55+
* which will call the onResult callback with the current data.
56+
*/
57+
void NimBLEScan::srTimerCb(ble_npl_event* event) {
58+
NimBLEScan* pScan = static_cast<NimBLEScan*>(ble_npl_event_get_arg(event));
59+
NimBLEAdvertisedDevice* curDev = pScan->m_pWaitingListHead;
60+
ble_npl_time_t now = ble_npl_time_get();
61+
62+
// Process the first device in the waiting list
63+
while (curDev != nullptr) {
64+
// Check if this device's timeout has expired
65+
if (now - curDev->m_time >= pScan->m_srTimeoutTicks) {
66+
NIMBLE_LOGI(LOG_TAG, "Scan response timeout for: %s", curDev->getAddress().toString().c_str());
67+
68+
// Schedule callback for this device
69+
ble_npl_event_set_arg(&pScan->m_srTimeoutEvent, curDev);
70+
ble_npl_eventq_put(nimble_port_get_dflt_eventq(), &pScan->m_srTimeoutEvent);
71+
72+
// Remove this device from waiting list and move to next
73+
NimBLEAdvertisedDevice* next = curDev->m_pNextWaiting;
74+
pScan->removeWaitingDevice(curDev);
75+
curDev = next;
76+
} else {
77+
// The head of the FIFO list is the next device due to time out.
78+
pScan->resetWaitingTimer();
79+
return;
80+
}
81+
}
82+
83+
// No more devices waiting for scan responses
84+
ble_npl_callout_stop(&pScan->m_srTimer);
85+
}
86+
3087
/**
3188
* @brief Scan constructor.
3289
*/
@@ -35,7 +92,11 @@ NimBLEScan::NimBLEScan()
3592
// default interval + window, no whitelist scan filter,not limited scan, no scan response, filter_duplicates
3693
m_scanParams{0, 0, BLE_HCI_SCAN_FILT_NO_WL, 0, 1, 1},
3794
m_pTaskData{nullptr},
38-
m_maxResults{0xFF} {}
95+
m_maxResults{0xFF} {
96+
ble_npl_callout_init(&m_srTimer, nimble_port_get_dflt_eventq(), NimBLEScan::srTimerCb, this);
97+
ble_npl_event_init(&m_srTimeoutEvent, forceResultCallback, NULL);
98+
ble_npl_time_ms_to_ticks(DEFAULT_SCAN_RESP_TIMEOUT_MS, &m_srTimeoutTicks);
99+
} // NimBLEScan::NimBLEScan
39100

40101
/**
41102
* @brief Scan destructor, release any allocated resources.
@@ -44,6 +105,93 @@ NimBLEScan::~NimBLEScan() {
44105
for (const auto& dev : m_scanResults.m_deviceVec) {
45106
delete dev;
46107
}
108+
109+
ble_npl_callout_deinit(&m_srTimer);
110+
ble_npl_event_deinit(&m_srTimeoutEvent);
111+
}
112+
113+
/**
114+
* @brief Add a device to the waiting list for scan responses.
115+
* @param [in] pDev The device to add to the list.
116+
*/
117+
void NimBLEScan::addWaitingDevice(NimBLEAdvertisedDevice* pDev) {
118+
if (pDev == nullptr || pDev->m_pNextWaiting != pDev) {
119+
return; // Invalid or already in list (self-pointer is the "not in list" sentinel)
120+
}
121+
122+
pDev->m_pNextWaiting = nullptr;
123+
if (m_pWaitingListTail == nullptr) {
124+
m_pWaitingListHead = pDev;
125+
m_pWaitingListTail = pDev;
126+
return;
127+
}
128+
129+
m_pWaitingListTail->m_pNextWaiting = pDev;
130+
m_pWaitingListTail = pDev;
131+
}
132+
133+
/**
134+
* @brief Remove a device from the waiting list.
135+
* @param [in] pDev The device to remove from the list.
136+
*/
137+
void NimBLEScan::removeWaitingDevice(NimBLEAdvertisedDevice* pDev) {
138+
if (pDev == nullptr) {
139+
return;
140+
}
141+
142+
if (pDev->m_pNextWaiting == pDev) {
143+
return; // Not in the list
144+
}
145+
146+
if (m_pWaitingListHead == pDev) {
147+
m_pWaitingListHead = pDev->m_pNextWaiting;
148+
if (m_pWaitingListHead == nullptr) {
149+
m_pWaitingListTail = nullptr;
150+
}
151+
} else {
152+
NimBLEAdvertisedDevice* current = m_pWaitingListHead;
153+
while (current != nullptr) {
154+
if (current->m_pNextWaiting == pDev) {
155+
current->m_pNextWaiting = pDev->m_pNextWaiting;
156+
if (m_pWaitingListTail == pDev) {
157+
m_pWaitingListTail = current;
158+
}
159+
break;
160+
}
161+
current = current->m_pNextWaiting;
162+
}
163+
}
164+
165+
pDev->m_pNextWaiting = pDev; // Restore sentinel: self-pointer means "not in list"
166+
}
167+
168+
/**
169+
* @brief Clear all devices from the waiting list.
170+
*/
171+
void NimBLEScan::clearWaitingList() {
172+
NimBLEAdvertisedDevice* current = m_pWaitingListHead;
173+
while (current != nullptr) {
174+
NimBLEAdvertisedDevice* next = current->m_pNextWaiting;
175+
current->m_pNextWaiting = current; // Restore sentinel
176+
current = next;
177+
}
178+
m_pWaitingListHead = nullptr;
179+
m_pWaitingListTail = nullptr;
180+
}
181+
182+
/**
183+
* @brief Reset the timer for the next waiting device at the head of the FIFO list.
184+
*/
185+
void NimBLEScan::resetWaitingTimer() {
186+
if (m_srTimeoutTicks == 0 || m_pWaitingListHead == nullptr) {
187+
ble_npl_callout_stop(&m_srTimer);
188+
return;
189+
}
190+
191+
ble_npl_time_t now = ble_npl_time_get();
192+
ble_npl_time_t elapsed = now - m_pWaitingListHead->m_time;
193+
ble_npl_time_t nextTime = elapsed >= m_srTimeoutTicks ? 1 : m_srTimeoutTicks - elapsed;
194+
ble_npl_callout_reset(&m_srTimer, nextTime);
47195
}
48196

49197
/**
@@ -101,6 +249,8 @@ int NimBLEScan::handleGapEvent(ble_gap_event* event, void* arg) {
101249
// If we haven't seen this device before; create a new instance and insert it in the vector.
102250
// Otherwise just update the relevant parameters of the already known device.
103251
if (advertisedDevice == nullptr) {
252+
pScan->m_stats.incDevCount();
253+
104254
// Check if we have reach the scan results limit, ignore this one if so.
105255
// We still need to store each device when maxResults is 0 to be able to append the scan results
106256
if (pScan->m_maxResults > 0 && pScan->m_maxResults < 0xFF &&
@@ -109,19 +259,38 @@ int NimBLEScan::handleGapEvent(ble_gap_event* event, void* arg) {
109259
}
110260

111261
if (isLegacyAdv && event_type == BLE_HCI_ADV_RPT_EVTYPE_SCAN_RSP) {
262+
pScan->m_stats.incOrphanedSrCount();
112263
NIMBLE_LOGI(LOG_TAG, "Scan response without advertisement: %s", advertisedAddress.toString().c_str());
113264
}
114265

115266
advertisedDevice = new NimBLEAdvertisedDevice(event, event_type);
116267
pScan->m_scanResults.m_deviceVec.push_back(advertisedDevice);
268+
advertisedDevice->m_time = ble_npl_time_get();
117269
NIMBLE_LOGI(LOG_TAG, "New advertiser: %s", advertisedAddress.toString().c_str());
118270
} else {
119271
advertisedDevice->update(event, event_type);
120272
if (isLegacyAdv) {
121273
if (event_type == BLE_HCI_ADV_RPT_EVTYPE_SCAN_RSP) {
274+
pScan->m_stats.recordSrTime(ble_npl_time_get() - advertisedDevice->m_time);
122275
NIMBLE_LOGI(LOG_TAG, "Scan response from: %s", advertisedAddress.toString().c_str());
276+
// Remove device from waiting list since we got the response
277+
pScan->removeWaitingDevice(advertisedDevice);
123278
} else {
279+
pScan->m_stats.incDupCount();
124280
NIMBLE_LOGI(LOG_TAG, "Duplicate; updated: %s", advertisedAddress.toString().c_str());
281+
// Restart scan-response timeout when we see a new non-scan-response
282+
// legacy advertisement during active scanning for a scannable device.
283+
advertisedDevice->m_time = ble_npl_time_get();
284+
advertisedDevice->m_callbackSent = 0;
285+
// Re-add to the tail so FIFO timeout order matches advertisement order.
286+
if (pScan->m_srTimeoutTicks && advertisedDevice->isScannable()) {
287+
bool wasHead = pScan->m_pWaitingListHead == advertisedDevice;
288+
pScan->removeWaitingDevice(advertisedDevice);
289+
pScan->addWaitingDevice(advertisedDevice);
290+
if (wasHead) {
291+
pScan->resetWaitingTimer();
292+
}
293+
}
125294
}
126295
}
127296
}
@@ -147,6 +316,12 @@ int NimBLEScan::handleGapEvent(ble_gap_event* event, void* arg) {
147316
advertisedDevice->m_callbackSent++;
148317
// got the scan response report the full data.
149318
pScan->m_pScanCallbacks->onResult(advertisedDevice);
319+
} else if (pScan->m_srTimeoutTicks && isLegacyAdv && advertisedDevice->isScannable()) {
320+
// Add to waiting list for scan response and start the timer
321+
pScan->addWaitingDevice(advertisedDevice);
322+
if (pScan->m_pWaitingListHead == advertisedDevice) {
323+
pScan->resetWaitingTimer();
324+
}
150325
}
151326

152327
// If not storing results and we have invoked the callback, delete the device.
@@ -158,14 +333,23 @@ int NimBLEScan::handleGapEvent(ble_gap_event* event, void* arg) {
158333
}
159334

160335
case BLE_GAP_EVENT_DISC_COMPLETE: {
336+
ble_npl_callout_stop(&pScan->m_srTimer);
337+
pScan->clearWaitingList();
338+
for (const auto& dev : pScan->m_scanResults.m_deviceVec) {
339+
if (dev->isScannable() && dev->m_callbackSent < 2) {
340+
pScan->m_stats.incMissedSrCount();
341+
pScan->m_pScanCallbacks->onResult(dev);
342+
}
343+
}
344+
161345
NIMBLE_LOGD(LOG_TAG, "discovery complete; reason=%d", event->disc_complete.reason);
346+
NIMBLE_LOGD(LOG_TAG, "%s", pScan->getStatsString().c_str());
162347

348+
pScan->m_pScanCallbacks->onScanEnd(pScan->m_scanResults, event->disc_complete.reason);
163349
if (pScan->m_maxResults == 0) {
164350
pScan->clearResults();
165351
}
166352

167-
pScan->m_pScanCallbacks->onScanEnd(pScan->m_scanResults, event->disc_complete.reason);
168-
169353
if (pScan->m_pTaskData != nullptr) {
170354
NimBLEUtils::taskRelease(*pScan->m_pTaskData, event->disc_complete.reason);
171355
}
@@ -178,6 +362,25 @@ int NimBLEScan::handleGapEvent(ble_gap_event* event, void* arg) {
178362
}
179363
} // handleGapEvent
180364

365+
/**
366+
* @brief Set the scan response timeout.
367+
* @param [in] timeoutMs The timeout in milliseconds to wait for a scan response, default: max advertising interval (10.24s)
368+
* @details If a scan response is not received within the timeout period,
369+
* a dummy scan response with null data will be sent to the scan event handler
370+
* which will trigger the callback with whatever data was in the advertisement.
371+
* If set to 0, no dummy scan response will be sent and the callback will only
372+
* be triggered when a scan response is received from the advertiser or when the scan completes.
373+
*/
374+
void NimBLEScan::setScanResponseTimeout(uint32_t timeoutMs) {
375+
if (timeoutMs == 0) {
376+
ble_npl_callout_stop(&m_srTimer);
377+
m_srTimeoutTicks = 0;
378+
return;
379+
}
380+
381+
ble_npl_time_ms_to_ticks(timeoutMs, &m_srTimeoutTicks);
382+
} // setScanResponseTimeout
383+
181384
/**
182385
* @brief Should we perform an active or passive scan?
183386
* The default is a passive scan. An active scan means that we will request a scan response.
@@ -323,11 +526,13 @@ bool NimBLEScan::start(uint32_t duration, bool isContinue, bool restart) {
323526

324527
if (!isContinue) {
325528
clearResults();
529+
m_stats.reset();
326530
}
327531
}
328532
} else { // Don't clear results while scanning is active
329533
if (!isContinue) {
330534
clearResults();
535+
m_stats.reset();
331536
}
332537
}
333538

@@ -394,6 +599,8 @@ bool NimBLEScan::stop() {
394599
return false;
395600
}
396601

602+
ble_npl_callout_stop(&m_srTimer);
603+
397604
if (m_maxResults == 0) {
398605
clearResults();
399606
}
@@ -414,6 +621,7 @@ void NimBLEScan::erase(const NimBLEAddress& address) {
414621
NIMBLE_LOGD(LOG_TAG, "erase device: %s", address.toString().c_str());
415622
for (auto it = m_scanResults.m_deviceVec.begin(); it != m_scanResults.m_deviceVec.end(); ++it) {
416623
if ((*it)->getAddress() == address) {
624+
removeWaitingDevice(*it);
417625
delete *it;
418626
m_scanResults.m_deviceVec.erase(it);
419627
break;
@@ -429,6 +637,7 @@ void NimBLEScan::erase(const NimBLEAdvertisedDevice* device) {
429637
NIMBLE_LOGD(LOG_TAG, "erase device: %s", device->getAddress().toString().c_str());
430638
for (auto it = m_scanResults.m_deviceVec.begin(); it != m_scanResults.m_deviceVec.end(); ++it) {
431639
if ((*it) == device) {
640+
removeWaitingDevice(*it);
432641
delete *it;
433642
m_scanResults.m_deviceVec.erase(it);
434643
break;
@@ -483,6 +692,7 @@ NimBLEScanResults NimBLEScan::getResults() {
483692
* @brief Clear the stored results of the scan.
484693
*/
485694
void NimBLEScan::clearResults() {
695+
clearWaitingList();
486696
if (m_scanResults.m_deviceVec.size()) {
487697
std::vector<NimBLEAdvertisedDevice*> vSwap{};
488698
ble_npl_hw_enter_critical();

0 commit comments

Comments
 (0)