Concurrent Advertising

I have been asked several times about variations of the following advertising scenario, so I decided to make a blog post that I can refer to, and hope it might prove useful for others out there that are looking to do similar things. I recommend continuing to read and have a look at the uploaded code if you are curious about switching between different advertising payloads, and/or looking to use the SoftDevice API calls without the SDK Advertising Module.
The use case we will look closer at is to keep a low duty cycle, non-connectable advertising (broadcast) active independent of the other advertising or connections. In effect running "concurrently" along the other advertising or ongoing connection.

Updating advertising data only

Updating the content of the advertising data is simple enough to do with the SoftDevice API (see ble_gap.h for more information)

sd_ble_gap_adv_set_configure(avd_handle, adv_data, NULL)

Or if you are using the advertising module. (See ble_advertising for more info see ble_advertising.h)

ble_advertising_advdata_update(module_instance, new_advdata, true);

The duty cycle for rotating between advertising sets can be driven by a timer, or we can use the duration of the advertising itself (APP_ADV_DURATION in most projects) and start advertising with a new set once the old advertising completes (Using the BLE_GAP_EVT_ADV_SET_TERMINATED event).

Updating more than just the data can get a bit more tedius since it requires stopping and starting of the advertising. There are many good reasons why you could want different advertising data and/or parameters. Obvious examples being that the device wants to communicate a change in internal state. A temperature is being broadcast and the temperature changes (advertising data only). Other scenarios could be that the payload could be too large to fit in the initial advertising, the payload wants to appear to a scanning device with different capabilities for example switch between Legacy and Extended advertising (advertising parameters). A common thing that many of our SDK examples do, is change the duty cycle or "intensity" of the advertising at certain times. In the same place in the code, we may want to do other things such as adjust the output power of the radio for limited bursts.

Updating advertising parameters and data

Most examples in our SDK have "one advertiser", and then stop advertising when a connection is established.

Now picture a scenario where a "secondary" advertising, let's say a beacon, will be interjected with the primary advertising every 5 seconds. The beacon advertising will also continue it's (non-connectable) advertising every 5 seconds even while the device is in a connection and stopped its "primary" advertising.

Let's modify the good old Nordic UART Service example to do exactly that!

Above: Device A advertises as a Nordic UART Service (NUS). Every 5 seconds, a beacon advertisement is sent off.

Below: Device B is a NUS client that connected to Device A. NUS advertising stopped, but the beacon keeps firing every 5 seconds.

I used ble_app_uart as a starting point, and copied needed ble_app_beacon code over to it. I uploaded the resulting main.c file for those curious to have a look. Just swap the main.c file of ble_app_uart in SDK 15.3 to try it out. Note that the Advertising Module in the SDK is a bit rigid and mostly good for simple single advertising. I removed it from the example, since using the SoftDevice API calls directly in many cases gives more freedom without adding complexity.

The advertising parameters and data must be stored by the application, so we have to fashion something like below.

// Memory for parameters
static ble_gap_adv_params_t m_adv_params_beacon;
static ble_gap_adv_params_t m_adv_params_nus;

// Memory for advertising data
static uint8_t    m_enc_advdata_beacon[BLE_GAP_ADV_SET_DATA_SIZE_MAX];
static uint8_t    m_enc_advdata_nus[BLE_GAP_ADV_SET_DATA_SIZE_MAX];
static uint8_t    m_enc_srdata_nus[BLE_GAP_ADV_SET_DATA_SIZE_MAX];

static ble_gap_adv_data_t m_adv_data_beacon =
{
    .adv_data =
    {
        .p_data = m_enc_advdata_beacon,
        .len    = BLE_GAP_ADV_SET_DATA_SIZE_MAX
    },
    .scan_rsp_data =
    {
        .p_data = NULL,
        .len    = 0
    }
};

static ble_gap_adv_data_t m_adv_data_nus =
{
    .adv_data =
    {
        .p_data = m_enc_advdata_nus,
        .len    = BLE_GAP_ADV_SET_DATA_SIZE_MAX
    },
    .scan_rsp_data =
    {
        .p_data = m_enc_srdata_nus,
        .len    = BLE_GAP_ADV_SET_DATA_SIZE_MAX
    }
};

next we fill the params and advdata with advertising_init_nus() and advertising_init_beacon(). For the advdata, we could fill in the array manually, but choose to use the helper function ble_advdata_encode().

After encoding would be the right place to call sd_ble_adv_set_configure(). But at the time of writing (SoftDevice 6.1.1, SDK 15.3.0) the SoftDevice only supports one advertising set for legacy advertising. We can't "save" sets and refer to them by their handle later. Because of this, the best place to define the advertising set is right before we begin the desired advertising; Always replacing the current set with the new set we are about to advertise.

static void advertising_start_nus(void)
{
    ret_code_t err_code;

    (void)sd_ble_gap_adv_stop(m_adv_handle);

    err_code = sd_ble_gap_adv_set_configure(&m_adv_handle, &m_adv_data_nus, &m_adv_params_nus);
    APP_ERROR_CHECK(err_code);

    err_code = sd_ble_gap_adv_start(m_adv_handle, APP_BLE_CONN_CFG_TAG);
    APP_ERROR_CHECK(err_code);

    err_code = bsp_indication_set(BSP_INDICATE_ADVERTISING);
    APP_ERROR_CHECK(err_code);
}

