BLE central mode scan of advertising BLE devices

I am trying to see if I can determine the number of BLE advertising devices (tablets) that are inside a cabinet. I am using an Argon setup in central mode to scan for advertising devices and count the number that are in proximity (with an RSSI > a certain value). I can determine the manufacturer ID and the device Bluetooth MAC. Is there anything else I can determine? Device name always seems to be empty, although BLE scan tools seem to be able to pick up some names e.g. Tile, Samsung and the advertising delay period.

The code being used is a simple modification of the iBeacon central example.

#include "Particle.h"

// This example does not require the cloud so you can run it in manual mode or
// normal cloud-connected mode
SYSTEM_MODE(MANUAL);

const size_t SCAN_RESULT_MAX = 30;

BleScanResult scanResults[SCAN_RESULT_MAX];

void setup()
{
    selectExternalMeshAntenna();
    Serial.begin();
    while (!Serial.available()) delay(10);
}

void loop()
{
    // Only scan for 500 milliseconds
    BLE.setScanTimeout(50);
    int count = BLE.scan(scanResults, SCAN_RESULT_MAX);

    for (int ii = 0; ii < count; ii++)
    {
        uint8_t buf[BLE_MAX_ADV_DATA_LEN];
        size_t len;

        len = scanResults[ii].advertisingData.get(BleAdvertisingDataType::MANUFACTURER_SPECIFIC_DATA, buf, BLE_MAX_ADV_DATA_LEN);
        // We have manufacturer-specific advertising data (0xff) and it's 7 bytes (without the AD type)

        // Byte: BLE_SIG_AD_TYPE_MANUFACTURER_SPECIFIC_DATA 
        // 16-bit: Company ID
        // Byte: Internal packet identifier

        Serial.printf("index: %2i RSSI: %d len: %i MANUF: %02X:%02X  %02X:%02X  MAC: %02X:%02X:%02X:%02X:%02X:%02X ",
        ii, scanResults[ii].rssi, len, buf[1], buf[0], buf[2], buf[3],
        scanResults[ii].address[0], scanResults[ii].address[1], scanResults[ii].address[2],
        scanResults[ii].address[3], scanResults[ii].address[4], scanResults[ii].address[5]);
        String name = scanResults[ii].advertisingData.deviceName();
        if (name.length() > 0) {Serial.printlnf("Name: %s", name.c_str());}
        else {Serial.println();}
    }
    delay(4000);
}
//applies to BT as well as mesh
void selectExternalMeshAntenna()
{
#if   (PLATFORM_ID == PLATFORM_ARGON)
	digitalWrite(ANTSW1, 1);
	digitalWrite(ANTSW2, 0);
#elif (PLATFORM_ID == PLATFORM_BORON)
	digitalWrite(ANTSW1, 0);
#elif (PLATFORM_ID == PLATFORM_XENON)
	digitalWrite(ANTSW1, 0);
	digitalWrite(ANTSW2, 1);
#endif
}
1 Like

You could request another BleAdvertisingDataType in addition to MANUFACTURER_SPECIFIC_DATA e.g. SHORT_LOCAL_NAME or COMPLETE_LOCAL_NAME

But sometimes the device might not expose a name but the BLE app you are using can deduce the brand from the manufacturer ID and/or MAC - that's something that's not built into the Particle BLE stack as it would either consume flash space (if static) or require an online connection to look-up the relation.

I am OK with this bit - there is a list of manufacturer ID codes and names on the bluetooth.com website - I can hold a small subset as required.

I guess my more specific questions are how would I get the whole 31 bytes which I can then dump out and analyse what specific data types it contains and how to perform the additional request to the peripheral to get the scan response data?

  • address The BLE address of the peripheral. You use this if you want to connect to it. See BleAddress .
  • advertisingData The advertising data provided by the peripheral. It's small (up to 31 bytes).
  • scanResponse The scan response data. This is an optional extra 31 bytes of data that can be provided by the peripheral. It requires an additional request to the peripheral, but is less overhead than connecting.
  • rssi The signal strength, which is a negative number of dBm. Numbers closer to 0 are a stronger signal.

When I want to answer questions like this in the Particle ecosystem that's not addressed in the reference docs I usually turn to the open source implementation and see what I can gather from there.
e.g.
https://github.com/particle-iot/device-os/blob/develop/wiring/inc/spark_wiring_ble.h
https://github.com/particle-iot/device-os/blob/develop/wiring/src/spark_wiring_ble.cpp

To get the entire content of the AdvertisingData buffer I'd go with BleAdvertisingData::get()
or directly refer to the internal buffer via BleAdvertisingData::data*

In order to interpret the data yourself you can traverse the buffer byte by byte. The format is quite well documented online (e.g. in a nutshell or more comprehensive)
However, you usually know what type of data you are interested in and then "asking" the BleAdvertisingData object for that specific data is much easier.

BTW, the are two BleAdvertisingData objects packed into one instance of BleScanResult

2 Likes

BTW, the are two BleAdvertisingData objects packed into one instance of BleScanResult

Just to be clear, does this mean there is no need to make a further request to get scanResponse data as the documentation suggests?

Understand the approach to looking at the open source - sometimes I struggle to understand the C++ though - :confused:

For instance, with the BleAddress object, I understand that I can use the = operator and compare addresses with a logical ==.
But how about assigning some test data to an array - this compiles but does not work

BleAddress storedDeviceMAC[STORED_DEVICE_MAX];
storedDeviceMAC[0] = {0x29,0x79,0x11,0xC9,0xEA,0x63};

then

if (storedDeviceMAC[0] == scanResults[ii].address) always returns false?

Where does it state that?
Seeing two objects in the class definition I'd assume both can be used at the same time otherwise there would not be need for two independent fields.

