BLE Hands on Beacon - joe-possum/IoT-Developer-Boot-Camp GitHub Wiki

In this worksheet we will explore implementing a Eddystone TLM beacon using a custom advertisement packet. We will introduce the Universal Configurator which allows us to add and configure features from the SDK.

As covered in the earlier Boot Camp material, a beacon broadcasts information without requiring connection. This is done using advertisements, these may be connectable, but ideally would be non-connectable. It might be useful to connect to a beaconing device to configure it, but it would be more elegant to have a non-connectable beacon and an additional connectable advertisement for configuration.

Bluetooth Specification for AdvData

The Bluetooth Core specification (Section 11, in Version 5.3, Vol 3, Part C) describes the format of AdvData as follows:

The data consists of a significant part and a nonsignificant part. The significant part contains a sequence of AD structures. Each AD structure shall have a Length field of one octet, which contains the Length value, and a Data field of Length octets. The first octet of the Data field contains the AD type field. The content of the remaining Length - 1 octets in the Data field depends on the value of the AD type field and is called the AD data.

AdvData

By way of example, the AdvData for the SoC Thermometer example contains the following:

AdvData

This encodes the following:

Type Data
0x01 (Flags) 0x06
0x03 (Complete List of 16-bit Service Class UUIDs) 0x1809 (Health Thermometer)
0x09 (Complete Local Name) "Thermometer Example"

This data was automatically generated by the stack an can be influenced by settings in GATT database.

The Gecko GSDK 4.x provides the function sl_bt_legacy_advertiser_set_data() which allow us to set the significant part of an advertising, or scan response, packet.

The values used for AD Type are defined in the Generic Access Profile document. Here I quote some of the relevant definitions from that document:

Data Type Value Data Type Name Reference for Definition
0x01 «Flags» Bluetooth Core Specification:Vol. 3, Part C, section 8.1.3 (v2.1 + EDR, 3.0 + HS and 4.0)Vol. 3, Part C, sections 11.1.3 and 18.1 (v4.0)Core Specification Supplement, Part A, section 1.3
0x03 «Complete List of 16-bit Service Class UUIDs» Bluetooth Core Specification:Vol. 3, Part C, section 8.1.1 (v2.1 + EDR, 3.0 + HS and 4.0)Vol. 3, Part C, sections 11.1.1 and 18.2 (v4.0)Core Specification Supplement, Part A, section 1.1
0x09 «Complete Local Name» Bluetooth Core Specification:Vol. 3, Part C, section 8.1.2 (v2.1 + EDR, 3.0 + HS and 4.0)Vol. 3, Part C, sections 11.1.2 and 18.4 (v4.0)Core Specification Supplement, Part A, section 1.2
0x16 «Service Data» Bluetooth Core Specification:Vol. 3, Part C, sections 11.1.10 and 18.10 (v4.0)

Eddystone

The Eddystone Beacon specification is published at Github/google/eddystone. Summarizing, Google have been assigned a 16-bit Service UUID, 0xFEAA for the Eddystone Service. The specification for an Eddystone Beacon requires that the AdvData:

  • Include the Eddystone UUID in Complete List of 16-bit Service Class UUIDs AD Data
  • Include an Eddystone Frame as data in Service Data AD Data

An Eddystone Frame for unencrypted TLM data is encoded as

AdvData

All multibyte fields are big endian so conversion will be required since the Gecko devices are little endian. In the case where a device is not powered from a battery, Battery Voltage should be zeroed. Beacon Temperature is specified as Celcius in "8.8 fixed-point notation" with a link that appears to suffered link-rot, however appears to agree with round(256*celcius). If not supported the value should be set to 0x8000, -128 °C. PDU count is the running count of advertisement frames of all types emitted by the beacon since power-up or reboot. Time Since Power On is a 0.1 second resolution counter that represents time since beacon power-up or reboot.

The total payload we need to produce is

AdvData

Sources of Data

We can use TEMPDRV to read the on-chip temperature sensor. There may be some devices which do not include this, in which case -128 °C should be hardcoded. We can obtain the number of advertising packets sent using sl_bt_system_get_counters(). This will provide total number of packets transmitted, but since all transmitted packets are advertising, this will be simple. The time since boot can be determined using sl_sleeptimer_get_tick_count64() and sl_sleeptimer_get_timer_frequency().

Implementation

