h2zero / NimBLE-Arduino

A fork of the NimBLE library structured for compilation with Arduino, for use with ESP32, nRF5x.
https://h2zero.github.io/NimBLE-Arduino/
Apache License 2.0
700 stars 145 forks source link

ESP32-WROOM iBeacon scan interval unstable #590

Closed glanch closed 4 months ago

glanch commented 1 year ago

Hello, I have a BLE beacon that advertises itself with an interval of roughly 100ms. Unfortunately, I am not able to scan the beacon every 100ms. Mostly it takes over 100ms to trigger the callback albeit the beacon advertises fast enough. I checked it with my phone. Two consecutive advertisements trigger the callback within roughly 200ms, so in total, the timings are okay.

My code looks like the following. I adapted it from two samples in this repository.

#include <Arduino.h>

#include <NimBLEDevice.h>
#include <NimBLEAdvertisedDevice.h>
#include "NimBLEEddystoneURL.h"
#include "NimBLEEddystoneTLM.h"
#include "NimBLEBeacon.h"

#define ENDIAN_CHANGE_U16(x) ((((x)&0xFF00) >> 8) + (((x)&0xFF) << 8))

unsigned long beacon_last_seen = 0;

BLEScan *pBLEScan;

class MyAdvertisedDeviceCallbacks : public BLEAdvertisedDeviceCallbacks
{
    void onResult(BLEAdvertisedDevice *advertisedDevice)
    {
        if (!advertisedDevice->haveServiceUUID())
        {
            if (advertisedDevice->haveManufacturerData() == true)
            {
                std::string strManufacturerData = advertisedDevice->getManufacturerData();

                uint8_t cManufacturerData[100];
                strManufacturerData.copy((char *)cManufacturerData, strManufacturerData.length(), 0);

                if (strManufacturerData.length() == 25 && cManufacturerData[0] == 0x4C && cManufacturerData[1] == 0x00)
                {
                    BLEBeacon oBeacon = BLEBeacon();
                    oBeacon.setData(strManufacturerData);

                    auto major = ENDIAN_CHANGE_U16(oBeacon.getMajor());
                    if (major == 1234)
                    {
                        Serial.println("Found an iBeacon!");
                        Serial.print("Last seen: ");
                        Serial.println(millis() - beacon_last_seen);
                        beacon_last_seen = millis();
                    }
                }
            }
            return;
        }
    }
};

void setup()
{
    Serial.begin(115200);
    Serial.println("Scanning...");

    NimBLEDevice::setScanFilterMode(CONFIG_BTDM_SCAN_DUPL_TYPE_DEVICE);

    NimBLEDevice::init("");
    pBLEScan = NimBLEDevice::getScan(); //create new scan
    pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks(), true);
    pBLEScan->setDuplicateFilter(false);
    pBLEScan->setActiveScan(false); //active scan uses more power, but get results faster
    pBLEScan->setInterval(97);      // How often the scan occurs / switches channels; in milliseconds,
    pBLEScan->setWindow(37);        // How long to scan during the interval; in milliseconds.
    pBLEScan->setMaxResults(0);     // do not store the scan results, use callback only.
}

void loop()
{
    // put your main code here, to run repeatedly:
    if (!pBLEScan->isScanning())
    {
        pBLEScan->start(0, nullptr, false);
    }
    delay(2000);
}

This problem also occurs if I increase the advertisement interval of the beacon to 500ms. Sometimes it takes 800ms to trigger the callback and after that it takes roughly 200ms, so roughly 1000ms total. It seems that this problem is not due to lack of power of the ESP32. Does anyone have an idea to fix this? Since I am relying on a "time out" mechanism to check for presence of this beacon, an alias of advertisement interval is not acceptable.

Best, glanch

JustTryingToGetSomeWorkDone commented 11 months ago