I'd have to have a look at the implementation but for your version to work we'd need an implicit cast operator that can produce a hal_ble_addr_t object from an int[] (assumed datatype for a { num, num, num } list).

You may want to have a look at the potential warnings your assignment instruction may produce.


Update:
Looking at the implementation of BleAddress it inherits from hal_ble_addr_t hence to get a properly initialised BleAddress you should fill all fields of that struct.

I'd assume an initialisation like this should work.

storedDeviceMAC[0] = (hal_ble_addr_t){ { 0x29,0x79,0x11,0xC9,0xEA,0x63 }, BLE_SIG_ADDR_TYPE_PUBLIC, 0 };

(not tested purely from looking at the limplementation)

For the addr_type field one of the following types should be used

1 Like

BleAddress object - I understand now that actually it is not 6 octets/bytes as explained but object assignment and comparison also stores 2 other bytes which aren’t so clear - thus simple setting of individual bytes using .address[0] = would work but not return true on the == operator due to the other bytes. This can be overcome by creating a put method to the list using the = operator.

Returning to the first question - this is what it says in the docs:
The scan response data. This is an optional extra 31 bytes of data that can be provided by the peripheral. It requires an additional request to the peripheral, but is less overhead than connecting.

How is this additional request performed? Is this via the BlePeerDevice object?

[Update] Hit Reply and hadn’t seen your edit!

Or by setting it via storedDeviceMAC[0].addr = {....}

No clue (yet :blush:)

I noticed that there is a call to get the scan response data:

ssize_t getScanResponseData(BleAdvertisingData* scanResponse) const;

Any advice or model about how to use this - does it need to be called in the scan callback handler and ssize_t gives the number of bytes or 0 if no data?

Maybe we can have @eugene0501 chime in on that.

Just going back to the Ble MAC address - the link to the header file shows that the struct for the type used by the BleAddress class is:

/* BLE device address */
typedef struct hal_ble_addr_t {
    uint8_t addr[BLE_SIG_ADDR_LEN];
    ble_sig_addr_type_t addr_type;
    uint8_t reserved;
} hal_ble_addr_t;

Thus, if I declare BleAddress storedMAC[10];
Sorry to be thick - why can I not access the MAC address with dot notation like storedMAC[].addr[]? Compiler is telling me that BleAddress has no member named addr.

My first thought would be hal_ble_addr_t is not the same as BleAddress.
And then I would look into the definition and implementation of BleAddress here

There is a private member hal_ble_addr_t address_ and to access that you need to use one of the methods that would hand it back to you (i.e. BleAddress::halAddress()) or you could use an overloaded operator (i.e. operator[]).

I found this thread while trying to build a similar system and thought I’d share a few of the things I found based on your existing work.

First, it appears that the documentation section regarding scanResponseData is probably incorrect. I suspect that it is a simple copy/paste error (because it looks the same as the text for setting the value for the local device). I can confirm that on my local Argon and Xenon devices, scanResponseData is automatically requested. It took me a little bit more looking in order to figure out why, but it appears that the active field on BleScanParams controls whether or not this extra data is requested. I found this via Nordic’s BLE Central tutorial:
https://devzone.nordicsemi.com/nordic/short-range-guides/b/bluetooth-low-energy/posts/ble-central-tutorial

Second, I found out that some devices will put the manufacturer specific data and/or name in the scan response rather than in the advertising packet. While this didn’t find names for all of the devices near my house, it did allow me to find the manufacturer’s id for every device I have scanned since the change. I modified my internal code to look like this:

        uint8_t buf[BLE_MAX_ADV_DATA_LEN];
        size_t len;
        len = scanResult.advertisingData.get(BleAdvertisingDataType::MANUFACTURER_SPECIFIC_DATA, buf, BLE_MAX_ADV_DATA_LEN);
        // comment above and uncomment below if using pointer for reference (i.e. callback overload)
        // len = scanResult->scanResponse.get(BleAdvertisingDataType::MANUFACTURER_SPECIFIC_DATA, buf, BLE_MAX_ADV_DATA_LEN);
        if (len == 0) {
            len = scanResult.scanResponse.get(BleAdvertisingDataType::MANUFACTURER_SPECIFIC_DATA, buf, BLE_MAX_ADV_DATA_LEN);
            // comment above and uncomment below if using pointer for reference (i.e. callback overload)
            // len = scanResult->scanResponse.get(BleAdvertisingDataType::MANUFACTURER_SPECIFIC_DATA, buf, BLE_MAX_ADV_DATA_LEN);
        }

I hope this helps with your own continued development.This text will be hidden

3 Likes

Thank you for sharing your discovery work and solution. I have been side tracked on other issues and haven’t done much more with this in the last 2 weeks - this definitely helps.
[Edit] your code snippet above doesn’t compile, shouldn’t

len = scanResult.advertisingData.get(BleAdvertisingDataType::MANUFACTURER_SPECIFIC_DATA, buf, BLE_MAX_ADV_DATA_LEN);
        if (len == 0) {
            len = scanResult.scanResponse.get(BleAdvertisingDataType::MANUFACTURER_SPECIFIC_DATA, buf, BLE_MAX_ADV_DATA_LEN);
        }

be changed to scanResult->scanReponse.get() and scanResult->advertisingData.get()?

You only need the -> notation if you're using the callback method for scanning, or if scanResult is a pointer for another reason. If you use the overload of scan that fills an array with values, the array returns the object itself. Also, I simply wrote a helper function that handles my printing/publishing to which I pass the scan result object. In the callback function, I then pass the object the pointer is referencing with the following notation:

PublishBLE(*scanResult);

I'll try to update my code above to make that syntax more clear.

I am using a callback method for scanning since otherwise can be a hostage to defined scan result array size!