As in the earlier exercises, from the Launcher perspective, select the WSTK and locate the Bluetooth - SoC Empty project under the "EXAMPLE PROJECTS & DEMOS" tab. Create the project. Once in the Simplicity IDE, locate the file "soc_empty.slcp" in the Project Explorer (1). Double click on this file, this will display the project configuration tool where we can add, remove and configure system components. Select the "SOFTWARE COMPONENTS" tab (2). While it is possible to browse through the tree of available components, we highly recommend using the search feature. In this case, the component we want to add is TEMPDRV. Type "TEMPDRV" into the search box (3). Select the TEMPDRV component (4). Click "INSTALL" (5).

AdvData

TEMPDRV should now have a check mark to the left (6). The gear at the right, (7), indicates it has configurable options. These options are related to setting callbacks for over/under temperature and interrupt management, none of which are relevant to this exercise.

AdvData

Locate and open "app.c". As you can see it is very similar to the version in the Bluetooth - SoC Themometer project. Since we will use TEMPDRV to read the chip temperature, we should include the header files, "tempdrv.h". Simplicity IDE has a nice completion feature: Locate the include section in "app.c". Start typing "#inc"

AdvData

, then press [Ctrl]-[Space]. "#include " will be autocompleted.

AdvData

Type "temp' with an opening quote, then press [Ctrl]-[Space]. Since there is not a unique completion, a dropdown list of possible completions is shown.

AdvData

There are a number of ways we could address constructing the AdvData packet. My preference is to use a packed structure. Add the following code after the declaration of advertising_set_handle (approximately line 38).

struct __attribute__((packed)) {
  uint8_t len_serviceList, type_serviceList;
  uint16_t serviceList[1];
  uint8_t len_serviceData, type_serviceData;
  uint16_t id;
  uint8_t frameType, version;
  uint16_t be_mvBattery;
  int16_t be_temperature;
  uint32_t be_pduCount, be_time;
} advData = { .len_serviceList = 3,
    .type_serviceList = 3,
    .serviceList = { 0xfeaa },
    .len_serviceData = 0x11,
    .type_serviceData = 0x16,
    .id = 0xfeaa,
    .frameType = 0x20,
    .version = 0,
    .be_mvBattery = 0,
};

__attribute__((packed)) is critical, since it overrides the compiler's default behavior of padding structures so that n-byte fields are aligned to addresses which are multiples of n, which would result in a 2-byte gap between be_temperature and be_pduCount. The multi-byte fields in the TLM Frame at the end are prefixed with be_ as a memory-aid that they are big-endian, not the native little-endian.

Since we know the size of the structure should be 22 bytes, we can add a sanity check in app_init().

void app_init(void)
{
  app_assert(22 == sizeof(advData));
}

As a starting point we can define a function to populate the TLM fields and set the advertising data. The following sets fixed values for the telemetry data. Refer to sl_bt_legacy_advertiser_set_data. Note in particular that this function updates the content of an active advertising channel.

void setAdvData(void) {
  sl_status_t sc;
  advData.be_temperature = __builtin_bswap16((int16_t) (256*23.4));  // 23.4 celcius
  advData.be_pduCount = __builtin_bswap32((uint32_t)9001u);  // more than 9000
  advData.be_time = __builtin_bswap32((uint32_t) 10u*((15*24+7)*3600));  // 7 hours, 15 days
  sc = sl_bt_legacy_advertiser_set_data(advertising_set_handle, 
                                        sl_bt_advertiser_advertising_data_packet, // type 
                                        22,  // AdvData packet length
                                        (uint8_t*)&advData); // AdvData payload
  app_assert_status(sc);
}

Since the Beacon will not be connectable, we can simplify sl_bt_on_event. It only requires a handler for the system-boot event, where we allocate an advertising handle using sl_bt_advertiser_create_set, set the advertising interval with sl_bt_advertiser_set_timing, set the advertising data using the above defined setAdvData, then start advertising with sl_bt_legacy_advertiser_start.

