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+ auto * pScan = NimBLEDevice::getScan ();
59+ auto curDev = pScan->m_pWaitingListHead ;
60+
61+ if (curDev == nullptr ) {
62+ ble_npl_callout_stop (&pScan->m_srTimer );
63+ return ;
64+ }
65+
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 the waiting list and reset the timer for the next one (if any)
73+ pScan->removeWaitingDevice (curDev);
74+ pScan->resetWaitingTimer ();
75+ }
76+
3077/* *
3178 * @brief Scan constructor.
3279 */
@@ -35,7 +82,11 @@ NimBLEScan::NimBLEScan()
3582 // default interval + window, no whitelist scan filter,not limited scan, no scan response, filter_duplicates
3683 m_scanParams{0 , 0 , BLE_HCI_SCAN_FILT_NO_WL , 0 , 1 , 1 },
3784 m_pTaskData{nullptr },
38- m_maxResults{0xFF } {}
85+ m_maxResults{0xFF } {
86+ ble_npl_callout_init (&m_srTimer, nimble_port_get_dflt_eventq (), NimBLEScan::srTimerCb, nullptr );
87+ ble_npl_event_init (&m_srTimeoutEvent, forceResultCallback, NULL );
88+ ble_npl_time_ms_to_ticks (DEFAULT_SCAN_RESP_TIMEOUT_MS , &m_srTimeoutTicks);
89+ } // NimBLEScan::NimBLEScan
3990
4091/* *
4192 * @brief Scan destructor, release any allocated resources.
@@ -44,6 +95,112 @@ NimBLEScan::~NimBLEScan() {
4495 for (const auto & dev : m_scanResults.m_deviceVec ) {
4596 delete dev;
4697 }
98+
99+ ble_npl_callout_deinit (&m_srTimer);
100+ ble_npl_eventq_remove (nimble_port_get_dflt_eventq (), &m_srTimeoutEvent);
101+ ble_npl_event_deinit (&m_srTimeoutEvent);
102+ }
103+
104+ /* *
105+ * @brief Add a device to the waiting list for scan responses.
106+ * @param [in] pDev The device to add to the list.
107+ */
108+ void NimBLEScan::addWaitingDevice (NimBLEAdvertisedDevice* pDev) {
109+ if (pDev == nullptr || pDev->m_pNextWaiting != pDev) {
110+ return ; // Invalid or already in list (self-pointer is the "not in list" sentinel)
111+ }
112+
113+ pDev->m_pNextWaiting = nullptr ;
114+ if (m_pWaitingListTail == nullptr ) {
115+ ble_npl_hw_enter_critical ();
116+ m_pWaitingListHead = pDev;
117+ m_pWaitingListTail = pDev;
118+ ble_npl_hw_exit_critical (0 );
119+ return ;
120+ }
121+
122+ ble_npl_hw_enter_critical ();
123+ m_pWaitingListTail->m_pNextWaiting = pDev;
124+ m_pWaitingListTail = pDev;
125+ ble_npl_hw_exit_critical (0 );
126+ }
127+
128+ /* *
129+ * @brief Remove a device from the waiting list.
130+ * @param [in] pDev The device to remove from the list.
131+ */
132+ void NimBLEScan::removeWaitingDevice (NimBLEAdvertisedDevice* pDev) {
133+ if (pDev == nullptr ) {
134+ return ;
135+ }
136+
137+ if (pDev->m_pNextWaiting == pDev) {
138+ return ; // Not in the list
139+ }
140+
141+ bool resetTimer = false ;
142+ ble_npl_hw_enter_critical ();
143+ if (m_pWaitingListHead == pDev) {
144+ m_pWaitingListHead = pDev->m_pNextWaiting ;
145+ if (m_pWaitingListHead == nullptr ) {
146+ m_pWaitingListTail = nullptr ;
147+ } else {
148+ resetTimer = true ;
149+ }
150+ } else {
151+ NimBLEAdvertisedDevice* current = m_pWaitingListHead;
152+ while (current != nullptr ) {
153+ if (current->m_pNextWaiting == pDev) {
154+ current->m_pNextWaiting = pDev->m_pNextWaiting ;
155+ if (m_pWaitingListTail == pDev) {
156+ m_pWaitingListTail = current;
157+ }
158+ break ;
159+ }
160+ current = current->m_pNextWaiting ;
161+ }
162+ }
163+ ble_npl_hw_exit_critical (0 );
164+ pDev->m_pNextWaiting = pDev; // Restore sentinel: self-pointer means "not in list"
165+ if (resetTimer) {
166+ resetWaitingTimer ();
167+ }
168+ }
169+
170+ /* *
171+ * @brief Clear all devices from the waiting list.
172+ */
173+ void NimBLEScan::clearWaitingList () {
174+ // Stop the timer and remove any pending timeout events since we're clearing
175+ // the list and won't be processing any more timeouts for these devices
176+ ble_npl_callout_stop (&m_srTimer);
177+ ble_npl_eventq_remove (nimble_port_get_dflt_eventq (), &m_srTimeoutEvent);
178+
179+ ble_npl_hw_enter_critical ();
180+ NimBLEAdvertisedDevice* current = m_pWaitingListHead;
181+ while (current != nullptr ) {
182+ NimBLEAdvertisedDevice* next = current->m_pNextWaiting ;
183+ current->m_pNextWaiting = current; // Restore sentinel
184+ current = next;
185+ }
186+ m_pWaitingListHead = nullptr ;
187+ m_pWaitingListTail = nullptr ;
188+ ble_npl_hw_exit_critical (0 );
189+ }
190+
191+ /* *
192+ * @brief Reset the timer for the next waiting device at the head of the FIFO list.
193+ */
194+ void NimBLEScan::resetWaitingTimer () {
195+ if (m_srTimeoutTicks == 0 || m_pWaitingListHead == nullptr ) {
196+ ble_npl_callout_stop (&m_srTimer);
197+ return ;
198+ }
199+
200+ ble_npl_time_t now = ble_npl_time_get ();
201+ ble_npl_time_t elapsed = now - m_pWaitingListHead->m_time ;
202+ ble_npl_time_t nextTime = elapsed >= m_srTimeoutTicks ? 1 : m_srTimeoutTicks - elapsed;
203+ ble_npl_callout_reset (&m_srTimer, nextTime);
47204}
48205
49206/* *
@@ -101,6 +258,8 @@ int NimBLEScan::handleGapEvent(ble_gap_event* event, void* arg) {
101258 // If we haven't seen this device before; create a new instance and insert it in the vector.
102259 // Otherwise just update the relevant parameters of the already known device.
103260 if (advertisedDevice == nullptr ) {
261+ pScan->m_stats .incDevCount ();
262+
104263 // Check if we have reach the scan results limit, ignore this one if so.
105264 // We still need to store each device when maxResults is 0 to be able to append the scan results
106265 if (pScan->m_maxResults > 0 && pScan->m_maxResults < 0xFF &&
@@ -109,19 +268,34 @@ int NimBLEScan::handleGapEvent(ble_gap_event* event, void* arg) {
109268 }
110269
111270 if (isLegacyAdv && event_type == BLE_HCI_ADV_RPT_EVTYPE_SCAN_RSP ) {
271+ pScan->m_stats .incOrphanedSrCount ();
112272 NIMBLE_LOGI (LOG_TAG , " Scan response without advertisement: %s" , advertisedAddress.toString ().c_str ());
113273 }
114274
115275 advertisedDevice = new NimBLEAdvertisedDevice (event, event_type);
116276 pScan->m_scanResults .m_deviceVec .push_back (advertisedDevice);
277+ advertisedDevice->m_time = ble_npl_time_get ();
117278 NIMBLE_LOGI (LOG_TAG , " New advertiser: %s" , advertisedAddress.toString ().c_str ());
118279 } else {
119280 advertisedDevice->update (event, event_type);
120281 if (isLegacyAdv) {
121282 if (event_type == BLE_HCI_ADV_RPT_EVTYPE_SCAN_RSP ) {
283+ pScan->m_stats .recordSrTime (ble_npl_time_get () - advertisedDevice->m_time );
122284 NIMBLE_LOGI (LOG_TAG , " Scan response from: %s" , advertisedAddress.toString ().c_str ());
285+ // Remove device from waiting list since we got the response
286+ pScan->removeWaitingDevice (advertisedDevice);
123287 } else {
288+ pScan->m_stats .incDupCount ();
124289 NIMBLE_LOGI (LOG_TAG , " Duplicate; updated: %s" , advertisedAddress.toString ().c_str ());
290+ // Restart scan-response timeout when we see a new non-scan-response
291+ // legacy advertisement during active scanning for a scannable device.
292+ advertisedDevice->m_time = ble_npl_time_get ();
293+ advertisedDevice->m_callbackSent = 0 ;
294+ // Re-add to the tail so FIFO timeout order matches advertisement order.
295+ if (pScan->m_srTimeoutTicks && advertisedDevice->isScannable ()) {
296+ pScan->removeWaitingDevice (advertisedDevice);
297+ pScan->addWaitingDevice (advertisedDevice);
298+ }
125299 }
126300 }
127301 }
@@ -147,6 +321,12 @@ int NimBLEScan::handleGapEvent(ble_gap_event* event, void* arg) {
147321 advertisedDevice->m_callbackSent ++;
148322 // got the scan response report the full data.
149323 pScan->m_pScanCallbacks ->onResult (advertisedDevice);
324+ } else if (pScan->m_srTimeoutTicks && isLegacyAdv && advertisedDevice->isScannable ()) {
325+ // Add to waiting list for scan response and start the timer
326+ pScan->addWaitingDevice (advertisedDevice);
327+ if (pScan->m_pWaitingListHead == advertisedDevice) {
328+ pScan->resetWaitingTimer ();
329+ }
150330 }
151331
152332 // If not storing results and we have invoked the callback, delete the device.
@@ -158,14 +338,34 @@ int NimBLEScan::handleGapEvent(ble_gap_event* event, void* arg) {
158338 }
159339
160340 case BLE_GAP_EVENT_DISC_COMPLETE : {
341+ pScan->clearWaitingList ();
342+ // If we have any scannable devices that haven't received a scan response,
343+ // we should trigger the callback with whatever data we have since the scan is complete
344+ // and we won't be getting any more updates for these devices.
345+
346+ // Make a copy in case the callback modifies the vector (e.g. by calling clearResults)
347+ std::vector<NimBLEAdvertisedDevice*> pending{};
348+ pending.reserve (pScan->m_scanResults .m_deviceVec .size ());
349+ for (const auto & dev : pScan->m_scanResults .m_deviceVec ) {
350+ if (dev->isScannable () && dev->m_callbackSent < 2 ) {
351+ pScan->m_stats .incMissedSrCount ();
352+ dev->m_callbackSent = 2 ;
353+ pending.push_back (dev);
354+ }
355+ }
356+
357+ for (const auto & dev : pending) {
358+ pScan->m_pScanCallbacks ->onResult (dev);
359+ }
360+
161361 NIMBLE_LOGD (LOG_TAG , " discovery complete; reason=%d" , event->disc_complete .reason );
362+ NIMBLE_LOGD (LOG_TAG , " %s" , pScan->getStatsString ().c_str ());
162363
364+ pScan->m_pScanCallbacks ->onScanEnd (pScan->m_scanResults , event->disc_complete .reason );
163365 if (pScan->m_maxResults == 0 ) {
164366 pScan->clearResults ();
165367 }
166368
167- pScan->m_pScanCallbacks ->onScanEnd (pScan->m_scanResults , event->disc_complete .reason );
168-
169369 if (pScan->m_pTaskData != nullptr ) {
170370 NimBLEUtils::taskRelease (*pScan->m_pTaskData , event->disc_complete .reason );
171371 }
@@ -178,6 +378,25 @@ int NimBLEScan::handleGapEvent(ble_gap_event* event, void* arg) {
178378 }
179379} // handleGapEvent
180380
381+ /* *
382+ * @brief Set the scan response timeout.
383+ * @param [in] timeoutMs The timeout in milliseconds to wait for a scan response, default: max advertising interval (10.24s)
384+ * @details If a scan response is not received within the timeout period,
385+ * a dummy scan response with null data will be sent to the scan event handler
386+ * which will trigger the callback with whatever data was in the advertisement.
387+ * If set to 0, no dummy scan response will be sent and the callback will only
388+ * be triggered when a scan response is received from the advertiser or when the scan completes.
389+ */
390+ void NimBLEScan::setScanResponseTimeout (uint32_t timeoutMs) {
391+ if (timeoutMs == 0 ) {
392+ ble_npl_callout_stop (&m_srTimer);
393+ m_srTimeoutTicks = 0 ;
394+ return ;
395+ }
396+
397+ ble_npl_time_ms_to_ticks (timeoutMs, &m_srTimeoutTicks);
398+ } // setScanResponseTimeout
399+
181400/* *
182401 * @brief Should we perform an active or passive scan?
183402 * The default is a passive scan. An active scan means that we will request a scan response.
@@ -323,11 +542,13 @@ bool NimBLEScan::start(uint32_t duration, bool isContinue, bool restart) {
323542
324543 if (!isContinue) {
325544 clearResults ();
545+ m_stats.reset ();
326546 }
327547 }
328548 } else { // Don't clear results while scanning is active
329549 if (!isContinue) {
330550 clearResults ();
551+ m_stats.reset ();
331552 }
332553 }
333554
@@ -394,6 +615,8 @@ bool NimBLEScan::stop() {
394615 return false ;
395616 }
396617
618+ clearWaitingList ();
619+
397620 if (m_maxResults == 0 ) {
398621 clearResults ();
399622 }
@@ -414,6 +637,7 @@ void NimBLEScan::erase(const NimBLEAddress& address) {
414637 NIMBLE_LOGD (LOG_TAG , " erase device: %s" , address.toString ().c_str ());
415638 for (auto it = m_scanResults.m_deviceVec .begin (); it != m_scanResults.m_deviceVec .end (); ++it) {
416639 if ((*it)->getAddress () == address) {
640+ removeWaitingDevice (*it);
417641 delete *it;
418642 m_scanResults.m_deviceVec .erase (it);
419643 break ;
@@ -429,6 +653,7 @@ void NimBLEScan::erase(const NimBLEAdvertisedDevice* device) {
429653 NIMBLE_LOGD (LOG_TAG , " erase device: %s" , device->getAddress ().toString ().c_str ());
430654 for (auto it = m_scanResults.m_deviceVec .begin (); it != m_scanResults.m_deviceVec .end (); ++it) {
431655 if ((*it) == device) {
656+ removeWaitingDevice (*it);
432657 delete *it;
433658 m_scanResults.m_deviceVec .erase (it);
434659 break ;
@@ -483,6 +708,7 @@ NimBLEScanResults NimBLEScan::getResults() {
483708 * @brief Clear the stored results of the scan.
484709 */
485710void NimBLEScan::clearResults () {
711+ clearWaitingList ();
486712 if (m_scanResults.m_deviceVec .size ()) {
487713 std::vector<NimBLEAdvertisedDevice*> vSwap{};
488714 ble_npl_hw_enter_critical ();
0 commit comments