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 */
@@ -35,17 +72,129 @@ NimBLEScan::NimBLEScan()
3572 // default interval + window, no whitelist scan filter,not limited scan, no scan response, filter_duplicates
3673 m_scanParams{0 , 0 , BLE_HCI_SCAN_FILT_NO_WL , 0 , 1 , 1 },
3774 m_pTaskData{nullptr },
38- m_maxResults{0xFF } {}
75+ m_maxResults{0xFF } {
76+ ble_npl_callout_init (&m_srTimer, nimble_port_get_dflt_eventq (), NimBLEScan::srTimerCb, nullptr );
77+ ble_npl_time_ms_to_ticks (DEFAULT_SCAN_RESP_TIMEOUT_MS , &m_srTimeoutTicks);
78+ } // NimBLEScan::NimBLEScan
3979
4080/* *
4181 * @brief Scan destructor, release any allocated resources.
4282 */
4383NimBLEScan::~NimBLEScan () {
84+ ble_npl_callout_deinit (&m_srTimer);
85+
4486 for (const auto & dev : m_scanResults.m_deviceVec ) {
4587 delete dev;
4688 }
4789}
4890
91+ /* *
92+ * @brief Add a device to the waiting list for scan responses.
93+ * @param [in] pDev The device to add to the list.
94+ */
95+ void NimBLEScan::addWaitingDevice (NimBLEAdvertisedDevice* pDev) {
96+ if (pDev == nullptr ) {
97+ return ;
98+ }
99+
100+ ble_npl_hw_enter_critical ();
101+
102+ // Self-pointer is the "not in list" sentinel; anything else means already in list.
103+ if (pDev->m_pNextWaiting != pDev) {
104+ ble_npl_hw_exit_critical (0 );
105+ return ;
106+ }
107+
108+ // Initialize link field before inserting into the list.
109+ pDev->m_pNextWaiting = nullptr ;
110+ if (m_pWaitingListTail == nullptr ) {
111+ m_pWaitingListHead = pDev;
112+ m_pWaitingListTail = pDev;
113+ ble_npl_hw_exit_critical (0 );
114+ return ;
115+ }
116+
117+ m_pWaitingListTail->m_pNextWaiting = pDev;
118+ m_pWaitingListTail = pDev;
119+ ble_npl_hw_exit_critical (0 );
120+ }
121+
122+ /* *
123+ * @brief Remove a device from the waiting list.
124+ * @param [in] pDev The device to remove from the list.
125+ */
126+ void NimBLEScan::removeWaitingDevice (NimBLEAdvertisedDevice* pDev) {
127+ if (pDev == nullptr ) {
128+ return ;
129+ }
130+
131+ if (pDev->m_pNextWaiting == pDev) {
132+ return ; // Not in the list
133+ }
134+
135+ bool resetTimer = false ;
136+ ble_npl_hw_enter_critical ();
137+ if (m_pWaitingListHead == pDev) {
138+ m_pWaitingListHead = pDev->m_pNextWaiting ;
139+ if (m_pWaitingListHead == nullptr ) {
140+ m_pWaitingListTail = nullptr ;
141+ } else {
142+ resetTimer = true ;
143+ }
144+ } else {
145+ NimBLEAdvertisedDevice* current = m_pWaitingListHead;
146+ while (current != nullptr ) {
147+ if (current->m_pNextWaiting == pDev) {
148+ current->m_pNextWaiting = pDev->m_pNextWaiting ;
149+ if (m_pWaitingListTail == pDev) {
150+ m_pWaitingListTail = current;
151+ }
152+ break ;
153+ }
154+ current = current->m_pNextWaiting ;
155+ }
156+ }
157+ ble_npl_hw_exit_critical (0 );
158+ pDev->m_pNextWaiting = pDev; // Restore sentinel: self-pointer means "not in list"
159+ if (resetTimer) {
160+ resetWaitingTimer ();
161+ }
162+ }
163+
164+ /* *
165+ * @brief Clear all devices from the waiting list.
166+ */
167+ void NimBLEScan::clearWaitingList () {
168+ // Stop the timer and remove any pending timeout events since we're clearing
169+ // the list and won't be processing any more timeouts for these devices
170+ ble_npl_callout_stop (&m_srTimer);
171+ ble_npl_hw_enter_critical ();
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+ ble_npl_hw_exit_critical (0 );
181+ }
182+
183+ /* *
184+ * @brief Reset the timer for the next waiting device at the head of the FIFO list.
185+ */
186+ void NimBLEScan::resetWaitingTimer () {
187+ if (m_srTimeoutTicks == 0 || m_pWaitingListHead == nullptr ) {
188+ ble_npl_callout_stop (&m_srTimer);
189+ return ;
190+ }
191+
192+ ble_npl_time_t now = ble_npl_time_get ();
193+ ble_npl_time_t elapsed = now - m_pWaitingListHead->m_time ;
194+ ble_npl_time_t nextTime = elapsed >= m_srTimeoutTicks ? 1 : m_srTimeoutTicks - elapsed;
195+ ble_npl_callout_reset (&m_srTimer, nextTime);
196+ }
197+
49198/* *
50199 * @brief Handle GAP events related to scans.
51200 * @param [in] event The event type for this event.
@@ -101,6 +250,8 @@ int NimBLEScan::handleGapEvent(ble_gap_event* event, void* arg) {
101250 // If we haven't seen this device before; create a new instance and insert it in the vector.
102251 // Otherwise just update the relevant parameters of the already known device.
103252 if (advertisedDevice == nullptr ) {
253+ pScan->m_stats .incDevCount ();
254+
104255 // Check if we have reach the scan results limit, ignore this one if so.
105256 // We still need to store each device when maxResults is 0 to be able to append the scan results
106257 if (pScan->m_maxResults > 0 && pScan->m_maxResults < 0xFF &&
@@ -109,19 +260,39 @@ int NimBLEScan::handleGapEvent(ble_gap_event* event, void* arg) {
109260 }
110261
111262 if (isLegacyAdv && event_type == BLE_HCI_ADV_RPT_EVTYPE_SCAN_RSP ) {
263+ pScan->m_stats .incOrphanedSrCount ();
112264 NIMBLE_LOGI (LOG_TAG , " Scan response without advertisement: %s" , advertisedAddress.toString ().c_str ());
113265 }
114266
115267 advertisedDevice = new NimBLEAdvertisedDevice (event, event_type);
116268 pScan->m_scanResults .m_deviceVec .push_back (advertisedDevice);
269+ advertisedDevice->m_time = ble_npl_time_get ();
117270 NIMBLE_LOGI (LOG_TAG , " New advertiser: %s" , advertisedAddress.toString ().c_str ());
118271 } else {
119272 advertisedDevice->update (event, event_type);
120273 if (isLegacyAdv) {
121274 if (event_type == BLE_HCI_ADV_RPT_EVTYPE_SCAN_RSP ) {
275+ pScan->m_stats .recordSrTime (ble_npl_time_get () - advertisedDevice->m_time );
122276 NIMBLE_LOGI (LOG_TAG , " Scan response from: %s" , advertisedAddress.toString ().c_str ());
277+ // Remove device from waiting list since we got the response
278+ pScan->removeWaitingDevice (advertisedDevice);
123279 } else {
280+ pScan->m_stats .incDupCount ();
124281 NIMBLE_LOGI (LOG_TAG , " Duplicate; updated: %s" , advertisedAddress.toString ().c_str ());
282+ // Restart scan-response timeout when we see a new non-scan-response
283+ // legacy advertisement during active scanning for a scannable device.
284+ advertisedDevice->m_time = ble_npl_time_get ();
285+ // Re-add to the tail so FIFO timeout order matches advertisement order.
286+ if (advertisedDevice->isScannable ()) {
287+ pScan->removeWaitingDevice (advertisedDevice);
288+ pScan->addWaitingDevice (advertisedDevice);
289+ }
290+
291+ // If we're not filtering duplicates, we need to reset the callbackSent count
292+ // so that callbacks will be triggered again for this device
293+ if (!pScan->m_scanParams .filter_duplicates ) {
294+ advertisedDevice->m_callbackSent = 0 ;
295+ }
125296 }
126297 }
127298 }
@@ -147,6 +318,12 @@ int NimBLEScan::handleGapEvent(ble_gap_event* event, void* arg) {
147318 advertisedDevice->m_callbackSent ++;
148319 // got the scan response report the full data.
149320 pScan->m_pScanCallbacks ->onResult (advertisedDevice);
321+ } else if (isLegacyAdv && advertisedDevice->isScannable ()) {
322+ // Add to waiting list for scan response and start the timer
323+ pScan->addWaitingDevice (advertisedDevice);
324+ if (pScan->m_pWaitingListHead == advertisedDevice) {
325+ pScan->resetWaitingTimer ();
326+ }
150327 }
151328
152329 // If not storing results and we have invoked the callback, delete the device.
@@ -158,12 +335,26 @@ int NimBLEScan::handleGapEvent(ble_gap_event* event, void* arg) {
158335 }
159336
160337 case BLE_GAP_EVENT_DISC_COMPLETE : {
161- NIMBLE_LOGD (LOG_TAG , " discovery complete; reason=%d" , event->disc_complete .reason );
338+ ble_npl_callout_stop (&pScan->m_srTimer );
339+
340+ // If we have any scannable devices that haven't received a scan response,
341+ // we should trigger the callback with whatever data we have since the scan is complete
342+ // and we won't be getting any more updates for these devices.
343+ while (pScan->m_pWaitingListHead != nullptr ) {
344+ auto pDev = pScan->m_pWaitingListHead ;
345+ pScan->m_stats .incMissedSrCount ();
346+ pScan->removeWaitingDevice (pDev);
347+ pDev->m_callbackSent = 2 ;
348+ pScan->m_pScanCallbacks ->onResult (pDev);
349+ }
162350
163351 if (pScan->m_maxResults == 0 ) {
164352 pScan->clearResults ();
165353 }
166354
355+ NIMBLE_LOGD (LOG_TAG , " discovery complete; reason=%d" , event->disc_complete .reason );
356+ NIMBLE_LOGD (LOG_TAG , " %s" , pScan->getStatsString ().c_str ());
357+
167358 pScan->m_pScanCallbacks ->onScanEnd (pScan->m_scanResults , event->disc_complete .reason );
168359
169360 if (pScan->m_pTaskData != nullptr ) {
@@ -178,6 +369,27 @@ int NimBLEScan::handleGapEvent(ble_gap_event* event, void* arg) {
178369 }
179370} // handleGapEvent
180371
372+ /* *
373+ * @brief Set the scan response timeout.
374+ * @param [in] timeoutMs The timeout in milliseconds to wait for a scan response, default: max advertising interval (10.24s)
375+ * @details If a scan response is not received within the timeout period,
376+ * the pending device will be reported to the scan result callback with whatever
377+ * data was present in the advertisement; no synthetic scan-response event is generated.
378+ * If set to 0, the scan result callback will only be triggered when a scan response
379+ * is received from the advertiser or when the scan completes, at which point any
380+ * pending scannable devices will be reported with the advertisement data only.
381+ */
382+ void NimBLEScan::setScanResponseTimeout (uint32_t timeoutMs) {
383+ if (timeoutMs == 0 ) {
384+ ble_npl_callout_stop (&m_srTimer);
385+ m_srTimeoutTicks = 0 ;
386+ return ;
387+ }
388+
389+ ble_npl_time_ms_to_ticks (timeoutMs, &m_srTimeoutTicks);
390+ resetWaitingTimer ();
391+ } // setScanResponseTimeout
392+
181393/* *
182394 * @brief Should we perform an active or passive scan?
183395 * The default is a passive scan. An active scan means that we will request a scan response.
@@ -208,7 +420,7 @@ void NimBLEScan::setDuplicateFilter(uint8_t enabled) {
208420 */
209421void NimBLEScan::setLimitedOnly (bool enabled) {
210422 m_scanParams.limited = enabled;
211- } // setLimited
423+ } // setLimitedOnly
212424
213425/* *
214426 * @brief Sets the scan filter policy.
@@ -323,11 +535,13 @@ bool NimBLEScan::start(uint32_t duration, bool isContinue, bool restart) {
323535
324536 if (!isContinue) {
325537 clearResults ();
538+ m_stats.reset ();
326539 }
327540 }
328541 } else { // Don't clear results while scanning is active
329542 if (!isContinue) {
330543 clearResults ();
544+ m_stats.reset ();
331545 }
332546 }
333547
@@ -394,6 +608,8 @@ bool NimBLEScan::stop() {
394608 return false ;
395609 }
396610
611+ clearWaitingList ();
612+
397613 if (m_maxResults == 0 ) {
398614 clearResults ();
399615 }
@@ -414,6 +630,7 @@ void NimBLEScan::erase(const NimBLEAddress& address) {
414630 NIMBLE_LOGD (LOG_TAG , " erase device: %s" , address.toString ().c_str ());
415631 for (auto it = m_scanResults.m_deviceVec .begin (); it != m_scanResults.m_deviceVec .end (); ++it) {
416632 if ((*it)->getAddress () == address) {
633+ removeWaitingDevice (*it);
417634 delete *it;
418635 m_scanResults.m_deviceVec .erase (it);
419636 break ;
@@ -429,6 +646,7 @@ void NimBLEScan::erase(const NimBLEAdvertisedDevice* device) {
429646 NIMBLE_LOGD (LOG_TAG , " erase device: %s" , device->getAddress ().toString ().c_str ());
430647 for (auto it = m_scanResults.m_deviceVec .begin (); it != m_scanResults.m_deviceVec .end (); ++it) {
431648 if ((*it) == device) {
649+ removeWaitingDevice (*it);
432650 delete *it;
433651 m_scanResults.m_deviceVec .erase (it);
434652 break ;
@@ -483,6 +701,12 @@ NimBLEScanResults NimBLEScan::getResults() {
483701 * @brief Clear the stored results of the scan.
484702 */
485703void NimBLEScan::clearResults () {
704+ if (isScanning ()) {
705+ NIMBLE_LOGW (LOG_TAG , " Cannot clear results while scan is active" );
706+ return ;
707+ }
708+
709+ clearWaitingList ();
486710 if (m_scanResults.m_deviceVec .size ()) {
487711 std::vector<NimBLEAdvertisedDevice*> vSwap{};
488712 ble_npl_hw_enter_critical ();
0 commit comments