Skip to content

Commit 7a5a25a

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 7a5a25a

4 files changed

Lines changed: 319 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: 212 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,43 @@
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 This will schedule an event to run in the host task that will call forceResultCallback
39+
* which will call the onResult callback with the current data.
40+
*/
41+
void NimBLEScan::srTimerCb(ble_npl_event* event) {
42+
auto* pScan = NimBLEDevice::getScan();
43+
auto curDev = pScan->m_pWaitingListHead;
44+
45+
if (curDev == nullptr) {
46+
ble_npl_callout_stop(&pScan->m_srTimer);
47+
return;
48+
}
49+
50+
NIMBLE_LOGI(LOG_TAG, "Scan response timeout for: %s", curDev->getAddress().toString().c_str());
51+
52+
pScan->removeWaitingDevice(curDev);
53+
curDev->m_callbackSent = 2;
54+
pScan->m_pScanCallbacks->onResult(curDev);
55+
if (pScan->m_maxResults == 0) {
56+
pScan->erase(curDev);
57+
}
58+
}
59+
3060
/**
3161
* @brief Scan constructor.
3262
*/
@@ -35,7 +65,10 @@ NimBLEScan::NimBLEScan()
3565
// default interval + window, no whitelist scan filter,not limited scan, no scan response, filter_duplicates
3666
m_scanParams{0, 0, BLE_HCI_SCAN_FILT_NO_WL, 0, 1, 1},
3767
m_pTaskData{nullptr},
38-
m_maxResults{0xFF} {}
68+
m_maxResults{0xFF} {
69+
ble_npl_callout_init(&m_srTimer, nimble_port_get_dflt_eventq(), NimBLEScan::srTimerCb, nullptr);
70+
ble_npl_time_ms_to_ticks(DEFAULT_SCAN_RESP_TIMEOUT_MS, &m_srTimeoutTicks);
71+
} // NimBLEScan::NimBLEScan
3972

4073
/**
4174
* @brief Scan destructor, release any allocated resources.
@@ -44,6 +77,108 @@ NimBLEScan::~NimBLEScan() {
4477
for (const auto& dev : m_scanResults.m_deviceVec) {
4578
delete dev;
4679
}
80+
81+
ble_npl_callout_deinit(&m_srTimer);
82+
}
83+
84+
/**
85+
* @brief Add a device to the waiting list for scan responses.
86+
* @param [in] pDev The device to add to the list.
87+
*/
88+
void NimBLEScan::addWaitingDevice(NimBLEAdvertisedDevice* pDev) {
89+
if (pDev == nullptr || pDev->m_pNextWaiting != pDev) {
90+
return; // Invalid or already in list (self-pointer is the "not in list" sentinel)
91+
}
92+
93+
pDev->m_pNextWaiting = nullptr;
94+
if (m_pWaitingListTail == nullptr) {
95+
ble_npl_hw_enter_critical();
96+
m_pWaitingListHead = pDev;
97+
m_pWaitingListTail = pDev;
98+
ble_npl_hw_exit_critical(0);
99+
return;
100+
}
101+
102+
ble_npl_hw_enter_critical();
103+
m_pWaitingListTail->m_pNextWaiting = pDev;
104+
m_pWaitingListTail = pDev;
105+
ble_npl_hw_exit_critical(0);
106+
}
107+
108+
/**
109+
* @brief Remove a device from the waiting list.
110+
* @param [in] pDev The device to remove from the list.
111+
*/
112+
void NimBLEScan::removeWaitingDevice(NimBLEAdvertisedDevice* pDev) {
113+
if (pDev == nullptr) {
114+
return;
115+
}
116+
117+
if (pDev->m_pNextWaiting == pDev) {
118+
return; // Not in the list
119+
}
120+
121+
bool resetTimer = false;
122+
ble_npl_hw_enter_critical();
123+
if (m_pWaitingListHead == pDev) {
124+
m_pWaitingListHead = pDev->m_pNextWaiting;
125+
if (m_pWaitingListHead == nullptr) {
126+
m_pWaitingListTail = nullptr;
127+
} else {
128+
resetTimer = true;
129+
}
130+
} else {
131+
NimBLEAdvertisedDevice* current = m_pWaitingListHead;
132+
while (current != nullptr) {
133+
if (current->m_pNextWaiting == pDev) {
134+
current->m_pNextWaiting = pDev->m_pNextWaiting;
135+
if (m_pWaitingListTail == pDev) {
136+
m_pWaitingListTail = current;
137+
}
138+
break;
139+
}
140+
current = current->m_pNextWaiting;
141+
}
142+
}
143+
ble_npl_hw_exit_critical(0);
144+
pDev->m_pNextWaiting = pDev; // Restore sentinel: self-pointer means "not in list"
145+
if (resetTimer) {
146+
resetWaitingTimer();
147+
}
148+
}
149+
150+
/**
151+
* @brief Clear all devices from the waiting list.
152+
*/
153+
void NimBLEScan::clearWaitingList() {
154+
// Stop the timer and remove any pending timeout events since we're clearing
155+
// the list and won't be processing any more timeouts for these devices
156+
ble_npl_callout_stop(&m_srTimer);
157+
ble_npl_hw_enter_critical();
158+
NimBLEAdvertisedDevice* current = m_pWaitingListHead;
159+
while (current != nullptr) {
160+
NimBLEAdvertisedDevice* next = current->m_pNextWaiting;
161+
current->m_pNextWaiting = current; // Restore sentinel
162+
current = next;
163+
}
164+
m_pWaitingListHead = nullptr;
165+
m_pWaitingListTail = nullptr;
166+
ble_npl_hw_exit_critical(0);
167+
}
168+
169+
/**
170+
* @brief Reset the timer for the next waiting device at the head of the FIFO list.
171+
*/
172+
void NimBLEScan::resetWaitingTimer() {
173+
if (m_srTimeoutTicks == 0 || m_pWaitingListHead == nullptr) {
174+
ble_npl_callout_stop(&m_srTimer);
175+
return;
176+
}
177+
178+
ble_npl_time_t now = ble_npl_time_get();
179+
ble_npl_time_t elapsed = now - m_pWaitingListHead->m_time;
180+
ble_npl_time_t nextTime = elapsed >= m_srTimeoutTicks ? 1 : m_srTimeoutTicks - elapsed;
181+
ble_npl_callout_reset(&m_srTimer, nextTime);
47182
}
48183

