Beware that this post is related to an SDK in maintenance mode
More Info: Consider nRF Connect SDK for new designs
This post is older than 2 years and might not be relevant anymore
More Info: Consider searching for newer posts

Dynamically updating Advertising data in SDK 15

(side note : it would have been handy if the migration guide mentioned that the advertising timeout configs have changed from seconds to 10's of ms)

It seems to have become harder to dynamically update the advertising packet in SDK15. In SDK14, using ble_advertising, I could just update the manufacturer data used by my instance of ble_advertising_t and call ble_advdata_set and be done. Then whenever ble_advertising did something later, it would continue with the latest data (eg auto restart advertising, change speed etc). Advertising would not be restarted and if you use fast and slow advertising, then that timing and transition would not be affected if you changed the manufacturer data.

Now, we need to use sd_ble_gap_adv_set_configure instead of ble_advdata_set, but the new call wont allow changing the encoded data contents and using the same buffer. You need to provide a new buffer if its busy advertising. Providing a new long lived buffer for each update causes many other issues. One way to deal with this is to stop and restart advertising with the same but updated buffer. But that affects the fast to slow advertising timing and transitions : say you update the data every 1 minute, and your fast advertising goes slow after 2 minutes, then you'll always be in fast mode.

Is there an easy and minimally impactful way to update just the manufacturer data in SDK15 ?

Parents
  • While the original poster has likely moved on, I am posting for others who come here with a similar issue. 

    My team also wanted to update the advertising data of a device dynamically. Stripping away any details that are beyond the scope of the question, our use case was a sensor which would advertise 1 when sensing an event and advertise 0 when not sensing an event. 

    We ran into the same problems using the SDK and API to dynamically update the advertising data as others here and in other forum posts. 

    We did find a working and efficient solution, however, and it is as follows:

    Note the ble_gap_adv_data_t struct which holds the advertising data. When initialized it would look something like:

    static uint8_t m_adv_handle = BLE_GAP_ADV_SET_HANDLE_NOT_SET;                   /**< Advertising handle used to identify an advertising set. */
    static uint8_t m_enc_advdata[BLE_GAP_ADV_SET_DATA_SIZE_MAX];                    /**< Buffer for storing an encoded advertising set. */
    static uint8_t m_enc_scan_response_data[BLE_GAP_ADV_SET_DATA_SIZE_MAX];         /**< Buffer for storing an encoded scan data. */
    
    /**@brief Struct that contains pointers to the encoded advertising data. */
    static ble_gap_adv_data_t m_adv_data =
    {
        .adv_data =
        {
            .p_data = m_enc_advdata,
            .len    = BLE_GAP_ADV_SET_DATA_SIZE_MAX
        },
        .scan_rsp_data =
        {
            .p_data = m_enc_scan_response_data,
            .len    = BLE_GAP_ADV_SET_DATA_SIZE_MAX
    
        }
    };

    The .adv_data.p_data data field is the right place to be looking. So let's follow .adv_data.p_data through the code and see what happens...

    First, let's look at the relevant bits of advertising_init()

    static void advertising_init(void)
    {
        .
        .
        .
    
        // Build and set advertising data.
        ble_advdata_t advdata;
        memset(&advdata, 0, sizeof(advdata));
        
        uint8_t data[]                      = {0xDE, 0xAD, 0xBE, 0xEF};
        manuf_data.company_identifier       = 0x0059;
        manuf_data.data.p_data              = data;
        manuf_data.data.size                = sizeof(data);
        advdata.p_manuf_specific_data       = &manuf_data;
    
        advdata.name_type          = BLE_ADVDATA_FULL_NAME;
        advdata.include_appearance = true;
        advdata.flags              = BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE;
    
        err_code = ble_advdata_encode(&advdata, m_adv_data.adv_data.p_data, &m_adv_data.adv_data.len);
        APP_ERROR_CHECK(err_code);
        
        NRF_LOG_INFO("m_adv_data.adv_data.p_data address %p", m_adv_data.adv_data.p_data);
        
        // Build and set scan response data.
        .
        .
        .
    
        // Build and set advertising parameters.
        ble_gap_adv_params_t adv_params;
        .
        .
        .
    
        err_code = sd_ble_gap_adv_set_configure(&m_adv_handle, &m_adv_data, &adv_params);
        APP_ERROR_CHECK(err_code);
    }

    Of important note here is that, to keep with Bluetooth standards, the ble_advdata_encode() function is used. This is because, from Source:

    "...the term advertising data refers to the 0..31 byte long payload that is available for application use. (In Bluetooth Core specification this field is referred to as AdvData).

    Advertising data consists of one or more Advertising Data (AD) elements. Each element is formatted as follows:

    • 1st byte: length of the element (excluding the length byte itself)
    • 2nd byte: AD type – specifies what data is included in the element
    • AD data – one or more bytes - the meaning is defined by AD type"

    Which is why our dynamic advertising data is in the manufacturer-specific data field.

    So we don't want to simply override the entire .adv_data.p_data or we will not be in compliance with Bluetooth standards (unless you take care to format your payload yourself). 

    The last call in the advertising_init() snippet was:

    err_code = sd_ble_gap_adv_set_configure(&m_adv_handle, &m_adv_data, &adv_params);

    which is a Soft Device call so all we can look at is the declaration of that function which can be found in ble_gap.h:

    SVCALL(SD_BLE_GAP_ADV_SET_CONFIGURE, uint32_t, sd_ble_gap_adv_set_configure(uint8_t *p_adv_handle, ble_gap_adv_data_t const *p_adv_data, ble_gap_adv_params_t const *p_adv_params));

    ...noting that SVCALL is a macro defined in nrf_svc.h and that this whole thing really does turn into a function definition. So why did we come all this way to look at a messy function declaration? We came so we could see the parameter ble_gap_adv_data_t const *p_adv_data, and in particular note that the pointer it is looking for is a constant. Therefore, while we can't know what sd_ble_gap_adv_set_configure(...) is doing since the soft device is proprietary, we can know that it expects the pointer to the advertising data to never change, which suggests the soft device will keep looking in the same location for advertising data (testing have verified this). 

    And that brings us to our solution, because we defined m_adv_data, so we can directly write to its address in memory. Writing to it is easy, we just use memcpy() or something like:

    m_adv_data.adv_data.p_data[your_offset] = 0x01;

    The only thing you need to do is determine your_offset, which is the where in p_data we want to put our dynamically updated payload, as it is going to be different depending on what fields you have in your advertising payload. Remember, we can't just write anywhere in p_data because the format is standardized, and only certain fields are meant to convey arbitrary data.

    And now you may realize why 

    NRF_LOG_INFO("m_adv_data.adv_data.p_data address %p", m_adv_data.adv_data.p_data);

    was in the advertising_init() function, and why the manuf_data.data.p_data was set to 0xDEADBEEF... it was so we could run the debugger to find the offset we need to write directly to the manufacturer-specific data section of m_adv_data.adv_data.p_data.

    Now, if we ran the debugger and look at the logs we would see the address of m_adv_data.adv_data.p_data printed. Then, we would go to the view->debug->memory option in Segger Embedded Studio and go to that address and look for 0xDEADBEEF. You count the bytes from the address of m_adv_data.adv_data.p_data to the first D in 0xDEADBEEF to get your offset. 

    As for my team's use case, we can now dynamically update the advertising data by writing 

    m_adv_data.adv_data.p_data[11] = 0x01;

    when the sensor picks up an event and 

    m_adv_data.adv_data.p_data[11] = 0x00;

    when it does not. This is a simple solution that bypasses using the advertising update functions of the SDK, and avoids other complex solutions like double buffering. It is just one line to change the advertising data dynamically. 

    Just be mindful, as since you are writing directly to memory nothing is error checking what you are writing. You need to make sure that you don't write more characters than your field allows, that you update the offset if you change the number of fields in the advertising payload, and that you don't write anything which breaks the Bluetooth standard (such as writing over length fields, etc).

    Hope that helps.

Reply
  • While the original poster has likely moved on, I am posting for others who come here with a similar issue. 

    My team also wanted to update the advertising data of a device dynamically. Stripping away any details that are beyond the scope of the question, our use case was a sensor which would advertise 1 when sensing an event and advertise 0 when not sensing an event. 

    We ran into the same problems using the SDK and API to dynamically update the advertising data as others here and in other forum posts. 

    We did find a working and efficient solution, however, and it is as follows:

    Note the ble_gap_adv_data_t struct which holds the advertising data. When initialized it would look something like:

    static uint8_t m_adv_handle = BLE_GAP_ADV_SET_HANDLE_NOT_SET;                   /**< Advertising handle used to identify an advertising set. */
    static uint8_t m_enc_advdata[BLE_GAP_ADV_SET_DATA_SIZE_MAX];                    /**< Buffer for storing an encoded advertising set. */
    static uint8_t m_enc_scan_response_data[BLE_GAP_ADV_SET_DATA_SIZE_MAX];         /**< Buffer for storing an encoded scan data. */
    
    /**@brief Struct that contains pointers to the encoded advertising data. */
    static ble_gap_adv_data_t m_adv_data =
    {
        .adv_data =
        {
            .p_data = m_enc_advdata,
            .len    = BLE_GAP_ADV_SET_DATA_SIZE_MAX
        },
        .scan_rsp_data =
        {
            .p_data = m_enc_scan_response_data,
            .len    = BLE_GAP_ADV_SET_DATA_SIZE_MAX
    
        }
    };

    The .adv_data.p_data data field is the right place to be looking. So let's follow .adv_data.p_data through the code and see what happens...

    First, let's look at the relevant bits of advertising_init()

    static void advertising_init(void)
    {
        .
        .
        .
    
        // Build and set advertising data.
        ble_advdata_t advdata;
        memset(&advdata, 0, sizeof(advdata));
        
        uint8_t data[]                      = {0xDE, 0xAD, 0xBE, 0xEF};
        manuf_data.company_identifier       = 0x0059;
        manuf_data.data.p_data              = data;
        manuf_data.data.size                = sizeof(data);
        advdata.p_manuf_specific_data       = &manuf_data;
    
        advdata.name_type          = BLE_ADVDATA_FULL_NAME;
        advdata.include_appearance = true;
        advdata.flags              = BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE;
    
        err_code = ble_advdata_encode(&advdata, m_adv_data.adv_data.p_data, &m_adv_data.adv_data.len);
        APP_ERROR_CHECK(err_code);
        
        NRF_LOG_INFO("m_adv_data.adv_data.p_data address %p", m_adv_data.adv_data.p_data);
        
        // Build and set scan response data.
        .
        .
        .
    
        // Build and set advertising parameters.
        ble_gap_adv_params_t adv_params;
        .
        .
        .
    
        err_code = sd_ble_gap_adv_set_configure(&m_adv_handle, &m_adv_data, &adv_params);
        APP_ERROR_CHECK(err_code);
    }

    Of important note here is that, to keep with Bluetooth standards, the ble_advdata_encode() function is used. This is because, from Source:

    "...the term advertising data refers to the 0..31 byte long payload that is available for application use. (In Bluetooth Core specification this field is referred to as AdvData).

    Advertising data consists of one or more Advertising Data (AD) elements. Each element is formatted as follows:

    • 1st byte: length of the element (excluding the length byte itself)
    • 2nd byte: AD type – specifies what data is included in the element
    • AD data – one or more bytes - the meaning is defined by AD type"

    Which is why our dynamic advertising data is in the manufacturer-specific data field.

    So we don't want to simply override the entire .adv_data.p_data or we will not be in compliance with Bluetooth standards (unless you take care to format your payload yourself). 

    The last call in the advertising_init() snippet was:

    err_code = sd_ble_gap_adv_set_configure(&m_adv_handle, &m_adv_data, &adv_params);

    which is a Soft Device call so all we can look at is the declaration of that function which can be found in ble_gap.h:

    SVCALL(SD_BLE_GAP_ADV_SET_CONFIGURE, uint32_t, sd_ble_gap_adv_set_configure(uint8_t *p_adv_handle, ble_gap_adv_data_t const *p_adv_data, ble_gap_adv_params_t const *p_adv_params));

    ...noting that SVCALL is a macro defined in nrf_svc.h and that this whole thing really does turn into a function definition. So why did we come all this way to look at a messy function declaration? We came so we could see the parameter ble_gap_adv_data_t const *p_adv_data, and in particular note that the pointer it is looking for is a constant. Therefore, while we can't know what sd_ble_gap_adv_set_configure(...) is doing since the soft device is proprietary, we can know that it expects the pointer to the advertising data to never change, which suggests the soft device will keep looking in the same location for advertising data (testing have verified this). 

    And that brings us to our solution, because we defined m_adv_data, so we can directly write to its address in memory. Writing to it is easy, we just use memcpy() or something like:

    m_adv_data.adv_data.p_data[your_offset] = 0x01;

    The only thing you need to do is determine your_offset, which is the where in p_data we want to put our dynamically updated payload, as it is going to be different depending on what fields you have in your advertising payload. Remember, we can't just write anywhere in p_data because the format is standardized, and only certain fields are meant to convey arbitrary data.

    And now you may realize why 

    NRF_LOG_INFO("m_adv_data.adv_data.p_data address %p", m_adv_data.adv_data.p_data);

    was in the advertising_init() function, and why the manuf_data.data.p_data was set to 0xDEADBEEF... it was so we could run the debugger to find the offset we need to write directly to the manufacturer-specific data section of m_adv_data.adv_data.p_data.

    Now, if we ran the debugger and look at the logs we would see the address of m_adv_data.adv_data.p_data printed. Then, we would go to the view->debug->memory option in Segger Embedded Studio and go to that address and look for 0xDEADBEEF. You count the bytes from the address of m_adv_data.adv_data.p_data to the first D in 0xDEADBEEF to get your offset. 

    As for my team's use case, we can now dynamically update the advertising data by writing 

    m_adv_data.adv_data.p_data[11] = 0x01;

    when the sensor picks up an event and 

    m_adv_data.adv_data.p_data[11] = 0x00;

    when it does not. This is a simple solution that bypasses using the advertising update functions of the SDK, and avoids other complex solutions like double buffering. It is just one line to change the advertising data dynamically. 

    Just be mindful, as since you are writing directly to memory nothing is error checking what you are writing. You need to make sure that you don't write more characters than your field allows, that you update the offset if you change the number of fields in the advertising payload, and that you don't write anything which breaks the Bluetooth standard (such as writing over length fields, etc).

    Hope that helps.

Children
  • Hi MD,

    I do believe I initially used your idea as my solution, but ultimately felt it was flawed due to the fact that I could not guarantee that the BLE stack would not send an advertisement out while I was in the middle of updating the data. For you it may not be an issue because you seem to only be updating a single byte; this was not my case. I am sure there may be some methods to ensure the data is updated "atomically" with regards to an advertisement packet being sent, but I opted to update the adv_data pointer to a different buffer instead as this just felt more reliable, IMO.

    Again, I find no issue with your solution for your particular case.

    Cheers!

  • We are also using the double buffering approach I linked to previously, since we update more than one byte but not more frequently than 1-10 minutes.

    https://devzone.nordicsemi.com/f/nordic-q-a/38056/advertising-data-update-nrf_error_invalid_addr 

  • Hello Josh West,

    I am glad you brought that weakness up as I almost mentioned it in my post before deciding the post was already long enough as it was ^-^ 

    You are correct that we cannot guarantee that the BLE stack will not read the advertising data when we are in the middle of the update if we are executing our changes in a regular runtime, and this limitation is important to point out. 

    There may be a way to protect ourselves from this, however, by using an interrupt to dynamically update the advertising data (this could be hardware or software driven) as the BLE Soft Device uses interrupt priorities of 1,4, and 5. Particularly, it uses priorities 4 and 5 for the BLE stack (GAP, GATT) and uses priority 1 for the radio. Thus, if the dynamic update of the advertising data happens in an interrupt of priority 2 or 3 we can be certain that the advertising data will not be read until we finish writing. 

    That being said, this interrupt would have to be very fast to avoid any timing issues with the BLE stack (I can't find the post now, but one Nordic developer recommended a few dozen microseconds for interrupts at priorities of 2 or 3 when using the BLE Soft Device). To see how safe we would be with timing we can run some numbers and note that advertising data is a max of 31 bytes and a STR instruction on the Cortex M4 (the NRF52840 MPU) takes two clock cycles. Let's be very generous and assume each of those 31 writes to memory takes 10 cycles (due to the prep involved), and let's assume that entering and leaving the interrupt takes 100 cycles each- at 64MHz we would still complete all of those cycles in about 8 microseconds. This is well within a few dozen microseconds, with some time left over for a few more quick operations.

    In fact, this is what my team did since we only updated the advertising data when a sensor value changed. We had the sensor trigger an interrupt and updated the advertising data in its interrupt service routine.

    I think for many situations your multi-buffer approach may be more appropriate, is quite elegant, and it plays within the SDK (which is always a plus). I merely wanted to provide an alternative approach for those scouring the forums for potential solutions, and noted that this method hadn't been shared yet.

    Glad to hear back from you :D

  • ShannonPahl,

    You sure showed me for starting with "While the original poster has likely moved on..."!

    Thanks for starting this thread and helping point people to good resources :)

Related