static void advertising_start_beacon(void)
{
    ret_code_t err_code;

    sd_ble_gap_adv_stop(m_adv_handle);

    err_code = sd_ble_gap_adv_set_configure(&m_adv_handle, &m_adv_data_beacon, &m_adv_params_beacon);
    APP_ERROR_CHECK(err_code);

    err_code = sd_ble_gap_adv_start(m_adv_handle, APP_BLE_CONN_CFG_TAG);
    APP_ERROR_CHECK(err_code);
}

An easy condition to update advertising parameters and data would be to set a duration for the nus advertising to time out, and then start the beacon advertising that in turn also terminates (below-right image). A toggling flag decides whose turn it is whenever an advertising terminates.

But if we are in a connection, the NUS advertising will not happen (meaning that the termination event will also not happen), and we still want to trigger the Beacon periodically. We therefore set up an "interval" timer for the beacon advertising, that will trigger every 5 seconds regardless of the NUS advertising. Once the beacon advertising terminates, we start the NUS advertising again. Unless we are in a connection (below-left image).

          

The dependencies for the timer is already present in the ble_app_uart application. We simply add the definition for out beacon timer:

APP_TIMER_DEF(m_beacon_interval_timer_id);

Modify timers_init() to create the timer:

/**@brief Function for initializing the timer module.
 */
static void timers_init(void)
{
    app_timer_init();

    app_timer_create(&m_beacon_interval_timer_id,
                    APP_TIMER_MODE_SINGLE_SHOT,
                    beacon_timeout_handler);
}

The job of beacon_timeour_handler() is just to call advertising_start_beacon(). This switching itself would happen in ble_evt_handler() where all the SoftDevice events are received by the application. Including the advertising termination event.

case BLE_GAP_EVT_ADV_SET_TERMINATED:
{
    if (  p_ble_evt->evt.gap_evt.params.adv_set_terminated.reason == BLE_GAP_EVT_ADV_SET_TERMINATED_REASON_TIMEOUT ||
          p_ble_evt->evt.gap_evt.params.adv_set_terminated.reason == BLE_GAP_EVT_ADV_SET_TERMINATED_REASON_LIMIT_REACHED)
    {
        err_code = app_timer_start(m_advertising_delay_id,
                                   APP_TIMER_TICKS(5000),
                                   NULL);
        APP_ERROR_CHECK(err_code);

        // If we are not connected, continue to advertise as a nus device.
        // If not, the next time we advertise will be as a beacon when the
        // beacon_interval timer times out again.
        if(m_conn_handle == BLE_CONN_HANDLE_INVALID)
        {
            advertising_start_nus();
        }
    }
} break;

The main function of the application is modified to run the new inits and kick off the first beacon advertisement.

int main(void)
{
    ...
    advertising_init_nus();
    advertising_init_beacon();
    conn_params_init();

    // Start execution.
    NRF_LOG_INFO("Debug logging for NUS+iBeacon over RTT started.");
    advertising_start_beacon();

    // Enter main loop.
    for (;;)
    {
        idle_state_handle();
    }
}

Testing

Now to download and test the application! After flashing, I used the new nRF Connect for iOS app.

Success! Nordic UART shows up. We can connect to it and use it just like the normal ble_app_uart example. However, the beacon advertising was hard to catch on nRF Connect because they are so sparse compared to the NUS advertising. I added a filter to look for our company identifier, and that did the trick!

   

To test it out for yourself, simply replace the main file of <sdk>\examples\ble_peripheral\ble_app_uart with the main.c included in the uploaded zip. I tested this with SDK 15.3.0

Bonus: nRF Connect SDK

A similar scheme is surprisingly simple to set up with nRF Connect SDK.

In the "peripheral UART" example, we can define a new thread:

K_THREAD_DEFINE(ble_adv_thread_id, STACKSIZE, ble_adv_thread, NULL, NULL,
NULL, PRIORITY, 0, K_FOREVER);

Run k_thread_start(ble_adv_thread_id); after the Nordic UART Service is initialized to start advertising.

The thread itself will advertise NUS for 5 seconds (if there are still connection available), and then one burst of iBeacon broadcasting.

Note that the Bluetooth stack in zephyr currently does not support specifying the number of advertisements like the SoftDevice used by nRF5 SDK can do. To send out one beacon payload, we specify the sleep time before stopping to be equal to the advertising interval, +10 ms to account for random time added according to the Bluetooth specification, so that we don't accidentally stop advertising before a packet has been sent out.

static void ble_adv_thread(void)
{
  int err = 0;
  for(;;) {
          (void) bt_le_adv_stop();
          err = bt_le_adv_start(BT_LE_ADV_CONN, ad, ARRAY_SIZE(ad), 
                                       sd, ARRAY_SIZE(sd));
          if (err && (err !=ECONNREFUSED)) {
                  printk("Advertising failed to start (err %d)\n", err);
          }
          k_sleep(5000);
          (void) bt_le_adv_stop();

          //Send out one iBeacon advertisement.
          err = bt_le_adv_start(BT_LE_ADV_NCONN, ad_beacon, ARRAY_SIZE(ad_beacon),
                                        0, 0);
          if (err) {
                  printk("Beacon advertising failed to start (err %d)\n", err);
          }

          k_sleep(BT_GAP_ADV_FAST_INT_MAX_2+10); //Advertising will add a small random delay so we add 10 ms to make sure we can send at least one advertising.
  }
}

For this ncs example it was also enough to edit only the main.c file.

Replace the file in <ncs>\samples\bluetooth\peripheral_uart\src. I tested it on NCS 1.0.0.

main.zip
Anonymous