49184
/**
@@ -101,6 +236,8 @@ int NimBLEScan::handleGapEvent(ble_gap_event* event, void* arg) {
101236
// If we haven't seen this device before; create a new instance and insert it in the vector.
102237
// Otherwise just update the relevant parameters of the already known device.
103238
if (advertisedDevice == nullptr) {
239+
pScan->m_stats.incDevCount();
240+
104241
// Check if we have reach the scan results limit, ignore this one if so.
105242
// We still need to store each device when maxResults is 0 to be able to append the scan results
106243
if (pScan->m_maxResults > 0 && pScan->m_maxResults < 0xFF &&
@@ -109,19 +246,39 @@ int NimBLEScan::handleGapEvent(ble_gap_event* event, void* arg) {
109246
}
110247

111248
if (isLegacyAdv && event_type == BLE_HCI_ADV_RPT_EVTYPE_SCAN_RSP) {
249+
pScan->m_stats.incOrphanedSrCount();
112250
NIMBLE_LOGI(LOG_TAG, "Scan response without advertisement: %s", advertisedAddress.toString().c_str());
113251
}
114252

115253
advertisedDevice = new NimBLEAdvertisedDevice(event, event_type);
116254
pScan->m_scanResults.m_deviceVec.push_back(advertisedDevice);
255+
advertisedDevice->m_time = ble_npl_time_get();
117256
NIMBLE_LOGI(LOG_TAG, "New advertiser: %s", advertisedAddress.toString().c_str());
118257
} else {
119258
advertisedDevice->update(event, event_type);
120259
if (isLegacyAdv) {
121260
if (event_type == BLE_HCI_ADV_RPT_EVTYPE_SCAN_RSP) {
261+
pScan->m_stats.recordSrTime(ble_npl_time_get() - advertisedDevice->m_time);
122262
NIMBLE_LOGI(LOG_TAG, "Scan response from: %s", advertisedAddress.toString().c_str());
263+
// Remove device from waiting list since we got the response
264+
pScan->removeWaitingDevice(advertisedDevice);
123265
} else {
266+
pScan->m_stats.incDupCount();
124267
NIMBLE_LOGI(LOG_TAG, "Duplicate; updated: %s", advertisedAddress.toString().c_str());
268+
// Restart scan-response timeout when we see a new non-scan-response
269+
// legacy advertisement during active scanning for a scannable device.
270+
advertisedDevice->m_time = ble_npl_time_get();
271+
// Re-add to the tail so FIFO timeout order matches advertisement order.
272+
if (pScan->m_srTimeoutTicks && advertisedDevice->isScannable()) {
273+
pScan->removeWaitingDevice(advertisedDevice);
274+
pScan->addWaitingDevice(advertisedDevice);
275+
}
276+
277+
// If we're not filtering duplicates, we need to reset the callbackSent count
278+
// so that callbacks will be triggered again for this device
279+
if (!pScan->m_scanParams.filter_duplicates) {
280+
advertisedDevice->m_callbackSent = 0;
281+
}
125282
}
126283
}
127284
}
@@ -147,6 +304,12 @@ int NimBLEScan::handleGapEvent(ble_gap_event* event, void* arg) {
147304
advertisedDevice->m_callbackSent++;
148305
// got the scan response report the full data.
149306
pScan->m_pScanCallbacks->onResult(advertisedDevice);
307+
} else if (pScan->m_srTimeoutTicks && isLegacyAdv && advertisedDevice->isScannable()) {
308+
// Add to waiting list for scan response and start the timer
309+
pScan->addWaitingDevice(advertisedDevice);
310+
if (pScan->m_pWaitingListHead == advertisedDevice) {
311+
pScan->resetWaitingTimer();
312+
}
150313
}
151314

152315
// If not storing results and we have invoked the callback, delete the device.
@@ -158,14 +321,34 @@ int NimBLEScan::handleGapEvent(ble_gap_event* event, void* arg) {
158321
}
159322

160323
case BLE_GAP_EVENT_DISC_COMPLETE: {
324+
pScan->clearWaitingList();
325+
// If we have any scannable devices that haven't received a scan response,
326+
// we should trigger the callback with whatever data we have since the scan is complete
327+
// and we won't be getting any more updates for these devices.
328+
329+
// Make a copy in case the callback modifies the vector (e.g. by calling clearResults)
330+
std::vector<NimBLEAdvertisedDevice*> pending{};
331+
pending.reserve(pScan->m_scanResults.m_deviceVec.size());
332+
for (const auto& dev : pScan->m_scanResults.m_deviceVec) {
333+
if (dev->isScannable() && dev->m_callbackSent < 2) {
334+
pScan->m_stats.incMissedSrCount();
335+
dev->m_callbackSent = 2;
336+
pending.push_back(dev);
337+
}
338+
}
339+
340+
for (const auto& dev : pending) {
341+
pScan->m_pScanCallbacks->onResult(dev);
342+
}
343+
161344
NIMBLE_LOGD(LOG_TAG, "discovery complete; reason=%d", event->disc_complete.reason);
345+
NIMBLE_LOGD(LOG_TAG, "%s", pScan->getStatsString().c_str());
162346

347+
pScan->m_pScanCallbacks->onScanEnd(pScan->m_scanResults, event->disc_complete.reason);
163348
if (pScan->m_maxResults == 0) {
164349
pScan->clearResults();
165350
}
166351

167-
pScan->m_pScanCallbacks->onScanEnd(pScan->m_scanResults, event->disc_complete.reason);
168-
169352
if (pScan->m_pTaskData != nullptr) {
170353
NimBLEUtils::taskRelease(*pScan->m_pTaskData, event->disc_complete.reason);
171354
}
@@ -178,6 +361,25 @@ int NimBLEScan::handleGapEvent(ble_gap_event* event, void* arg) {
178361
}
179362
} // handleGapEvent
180363

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

324526
if (!isContinue) {
325527
clearResults();
528+
m_stats.reset();
326529
}
327530
}
328531
} else { // Don't clear results while scanning is active
329532
if (!isContinue) {
330533
clearResults();
534+
m_stats.reset();
331535
}
332536
}
333537

@@ -394,6 +598,8 @@ bool NimBLEScan::stop() {
394598
return false;
395599
}
396600

601+
clearWaitingList();
602+
397603
if (m_maxResults == 0) {
398604
clearResults();
399605
}
@@ -414,6 +620,7 @@ void NimBLEScan::erase(const NimBLEAddress& address) {
414620
NIMBLE_LOGD(LOG_TAG, "erase device: %s", address.toString().c_str());
415621
for (auto it = m_scanResults.m_deviceVec.begin(); it != m_scanResults.m_deviceVec.end(); ++it) {
416622
if ((*it)->getAddress() == address) {
623+
removeWaitingDevice(*it);
417624
delete *it;
418625
m_scanResults.m_deviceVec.erase(it);
419626
break;
@@ -429,6 +636,7 @@ void NimBLEScan::erase(const NimBLEAdvertisedDevice* device) {
429636
NIMBLE_LOGD(LOG_TAG, "erase device: %s", device->getAddress().toString().c_str());
430637
for (auto it = m_scanResults.m_deviceVec.begin(); it != m_scanResults.m_deviceVec.end(); ++it) {
431638
if ((*it) == device) {
639+
removeWaitingDevice(*it);
432640
delete *it;
433641
m_scanResults.m_deviceVec.erase(it);
434642
break;
@@ -483,6 +691,7 @@ NimBLEScanResults NimBLEScan::getResults() {
483691
* @brief Clear the stored results of the scan.
484692
*/
485693
void NimBLEScan::clearResults() {
694+
clearWaitingList();
486695
if (m_scanResults.m_deviceVec.size()) {
487696
std::vector<NimBLEAdvertisedDevice*> vSwap{};
488697
ble_npl_hw_enter_critical();

0 commit comments

Comments
 (0)