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 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 */
485694void 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