Skip to content

Commit b4dc2d4

Browse files
committed
Add a timer for scan responses
This ensures that the callback will be called within the configured time (in ms) when devices fail to respond to a scan response request within that time. * Also adds stats when debug logging to help tune the scan response timeout/scan parameters.
1 parent b5110a2 commit b4dc2d4

3 files changed

Lines changed: 224 additions & 10 deletions

File tree

src/NimBLEAdvertisedDevice.h

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -158,11 +158,12 @@ class NimBLEAdvertisedDevice {
158158
uint8_t findAdvField(uint8_t type, uint8_t index = 0, size_t* data_loc = nullptr) const;
159159
size_t findServiceData(uint8_t index, uint8_t* bytes) const;
160160

161-
NimBLEAddress m_address{};
162-
uint8_t m_advType{};
163-
int8_t m_rssi{};
164-
uint8_t m_callbackSent{};
165-
uint16_t m_advLength{};
161+
NimBLEAddress m_address{};
162+
uint8_t m_advType{};
163+
int8_t m_rssi{};
164+
uint8_t m_callbackSent{};
165+
uint16_t m_advLength{};
166+
ble_npl_time_t m_time{};
166167

167168
# if MYNEWT_VAL(BLE_EXT_ADV)
168169
bool m_isLegacyAdv{};

src/NimBLEScan.cpp

Lines changed: 126 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,75 @@
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

2732
static const char* LOG_TAG = "NimBLEScan";
2833
static NimBLEScanCallbacks defaultScanCallbacks;
2934

35+
/**
36+
* @brief Calls the onResult callback with the given device,
37+
* this is used to provide a scan result to the callbacks when a device hasn't responded to
38+
* the scan request in time. This is called by the host task from the default event queue.
39+
*/
40+
void NimBLEScan::forceResultCallback(ble_npl_event* ev) {
41+
auto pScan = NimBLEDevice::getScan();
42+
pScan->m_stats.incMissedSrCount();
43+
NimBLEAdvertisedDevice* pDev = static_cast<NimBLEAdvertisedDevice*>(ble_npl_event_get_arg(ev));
44+
pDev->m_callbackSent = 2;
45+
pScan->m_pScanCallbacks->onResult(pDev);
46+
}
47+
48+
/**
49+
* @brief This will schedule an event to run in the host task that will call forceResultCallback
50+
* which will call the onResult callback with the current data.
51+
*/
52+
void NimBLEScan::srTimerCb(ble_npl_event* event) {
53+
NimBLEScan* pScan = static_cast<NimBLEScan*>(ble_npl_event_get_arg(event));
54+
NimBLEAdvertisedDevice* curDev = nullptr;
55+
NimBLEAdvertisedDevice* nextDev = nullptr;
56+
ble_npl_time_t now = ble_npl_time_get();
57+
58+
for (auto& dev : pScan->m_scanResults.m_deviceVec) {
59+
if (dev->m_callbackSent < 2 && dev->isScannable()) {
60+
if (!curDev || (now - dev->m_time > now - curDev->m_time)) {
61+
nextDev = curDev;
62+
curDev = dev;
63+
continue;
64+
}
65+
66+
if (!nextDev || now - dev->m_time > now - nextDev->m_time) {
67+
nextDev = dev;
68+
}
69+
}
70+
}
71+
72+
// Add the event to the host queue
73+
if (curDev && now - curDev->m_time >= pScan->m_srTimeoutTicks) {
74+
NIMBLE_LOGI(LOG_TAG, "Scan response timeout for: %s", curDev->getAddress().toString().c_str());
75+
ble_npl_event_set_arg(&pScan->m_srTimeoutEvent, curDev);
76+
ble_npl_eventq_put(nimble_port_get_dflt_eventq(), &pScan->m_srTimeoutEvent);
77+
}
78+
79+
// Restart the timer for the next device that we are expecting a scan response from
80+
if (nextDev) {
81+
auto nextTime = now - nextDev->m_time;
82+
if (nextTime >= pScan->m_srTimeoutTicks) {
83+
nextTime = 1;
84+
} else {
85+
nextTime = pScan->m_srTimeoutTicks - nextTime;
86+
}
87+
88+
ble_npl_callout_reset(&pScan->m_srTimer, nextTime);
89+
}
90+
}
91+
3092
/**
3193
* @brief Scan constructor.
3294
*/
@@ -35,7 +97,11 @@ NimBLEScan::NimBLEScan()
3597
// default interval + window, no whitelist scan filter,not limited scan, no scan response, filter_duplicates
3698
m_scanParams{0, 0, BLE_HCI_SCAN_FILT_NO_WL, 0, 1, 1},
3799
m_pTaskData{nullptr},
38-
m_maxResults{0xFF} {}
100+
m_maxResults{0xFF} {
101+
ble_npl_callout_init(&m_srTimer, nimble_port_get_dflt_eventq(), NimBLEScan::srTimerCb, this);
102+
ble_npl_event_init(&m_srTimeoutEvent, forceResultCallback, NULL);
103+
ble_npl_time_ms_to_ticks(BLE_GAP_SCAN_FAST_WINDOW, &m_srTimeoutTicks);
104+
} // NimBLEScan::NimBLEScan
39105

40106
/**
41107
* @brief Scan destructor, release any allocated resources.
@@ -44,6 +110,9 @@ NimBLEScan::~NimBLEScan() {
44110
for (const auto& dev : m_scanResults.m_deviceVec) {
45111
delete dev;
46112
}
113+
114+
ble_npl_callout_deinit(&m_srTimer);
115+
ble_npl_event_deinit(&m_srTimeoutEvent);
47116
}
48117

49118
/**
@@ -101,6 +170,8 @@ int NimBLEScan::handleGapEvent(ble_gap_event* event, void* arg) {
101170
// If we haven't seen this device before; create a new instance and insert it in the vector.
102171
// Otherwise just update the relevant parameters of the already known device.
103172
if (advertisedDevice == nullptr) {
173+
pScan->m_stats.incDevCount();
174+
104175
// Check if we have reach the scan results limit, ignore this one if so.
105176
// We still need to store each device when maxResults is 0 to be able to append the scan results
106177
if (pScan->m_maxResults > 0 && pScan->m_maxResults < 0xFF &&
@@ -109,19 +180,36 @@ int NimBLEScan::handleGapEvent(ble_gap_event* event, void* arg) {
109180
}
110181

111182
if (isLegacyAdv && event_type == BLE_HCI_ADV_RPT_EVTYPE_SCAN_RSP) {
183+
pScan->m_stats.incOrphanedSrCount();
112184
NIMBLE_LOGI(LOG_TAG, "Scan response without advertisement: %s", advertisedAddress.toString().c_str());
113185
}
114186

115187
advertisedDevice = new NimBLEAdvertisedDevice(event, event_type);
116188
pScan->m_scanResults.m_deviceVec.push_back(advertisedDevice);
189+
advertisedDevice->m_time = ble_npl_time_get();
117190
NIMBLE_LOGI(LOG_TAG, "New advertiser: %s", advertisedAddress.toString().c_str());
118191
} else {
119192
advertisedDevice->update(event, event_type);
120193
if (isLegacyAdv) {
121194
if (event_type == BLE_HCI_ADV_RPT_EVTYPE_SCAN_RSP) {
122195
NIMBLE_LOGI(LOG_TAG, "Scan response from: %s", advertisedAddress.toString().c_str());
196+
if (!pScan->m_stats.recordSrTime(ble_npl_time_get() - advertisedDevice->m_time,
197+
pScan->m_scanParams.window * 625 / 1000)) {
198+
NIMBLE_LOGD(LOG_TAG,
199+
"Abnormal scan response time, stats ignored for device: %s",
200+
advertisedAddress.toString().c_str());
201+
}
123202
} else {
203+
pScan->m_stats.incDupCount();
124204
NIMBLE_LOGI(LOG_TAG, "Duplicate; updated: %s", advertisedAddress.toString().c_str());
205+
// Restart scan-response timeout when we see a new non-scan-response
206+
// legacy advertisement during active scanning for a scannable device.
207+
advertisedDevice->m_time = ble_npl_time_get();
208+
advertisedDevice->m_callbackSent = 0;
209+
if (pScan->m_srTimeoutTicks && advertisedDevice->isScannable() &&
210+
!ble_npl_callout_is_active(&pScan->m_srTimer)) {
211+
ble_npl_callout_reset(&pScan->m_srTimer, pScan->m_srTimeoutTicks);
212+
}
125213
}
126214
}
127215
}
@@ -147,6 +235,10 @@ int NimBLEScan::handleGapEvent(ble_gap_event* event, void* arg) {
147235
advertisedDevice->m_callbackSent++;
148236
// got the scan response report the full data.
149237
pScan->m_pScanCallbacks->onResult(advertisedDevice);
238+
} else if (pScan->m_srTimeoutTicks && isLegacyAdv && advertisedDevice->isScannable() &&
239+
!ble_npl_callout_is_active(&pScan->m_srTimer)) {
240+
// Start the timer to wait for the scan response.
241+
ble_npl_callout_reset(&pScan->m_srTimer, pScan->m_srTimeoutTicks);
150242
}
151243

152244
// If not storing results and we have invoked the callback, delete the device.
@@ -158,14 +250,22 @@ int NimBLEScan::handleGapEvent(ble_gap_event* event, void* arg) {
158250
}
159251

160252
case BLE_GAP_EVENT_DISC_COMPLETE: {
253+
ble_npl_callout_stop(&pScan->m_srTimer);
254+
for (const auto& dev : pScan->m_scanResults.m_deviceVec) {
255+
if (dev->isScannable() && dev->m_callbackSent < 2) {
256+
pScan->m_stats.incMissedSrCount();
257+
pScan->m_pScanCallbacks->onResult(dev);
258+
}
259+
}
260+
161261
NIMBLE_LOGD(LOG_TAG, "discovery complete; reason=%d", event->disc_complete.reason);
262+
NIMBLE_LOGD(LOG_TAG, "%s", pScan->getStatsString().c_str());
162263

264+
pScan->m_pScanCallbacks->onScanEnd(pScan->m_scanResults, event->disc_complete.reason);
163265
if (pScan->m_maxResults == 0) {
164266
pScan->clearResults();
165267
}
166268

167-
pScan->m_pScanCallbacks->onScanEnd(pScan->m_scanResults, event->disc_complete.reason);
168-
169269
if (pScan->m_pTaskData != nullptr) {
170270
NimBLEUtils::taskRelease(*pScan->m_pTaskData, event->disc_complete.reason);
171271
}
@@ -178,6 +278,25 @@ int NimBLEScan::handleGapEvent(ble_gap_event* event, void* arg) {
178278
}
179279
} // handleGapEvent
180280

281+
/**
282+
* @brief Set the scan response timeout.
283+
* @param [in] timeoutMs The timeout in milliseconds to wait for a scan response.
284+
* @details If a scan response is not received within the timeout period,
285+
* a dummy scan response with null data will be sent to the scan event handler
286+
* which will trigger the callback with whatever data was in the advertisement.
287+
* If set to 0, no dummy scan response will be sent and the callback will only
288+
* be triggered when a scan response is received from the advertiser or when the scan completes.
289+
*/
290+
void NimBLEScan::setScanResponseTimeout(uint32_t timeoutMs) {
291+
if (timeoutMs == 0) {
292+
ble_npl_callout_stop(&m_srTimer);
293+
m_srTimeoutTicks = 0;
294+
return;
295+
}
296+
297+
ble_npl_time_ms_to_ticks(timeoutMs, &m_srTimeoutTicks);
298+
} // setScanResponseTimeout
299+
181300
/**
182301
* @brief Should we perform an active or passive scan?
183302
* The default is a passive scan. An active scan means that we will request a scan response.
@@ -323,11 +442,13 @@ bool NimBLEScan::start(uint32_t duration, bool isContinue, bool restart) {
323442

324443
if (!isContinue) {
325444
clearResults();
445+
m_stats.reset();
326446
}
327447
}
328448
} else { // Don't clear results while scanning is active
329449
if (!isContinue) {
330450
clearResults();
451+
m_stats.reset();
331452
}
332453
}
333454

@@ -394,6 +515,8 @@ bool NimBLEScan::stop() {
394515
return false;
395516
}
396517

518+
ble_npl_callout_stop(&m_srTimer);
519+
397520
if (m_maxResults == 0) {
398521
clearResults();
399522
}

src/NimBLEScan.h

Lines changed: 92 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
# endif
3232

3333
# include <vector>
34+
# include <cinttypes>
35+
# include <cstdio>
3436

3537
class NimBLEDevice;
3638
class NimBLEScan;
@@ -82,6 +84,8 @@ class NimBLEScan {
8284
void setMaxResults(uint8_t maxResults);
8385
void erase(const NimBLEAddress& address);
8486
void erase(const NimBLEAdvertisedDevice* device);
87+
void setScanResponseTimeout(uint32_t timeoutMs);
88+
std::string getStatsString() const { return m_stats.toString(); }
8589

8690
# if MYNEWT_VAL(BLE_EXT_ADV)
8791
enum Phy { SCAN_1M = 0x01, SCAN_CODED = 0x02, SCAN_ALL = 0x03 };
@@ -92,15 +96,101 @@ class NimBLEScan {
9296
private:
9397
friend class NimBLEDevice;
9498

99+
struct stats {
100+
# if MYNEWT_VAL(NIMBLE_CPP_LOG_LEVEL) >= 4
101+
uint32_t devCount = 0; // unique devices seen for the first time
102+
uint32_t dupCount = 0; // repeat advertisements from already-known devices
103+
uint32_t srMinMs = UINT32_MAX;
104+
uint32_t srMaxMs = 0;
105+
uint64_t srTotalMs = 0; // uint64 to avoid overflow on long/busy scans
106+
uint32_t srCount = 0; // matched scan responses (advertisement + SR pair)
107+
uint32_t orphanedSrCount = 0; // scan responses received with no prior advertisement
108+
uint32_t missedSrCount = 0; // scannable devices for which no SR ever arrived
109+
110+
void reset() {
111+
devCount = 0;
112+
dupCount = 0;
113+
srMinMs = UINT32_MAX;
114+
srMaxMs = 0;
115+
srTotalMs = 0;
116+
srCount = 0;
117+
orphanedSrCount = 0;
118+
missedSrCount = 0;
119+
}
120+
121+
void incDevCount() { devCount++; }
122+
void incDupCount() { dupCount++; }
123+
void incMissedSrCount() { missedSrCount++; }
124+
void incOrphanedSrCount() { orphanedSrCount++; }
125+
126+
std::string toString() const {
127+
std::string out;
128+
out.resize(400); // should be more than enough for the stats string
129+
snprintf(out.data(),
130+
out.size(),
131+
"Scan stats:\n"
132+
" Devices seen : %" PRIu32 "\n"
133+
" Duplicate advs : %" PRIu32 "\n"
134+
" Scan responses : %" PRIu32 "\n"
135+
" SR timing (ms) : min=%" PRIu32 ", max=%" PRIu32 ", avg=%" PRIu64 "\n"
136+
" Orphaned SR : %" PRIu32 "\n"
137+
" Missed SR : %" PRIu32 "\n",
138+
devCount,
139+
dupCount,
140+
srCount,
141+
srMinMs,
142+
srMaxMs,
143+
srCount ? srTotalMs / srCount : 0,
144+
orphanedSrCount,
145+
missedSrCount);
146+
out.resize(strlen(out.c_str()));
147+
return out;
148+
}
149+
150+
// Records scan-response round-trip time. Returns false if the timing is abnormal
151+
// (>= 5x the scan window) and should be excluded from stats.
152+
bool recordSrTime(uint32_t ticks, uint16_t window) {
153+
uint32_t ms;
154+
ble_npl_time_ticks_to_ms(ticks, &ms);
155+
if (ms >= (window ? window : BLE_GAP_SCAN_FAST_WINDOW) * 5) {
156+
//return false;
157+
}
158+
159+
if (ms < srMinMs) {
160+
srMinMs = ms;
161+
}
162+
if (ms > srMaxMs) {
163+
srMaxMs = ms;
164+
}
165+
srTotalMs += ms;
166+
srCount++;
167+
return true;
168+
}
169+
# else
170+
void reset() {}
171+
void incDevCount() {}
172+
void incDupCount() {}
173+
void incMissedSrCount() {}
174+
void incOrphanedSrCount() {}
175+
std::string toString() const { return ""; }
176+
bool recordSrTime(uint32_t ticks, uint16_t window) { return true; }
177+
# endif
178+
} m_stats;
179+
95180
NimBLEScan();
96181
~NimBLEScan();
97-
static int handleGapEvent(ble_gap_event* event, void* arg);
98-
void onHostSync();
182+
static int handleGapEvent(ble_gap_event* event, void* arg);
183+
void onHostSync();
184+
static void srTimerCb(ble_npl_event* event);
185+
static void forceResultCallback(ble_npl_event* ev);
99186

100187
NimBLEScanCallbacks* m_pScanCallbacks;
101188
ble_gap_disc_params m_scanParams;
102189
NimBLEScanResults m_scanResults;
103190
NimBLETaskData* m_pTaskData;
191+
ble_npl_callout m_srTimer{};
192+
ble_npl_time_t m_srTimeoutTicks{};
193+
ble_npl_event m_srTimeoutEvent{};
104194
uint8_t m_maxResults;
105195

106196
# if MYNEWT_VAL(BLE_EXT_ADV)

0 commit comments

Comments
 (0)