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