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+
2734static const char * LOG_TAG = " NimBLEScan" ;
2835static NimBLEScanCallbacks defaultScanCallbacks;
2936
37+ /* *
38+ * @brief This handles an event run in the host task when the scan response timeout for the head of
39+ * the waiting list is triggered and directly invokes the onResult callback with the current device.
40+ */
41+ void NimBLEScan::srTimerCb (ble_npl_event* event) {
42+ auto pScan = NimBLEDevice::getScan ();
43+ auto pDev = pScan->m_pWaitingListHead ;
44+
45+ if (pDev == nullptr ) {
46+ ble_npl_callout_stop (&pScan->m_srTimer );
47+ return ;
48+ }
49+
50+ if (ble_npl_time_get () - pDev->m_time < pScan->m_srTimeoutTicks ) {
51+ // This can happen if a scan response was received and the device was removed from the waiting list
52+ // after this was put in the queue. In this case, just reset the timer for this device.
53+ pScan->resetWaitingTimer ();
54+ return ;
55+ }
56+
57+ NIMBLE_LOGI (LOG_TAG , " Scan response timeout for: %s" , pDev->getAddress ().toString ().c_str ());
58+ pScan->m_stats .incMissedSrCount ();
59+ pScan->removeWaitingDevice (pDev);
60+ pDev->m_callbackSent = 2 ;
61+ pScan->m_pScanCallbacks ->onResult (pDev);
62+ if (pScan->m_maxResults == 0 ) {
63+ pScan->erase (pDev);
64+ }
65+ }
66+
3067/* *
3168 * @brief Scan constructor.
3269 */
@@ -47,17 +84,128 @@ NimBLEScan::NimBLEScan()
4784 },
4885 m_pTaskData{nullptr },
4986 m_maxResults{0xFF } {
50- }
87+ ble_npl_callout_init (&m_srTimer, nimble_port_get_dflt_eventq (), NimBLEScan::srTimerCb, nullptr );
88+ ble_npl_time_ms_to_ticks (DEFAULT_SCAN_RESP_TIMEOUT_MS , &m_srTimeoutTicks);
89+ } // NimBLEScan::NimBLEScan
5190
5291/* *
5392 * @brief Scan destructor, release any allocated resources.
5493 */
5594NimBLEScan::~NimBLEScan () {
95+ ble_npl_callout_deinit (&m_srTimer);
96+
5697 for (const auto & dev : m_scanResults.m_deviceVec ) {
5798 delete dev;
5899 }
59100}
60101
102+ /* *
103+ * @brief Add a device to the waiting list for scan responses.
104+ * @param [in] pDev The device to add to the list.
105+ */
106+ void NimBLEScan::addWaitingDevice (NimBLEAdvertisedDevice* pDev) {
107+ if (pDev == nullptr ) {
108+ return ;
109+ }
110+
111+ ble_npl_hw_enter_critical ();
112+
113+ // Self-pointer is the "not in list" sentinel; anything else means already in list.
114+ if (pDev->m_pNextWaiting != pDev) {
115+ ble_npl_hw_exit_critical (0 );
116+ return ;
117+ }
118+
119+ // Initialize link field before inserting into the list.
120+ pDev->m_pNextWaiting = nullptr ;
121+ if (m_pWaitingListTail == nullptr ) {
122+ m_pWaitingListHead = pDev;
123+ m_pWaitingListTail = pDev;
124+ ble_npl_hw_exit_critical (0 );
125+ return ;
126+ }
127+
128+ m_pWaitingListTail->m_pNextWaiting = pDev;
129+ m_pWaitingListTail = pDev;
130+ ble_npl_hw_exit_critical (0 );
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+ bool resetTimer = false ;
147+ ble_npl_hw_enter_critical ();
148+ if (m_pWaitingListHead == pDev) {
149+ m_pWaitingListHead = pDev->m_pNextWaiting ;
150+ if (m_pWaitingListHead == nullptr ) {
151+ m_pWaitingListTail = nullptr ;
152+ } else {
153+ resetTimer = true ;
154+ }
155+ } else {
156+ NimBLEAdvertisedDevice* current = m_pWaitingListHead;
157+ while (current != nullptr ) {
158+ if (current->m_pNextWaiting == pDev) {
159+ current->m_pNextWaiting = pDev->m_pNextWaiting ;
160+ if (m_pWaitingListTail == pDev) {
161+ m_pWaitingListTail = current;
162+ }
163+ break ;
164+ }
165+ current = current->m_pNextWaiting ;
166+ }
167+ }
168+ ble_npl_hw_exit_critical (0 );
169+ pDev->m_pNextWaiting = pDev; // Restore sentinel: self-pointer means "not in list"
170+ if (resetTimer) {
171+ resetWaitingTimer ();
172+ }
173+ }
174+
175+ /* *
176+ * @brief Clear all devices from the waiting list.
177+ */
178+ void NimBLEScan::clearWaitingList () {
179+ // Stop the timer and remove any pending timeout events since we're clearing
180+ // the list and won't be processing any more timeouts for these devices
181+ ble_npl_callout_stop (&m_srTimer);
182+ ble_npl_hw_enter_critical ();
183+ NimBLEAdvertisedDevice* current = m_pWaitingListHead;
184+ while (current != nullptr ) {
185+ NimBLEAdvertisedDevice* next = current->m_pNextWaiting ;
186+ current->m_pNextWaiting = current; // Restore sentinel
187+ current = next;
188+ }
189+ m_pWaitingListHead = nullptr ;
190+ m_pWaitingListTail = nullptr ;
191+ ble_npl_hw_exit_critical (0 );
192+ }
193+
194+ /* *
195+ * @brief Reset the timer for the next waiting device at the head of the FIFO list.
196+ */
197+ void NimBLEScan::resetWaitingTimer () {
198+ if (m_srTimeoutTicks == 0 || m_pWaitingListHead == nullptr ) {
199+ ble_npl_callout_stop (&m_srTimer);
200+ return ;
201+ }
202+
203+ ble_npl_time_t now = ble_npl_time_get ();
204+ ble_npl_time_t elapsed = now - m_pWaitingListHead->m_time ;
205+ ble_npl_time_t nextTime = elapsed >= m_srTimeoutTicks ? 1 : m_srTimeoutTicks - elapsed;
206+ ble_npl_callout_reset (&m_srTimer, nextTime);
207+ }
208+
61209/* *
62210 * @brief Handle GAP events related to scans.
63211 * @param [in] event The event type for this event.
@@ -113,6 +261,8 @@ int NimBLEScan::handleGapEvent(ble_gap_event* event, void* arg) {
113261 // If we haven't seen this device before; create a new instance and insert it in the vector.
114262 // Otherwise just update the relevant parameters of the already known device.
115263 if (advertisedDevice == nullptr ) {
264+ pScan->m_stats .incDevCount ();
265+
116266 // Check if we have reach the scan results limit, ignore this one if so.
117267 // We still need to store each device when maxResults is 0 to be able to append the scan results
118268 if (pScan->m_maxResults > 0 && pScan->m_maxResults < 0xFF &&
@@ -121,19 +271,39 @@ int NimBLEScan::handleGapEvent(ble_gap_event* event, void* arg) {
121271 }
122272
123273 if (isLegacyAdv && event_type == BLE_HCI_ADV_RPT_EVTYPE_SCAN_RSP ) {
274+ pScan->m_stats .incOrphanedSrCount ();
124275 NIMBLE_LOGI (LOG_TAG , " Scan response without advertisement: %s" , advertisedAddress.toString ().c_str ());
125276 }
126277
127278 advertisedDevice = new NimBLEAdvertisedDevice (event, event_type);
128279 pScan->m_scanResults .m_deviceVec .push_back (advertisedDevice);
280+ advertisedDevice->m_time = ble_npl_time_get ();
129281 NIMBLE_LOGI (LOG_TAG , " New advertiser: %s" , advertisedAddress.toString ().c_str ());
130282 } else {
131283 advertisedDevice->update (event, event_type);
132284 if (isLegacyAdv) {
133285 if (event_type == BLE_HCI_ADV_RPT_EVTYPE_SCAN_RSP ) {
286+ pScan->m_stats .recordSrTime (ble_npl_time_get () - advertisedDevice->m_time );
134287 NIMBLE_LOGI (LOG_TAG , " Scan response from: %s" , advertisedAddress.toString ().c_str ());
288+ // Remove device from waiting list since we got the response
289+ pScan->removeWaitingDevice (advertisedDevice);
135290 } else {
291+ pScan->m_stats .incDupCount ();
136292 NIMBLE_LOGI (LOG_TAG , " Duplicate; updated: %s" , advertisedAddress.toString ().c_str ());
293+ // Restart scan-response timeout when we see a new non-scan-response
294+ // legacy advertisement during active scanning for a scannable device.
295+ advertisedDevice->m_time = ble_npl_time_get ();
296+ // Re-add to the tail so FIFO timeout order matches advertisement order.
297+ if (advertisedDevice->isScannable ()) {
298+ pScan->removeWaitingDevice (advertisedDevice);
299+ pScan->addWaitingDevice (advertisedDevice);
300+ }
301+
302+ // If we're not filtering duplicates, we need to reset the callbackSent count
303+ // so that callbacks will be triggered again for this device
304+ if (!pScan->m_scanParams .filter_duplicates ) {
305+ advertisedDevice->m_callbackSent = 0 ;
306+ }
137307 }
138308 }
139309 }
@@ -159,6 +329,12 @@ int NimBLEScan::handleGapEvent(ble_gap_event* event, void* arg) {
159329 advertisedDevice->m_callbackSent ++;
160330 // got the scan response report the full data.
161331 pScan->m_pScanCallbacks ->onResult (advertisedDevice);
332+ } else if (isLegacyAdv && advertisedDevice->isScannable ()) {
333+ // Add to waiting list for scan response and start the timer
334+ pScan->addWaitingDevice (advertisedDevice);
335+ if (pScan->m_pWaitingListHead == advertisedDevice) {
336+ pScan->resetWaitingTimer ();
337+ }
162338 }
163339
164340 // If not storing results and we have invoked the callback, delete the device.
@@ -170,12 +346,26 @@ int NimBLEScan::handleGapEvent(ble_gap_event* event, void* arg) {
170346 }
171347
172348 case BLE_GAP_EVENT_DISC_COMPLETE : {
173- NIMBLE_LOGD (LOG_TAG , " discovery complete; reason=%d" , event->disc_complete .reason );
349+ ble_npl_callout_stop (&pScan->m_srTimer );
350+
351+ // If we have any scannable devices that haven't received a scan response,
352+ // we should trigger the callback with whatever data we have since the scan is complete
353+ // and we won't be getting any more updates for these devices.
354+ while (pScan->m_pWaitingListHead != nullptr ) {
355+ auto pDev = pScan->m_pWaitingListHead ;
356+ pScan->m_stats .incMissedSrCount ();
357+ pScan->removeWaitingDevice (pDev);
358+ pDev->m_callbackSent = 2 ;
359+ pScan->m_pScanCallbacks ->onResult (pDev);
360+ }
174361
175362 if (pScan->m_maxResults == 0 ) {
176363 pScan->clearResults ();
177364 }
178365
366+ NIMBLE_LOGD (LOG_TAG , " discovery complete; reason=%d" , event->disc_complete .reason );
367+ NIMBLE_LOGD (LOG_TAG , " %s" , pScan->getStatsString ().c_str ());
368+
179369 pScan->m_pScanCallbacks ->onScanEnd (pScan->m_scanResults , event->disc_complete .reason );
180370
181371 if (pScan->m_pTaskData != nullptr ) {
@@ -190,6 +380,27 @@ int NimBLEScan::handleGapEvent(ble_gap_event* event, void* arg) {
190380 }
191381} // handleGapEvent
192382
383+ /* *
384+ * @brief Set the scan response timeout.
385+ * @param [in] timeoutMs The timeout in milliseconds to wait for a scan response, default: max advertising interval (10.24s)
386+ * @details If a scan response is not received within the timeout period,
387+ * the pending device will be reported to the scan result callback with whatever
388+ * data was present in the advertisement; no synthetic scan-response event is generated.
389+ * If set to 0, the scan result callback will only be triggered when a scan response
390+ * is received from the advertiser or when the scan completes, at which point any
391+ * pending scannable devices will be reported with the advertisement data only.
392+ */
393+ void NimBLEScan::setScanResponseTimeout (uint32_t timeoutMs) {
394+ if (timeoutMs == 0 ) {
395+ ble_npl_callout_stop (&m_srTimer);
396+ m_srTimeoutTicks = 0 ;
397+ return ;
398+ }
399+
400+ ble_npl_time_ms_to_ticks (timeoutMs, &m_srTimeoutTicks);
401+ resetWaitingTimer ();
402+ } // setScanResponseTimeout
403+
193404/* *
194405 * @brief Should we perform an active or passive scan?
195406 * The default is a passive scan. An active scan means that we will request a scan response.
@@ -220,7 +431,7 @@ void NimBLEScan::setDuplicateFilter(uint8_t enabled) {
220431 */
221432void NimBLEScan::setLimitedOnly (bool enabled) {
222433 m_scanParams.limited = enabled;
223- } // setLimited
434+ } // setLimitedOnly
224435
225436/* *
226437 * @brief Sets the scan filter policy.
@@ -335,11 +546,13 @@ bool NimBLEScan::start(uint32_t duration, bool isContinue, bool restart) {
335546
336547 if (!isContinue) {
337548 clearResults ();
549+ m_stats.reset ();
338550 }
339551 }
340552 } else { // Don't clear results while scanning is active
341553 if (!isContinue) {
342554 clearResults ();
555+ m_stats.reset ();
343556 }
344557 }
345558
@@ -406,6 +619,8 @@ bool NimBLEScan::stop() {
406619 return false ;
407620 }
408621
622+ clearWaitingList ();
623+
409624 if (m_maxResults == 0 ) {
410625 clearResults ();
411626 }
@@ -426,6 +641,7 @@ void NimBLEScan::erase(const NimBLEAddress& address) {
426641 NIMBLE_LOGD (LOG_TAG , " erase device: %s" , address.toString ().c_str ());
427642 for (auto it = m_scanResults.m_deviceVec .begin (); it != m_scanResults.m_deviceVec .end (); ++it) {
428643 if ((*it)->getAddress () == address) {
644+ removeWaitingDevice (*it);
429645 delete *it;
430646 m_scanResults.m_deviceVec .erase (it);
431647 break ;
@@ -441,6 +657,7 @@ void NimBLEScan::erase(const NimBLEAdvertisedDevice* device) {
441657 NIMBLE_LOGD (LOG_TAG , " erase device: %s" , device->getAddress ().toString ().c_str ());
442658 for (auto it = m_scanResults.m_deviceVec .begin (); it != m_scanResults.m_deviceVec .end (); ++it) {
443659 if ((*it) == device) {
660+ removeWaitingDevice (*it);
444661 delete *it;
445662 m_scanResults.m_deviceVec .erase (it);
446663 break ;
@@ -495,6 +712,12 @@ NimBLEScanResults NimBLEScan::getResults() {
495712 * @brief Clear the stored results of the scan.
496713 */
497714void NimBLEScan::clearResults () {
715+ if (isScanning ()) {
716+ NIMBLE_LOGW (LOG_TAG , " Cannot clear results while scan is active" );
717+ return ;
718+ }
719+
720+ clearWaitingList ();
498721 if (m_scanResults.m_deviceVec .size ()) {
499722 std::vector<NimBLEAdvertisedDevice*> vSwap{};
500723 ble_npl_hw_enter_critical ();
0 commit comments