void sl_bt_on_event(sl_bt_msg_t *evt)
{
  sl_status_t sc;

  switch (SL_BT_MSG_ID(evt->header)) {
    // -------------------------------
    // This event indicates the device has started and the radio is ready.
    // Do not call any stack command before receiving this boot event!
    case sl_bt_evt_system_boot_id:

      // Create an advertising set.
      sc = sl_bt_advertiser_create_set(&advertising_set_handle);
      app_assert_status(sc);

      // Set advertising interval to 100ms.
      sc = sl_bt_advertiser_set_timing(
        advertising_set_handle,
        160, // min. adv. interval (milliseconds * 1.6)
        160, // max. adv. interval (milliseconds * 1.6)
        0,   // adv. duration
        0);  // max. num. adv. events
      app_assert_status(sc);

      // Set initial data before starting advertisement
      setAdvData();

      // Start advertising
      sc = sl_bt_legacy_advertiser_start(
        advertising_set_handle, 
        sl_bt_advertiser_non_connectable);
      app_assert_status(sc);
      break;

    ///////////////////////////////////////////////////////////////////////////
    // Add additional event handlers here as your application requires!      //
    ///////////////////////////////////////////////////////////////////////////

    // -------------------------------
    // Default event handler.
    default:
	  app_assert("No other events expected");
      break;
  }
}

This code appears to be sufficient to run, however you may have noted that the original system-boot event handler started advertising using sl_bt_advertiser_start which is now marked deprecated. As of GSDK release 4.0.0, Bluetooth - SoC Empty is using the deprecated call and the Legacy Advertising component is not included in the project. However, this provides an opportunity to debug though such an issue.

Launch the debugger on the application by selecting the project root in the Project Explorer (1), then clicking the Debug icon, (2).

AdvData

Resume execution by clicking the Resume icon, (3).

AdvData

Let the application run for a few seconds, then click the Suspend icon, (4).

AdvData

If execution has halted at an assert, then there is an issue, specifically that sc has a non-successful value indicating the sl_bt_legacy_advertiser_set_data call failed.

AdvData

We can examine the value of sc to determine the issue. We can get the value by several methods, the quickest is to hover the mouse over the variable, a dialog will pop up.

AdvData

Alternatively, we could use the Variables pane

AdvData

We can look up this value by examining the definition of sc. Click on sc to set the cursor on it, then right-click, a context menu will pop up. Select Open Declaration.

AdvData

Notice the cursor has moved to the declaration of sc at line 62. Repeat this procedure on sl_status_t.

AdvData

This will open "sl_status.h".

AdvData

Scrolling up we can find the definition of error code, 15, SL_STATUS_NOT_SUPPORTED at line 99 below.

AdvData

This is the error code which is returned when a call is made to a Bluetooth feature which is optional and the component has not been included in the project.

Disconnect the debugger using the Disconnect icon, (5).

AdvData

Return to the Simplicity IDE, (6), and select the "soc_empty.slcp" file, (7), to add the Legacy Advertiser component. Enter "legacy" in the search box. Select "Legacy Advertising" component under Bluetooth / Feature. Click "INSTALL" (8).

AdvData

Repeat the procedure to execute the application under debugger control. After running for a few seconds click Suspend. You should see something similar to the following. When advertising the device wakes up periodically to transmit the current advertisement, then returns to sleep. When execution is suspended, the __WFI call is interrupted. This indicates normal operation. Disconnect the debugger from the device.

Test with EFR Connect

Reopen the EFR Connect mobile app. This time switch to Developer view (6) and select the Browser icon, (7).

AdvData

This will list all Bluetooth advertisers observed. If you are in a confronted with a massive list you can simplify life by sorting (8) devices by RSSI .

AdvData

Select "descending".

AdvData

Placing the mobile close to the WSTK, the device should appear at the top of the list. Recall that iOS will associate the device address (which is hidden from the app developer) with the name it most recently read from GATT, so even though this beacon does not have a name, iOS shows the name "Themom...". On the bottom row of the entry, EFR Connect indicates that it recognizes the device as an Eddystone beacon (9).

AdvData

Tapping the entry we can see more detail, specifically, at the bottom the Eddystone Data displayed is consistent with the values we hard coded,

AdvData

Implementing Real Data

Replacing the hard coded values in the beacon with real values is left as an exercise. The following hints may be useful:

  • Recall that app_process_action is called before the device goes to sleep. The device will periodically wake up to transmit an advertisement.
  • No Bluetooth stack calls shoud be made before the system-boot event has been received.
  • The values fetched by sl_bt_system_get_counters() at 16-bit, but the advertisement counter in the TLM frame is 32-bit.
  • sl_bt_system_get_counters() has a reset flag which will clear the counters after reading.

If you get stuck, you can take a look at my solution.

⚠️ **GitHub.com Fallback** ⚠️