There are only a couple of things that I can think of: 1) Serial port writing will slow down the BLE stack to the point of not even being able to connect to another device. Try to reduce or completely remove those calls and see if that helps. You might be able to see everything you need from the "core debug level" being set to debug or verbose, and those don't seem to block the BLE stack as far as I can tell. You can find that setting under tools of the Arduino IDE, or the menuconfig of ESP-IDF (if I remember correctly). 2) In your code you have active scanning set to false: "pBLEScan->setActiveScan(false); //active scan uses more power, but get results faster" Notice the comment "get results faster"... maybe you missed it, but I recommend setting it to true, regardless of whether or not you think the power is sufficient and see if that helps. My guess is that it will. You can also increase the transmission power of the ESP (I assume you're using an esp for this?) by using this code:

#ifdef ESP_PLATFORM
    NimBLEDevice::setPower(ESP_PWR_LVL_P9); /** +9db */
#else
    NimBLEDevice::setPower(9); /** +9db */
#endif

Again, if power use is not an issue, increasing the power will increase your results as far as I can tell. Fewer errors in data transmission means faster data processing.

I hope this helps.

h2zero commented 11 months ago

"pBLEScan->setActiveScan(false); //active scan uses more power, but get results faster" Notice the comment "get results faster"

This is misleading and unfortunately still in the example code from the original library. Active scanning actually slows down the scan results as it needs to send a scan response request and wait for the response before the callback is called.

What I think could be done is change the scan window and interval as follows:

    pBLEScan->setInterval(33);
    pBLEScan->setWindow(33); 

This will scan continuously and switch the scanned channel every 33ms.

glanch commented 10 months ago

Hi,

1. Serial port writing will slow down the BLE stack to the point of not even being able to connect to another device. Try to reduce or completely remove those calls and see if that helps. You might be able to see everything you need from the "core debug level" being set to debug or verbose, and those don't seem to block the BLE stack as far as I can tell. You can find that setting under tools of the Arduino IDE, or the menuconfig of ESP-IDF (if I remember correctly).

It seems that serial printing is not the issue here. I tried minimizing the serial output and also increasing it vastly. This made no difference at all. As far as I am concerned, the BLE tasks are scheduled on another core than the Arduino main loop, so this should not make any difference, even if serial printing is time consuming.

#ifdef ESP_PLATFORM
    NimBLEDevice::setPower(ESP_PWR_LVL_P9); /** +9db */
#else
    NimBLEDevice::setPower(9); /** +9db */
#endif

Again, if power use is not an issue, increasing the power will increase your results as far as I can tell. Fewer errors in data transmission means faster data processing.

Unfortunately, this didn't help at all.

But thanks for all of the suggestions.

glanch commented 10 months ago

"pBLEScan->setActiveScan(false); //active scan uses more power, but get results faster" Notice the comment "get results faster"

This is misleading and unfortunately still in the example code from the original library. Active scanning actually slows down the scan results as it needs to send a scan response request and wait for the response before the callback is called.

This is exactly what I observed, thanks for clarifying.

What I think could be done is change the scan window and interval as follows:

    pBLEScan->setInterval(33);
    pBLEScan->setWindow(33); 

This will scan continuously and switch the scanned channel every 33ms.

This didn't help either, the delay still exists.

It seems that the delay between scans increases proportionally with the advertisement interval of the beacon. Can you think of any operation in the library that would induce this behavior?

h2zero commented 10 months ago

How long is the advertisement window? If it only advertising for a few Ms every 100ms then that could explain it.

glanch commented 10 months ago

What exactly do you mean by advertisement window? I guess this is a property of my advertising device. I haven't set explicitly, so I cannot say.

h2zero commented 10 months ago

I mean if it's advertising every 100ms how long does it advertise for? The whole time?

glanch commented 10 months ago

Can't really tell since one beacon is OEM and the other one is ESPHome and I wasn't able to find out. I tried to use nRF Connect App on Android to find out, but as far as I am concerned this is not measured by the application.

h2zero commented 10 months ago

Well it's quite normal to miss an advertisement, there is a lot of interference in the 2.4ghz spectrum. The best you can do is set the scan interval and window to the same value and that value should be something that allows the channel to switch often enough but not too often to capture the advertisement. You'll need to determine that value for yourself through trial and error.

How to think about this is the advertiser is broadcasting for X amount of time every Y milliseconds. The channel changes between 3 channels on each broadcast start and there is a +-10ms randomized timing of the broadcast start time. How can you configure the scanner to maximize the broadcasts received?