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.
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.
By way of example, the AdvData for the SoC Thermometer example contains the following:
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) |
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
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
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().
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).
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.
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"
, then press [Ctrl]-[Space]. "#include " will be autocompleted.
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.
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).
Resume execution by clicking the Resume icon, (3).
Let the application run for a few seconds, then click the Suspend icon, (4).
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.
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.
Alternatively, we could use the Variables pane
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.
Notice the cursor has moved to the declaration of sc
at line 62. Repeat this procedure on sl_status_t
.
This will open "sl_status.h".
Scrolling up we can find the definition of error code, 15, SL_STATUS_NOT_SUPPORTED
at line 99 below.
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).
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).
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.
Reopen the EFR Connect mobile app. This time switch to Developer view (6) and select the Browser icon, (7).
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 .
Select "descending".
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).
Tapping the entry we can see more detail, specifically, at the bottom the Eddystone Data displayed is consistent with the values we hard coded,
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.