In this document we will be seeing how to create a BLE demo application for an adopted BLE profile based on another demo application with a different profile. In this demo, the Pulse Oximeter Profile will be implemented. The PLX (Pulse Oximeter) Profile was adopted by the Bluetooth SIG on 14th of July 2015. You can download the adopted profile and services specifications on https://www.bluetooth.org/en-us/specification/adopted-specifications.
The files that will be modified in this post are, app.c, app_config.c, app_preinclude.h, gatt_db.h, pulse_oximeter_service.c and pulse_oximeter_interface.h.
A profile can have many services, the specification for the PLX profile defines which services need to be instantiated. The following table shows the Sensor Service Requirements.
Service | Sensor |
Pulse Oximeter Service | Mandatory |
Device Information Service | Mandatory |
Current Time Service | Optional |
Bond Management Service | Optional |
Battery Service | Optional |
Table 1. Sensor Service Requirements
For this demo we will instantiate the PLX service, the Device Information Service and the Battery Service. Each service has a source file and an interface file, the device information and battery services are already implemented, so we will only need to create the pulse_oximeter_interface.h file and the pulse_oximeter_service.c file.
The PLX Service also has some requirements, these can be seen in the PLX service specification. The characteristic requirements for this service are shown in the table below.
Characteristic Name | Requirement | Mandatory Properties | Security Permissions |
PLX Spot-check Measurement | C1 | Indicate | None |
PLX Continuous Measurement | C1 | Notify | None |
PLX Features | Mandatory | Read | None |
Record Access Control Point | C2 | Indicate, Write | None |
Table 2. Pulse Oximeter Service Characteristics
C1: Mandatory to support at least one of these characteristics.
C2: Mandatory if measurement storage is supported for Spot-check measurements.
For this demo, all the characteristics will be supported.
Create a folder for the pulse oximeter service in \ConnSw\bluetooth\profiles named pulse_oximeter and create the pulse_oximeter_service.c file. Next, go to the interface folder in \ConnSw\bluetooth\profiles and create the pulse_oximeter_interface.h file. At this point these files will be blank, but as we advance in the document we will be adding the service implementation and the interface macros and declarations.
Clonate a BLE project with the cloner tool. For this demo the heart rate sensor project was clonated. You can choose an RTOS between bare metal or FreeRTOS. You will need to change some workspace configuration. In the bluetooth->profiles->interface group, remove the interface file for the heart rate service and add the interface file that we just created. Rename the group named heart_rate in the bluetooth->profiles group to pulse_oximeter and remove the heart rate service source file and add the pulse_oximeter_service.c source file. These changes will be saved on the actual workspace, so if you change your RTOS you need to reconfigure your workspace.
To change the device name that will be advertised you have to change the advertising structure located in app_config.h.
/* Scanning and Advertising Data */
static const uint8_t adData0[1] = { (gapAdTypeFlags_t)(gLeGeneralDiscoverableMode_c | gBrEdrNotSupported_c) };
static const uint8_t adData1[2] = { UuidArray(gBleSig_PulseOximeterService_d)};
static const gapAdStructure_t advScanStruct[] = {
{
.length = NumberOfElements(adData0) + 1,
.adType = gAdFlags_c,
.aData = (void *)adData0
},
{
.length = NumberOfElements(adData1) + 1,
.adType = gAdIncomplete16bitServiceList_c,
.aData = (void *)adData1
},
{
.adType = gAdShortenedLocalName_c,
.length = 8,
.aData = "FSL_PLX"
}
};
We also need to change the address of the device so we do not have conflicts with another device with the same address. The definition for the address is located in app_preinclude.h and is called BD_ADDR. In the demo it was changed to:
#define BD_ADDR 0xBE,0x00,0x00,0x9F,0x04,0x00
Add the definitions in ble_sig_defines.h located in Bluetooth->host->interface for the UUID’s of the PLX service and its characteristics.
/*! Pulse Oximeter Service UUID */
#define gBleSig_PulseOximeterService_d 0x1822
/*! PLX Spot-Check Measurement Characteristic UUID */
#define gBleSig_PLXSpotCheckMeasurement_d 0x2A5E
/*! PLX Continuous Measurement Characteristic UUID */
#define gBleSig_PLXContinuousMeasurement_d 0x2A5F
/*! PLX Features Characteristic UUID */
#define gBleSig_PLXFeatures_d 0x2A60
We need to create the GATT database for the pulse oximeter service. The requirements for the service can be found in the PLX Service specification. The database is created at compile time and is defined in the gatt_db.h. Each characteristic can have certain properties such as read, write, notify, indicate, etc. We will modify the existing database according to our needs. The database for the pulse oximeter service should look something like this.
PRIMARY_SERVICE(service_pulse_oximeter, gBleSig_PulseOximeterService_d)
CHARACTERISTIC(char_plx_spotcheck_measurement, gBleSig_PLXSpotCheckMeasurement_d, (gGattCharPropIndicate_c))
VALUE_VARLEN(value_PLX_spotcheck_measurement, gBleSig_PLXSpotCheckMeasurement_d, (gPermissionNone_c), 19, 3, 0x00, 0x00, 0x00)
CCCD(cccd_PLX_spotcheck_measurement)
CHARACTERISTIC(char_plx_continuous_measurement, gBleSig_PLXContinuousMeasurement_d, (gGattCharPropNotify_c))
VALUE_VARLEN(value_PLX_continuous_measurement, gBleSig_PLXContinuousMeasurement_d, (gPermissionNone_c), 20, 3, 0x00, 0x00, 0x00)
CCCD(cccd_PLX_continuous_measurement)
CHARACTERISTIC(char_plx_features, gBleSig_PLXFeatures_d, (gGattCharPropRead_c))
VALUE_VARLEN(value_plx_features, gBleSig_PLXFeatures_d, (gPermissionFlagReadable_c), 7, 2, 0x00, 0x00)
CHARACTERISTIC(char_RACP, gBleSig_RaCtrlPoint_d, (gGattCharPropIndicate_c | gGattCharPropWrite_c))
VALUE_VARLEN(value_RACP, gBleSig_RaCtrlPoint_d, (gPermissionFlagWritable_c), 4, 3, 0x00, 0x00, 0x00)
CCCD(cccd_RACP)
For more information on how to create a GATT database you can check the BLE Application Developer’s Guide chapter 7.
Now we need to make the interface file that contains all the macros and declarations of the structures needed by the PLX service. Enumerated types need to be created for each of the flags field or status field of every characteristic of the service. For example, the PLX Spot-check measurement field has a flags field, so we declare an enumerated type that will help us keep the program organized and well structured. The enum should look something like this:
/*! Pulse Oximeter Service - PLX Spotcheck Measurement Flags */
typedef enum
{
gPlx_TimestampPresent_c = BIT0, /* C1 */
gPlx_SpotcheckMeasurementStatusPresent_c = BIT1, /* C2 */
gPlx_SpotcheckDeviceAndSensorStatusPresent_c = BIT2, /* C3 */
gPlx_SpotcheckPulseAmplitudeIndexPresent_c = BIT3, /* C4 */
gPlx_DeviceClockNotSet_c = BIT4
} plxSpotcheckMeasurementFlags_tag;
The characteristics that will be indicated or notified need to have a structure type that contains all the fields that need to be transmitted to the client. Some characteristics will not always notify or indicate the same fields, this varies depending on the flags field and the requirements for each field. In order to notify a characteristic we need to check the flags in the measurement structure to know which fields need to be transmitted. The structure for the PLX Spot-check measurement should look something like this:
/*! Pulse Oximeter Service - Spotcheck Measurement */
typedef struct plxSpotcheckMeasurement_tag
{
ctsDateTime_t timestamp; /* C1 */
plxSpO2PR_t SpO2PRSpotcheck; /* M */
uint32_t deviceAndSensorStatus; /* C3 */
uint16_t measurementStatus; /* C2 */
ieee11073_16BitFloat_t pulseAmplitudeIndex; /* C4 */
uint8_t flags; /* M */
}plxSpotcheckMeasurement_t;
The service has a configuration structure that contains the service handle, the initial features of the PLX Features characteristic and a pointer to an allocated space in memory to store spot-check measurements. The interface will also declare some functions such as Start, Stop, Subscribe, Unsubscribe, Record Measurements and the control point handler.
/*! Pulse Oximeter Service - Configuration */
typedef struct plxConfig_tag
{
uint16_t serviceHandle;
plxFeatures_t plxFeatureFlags;
plxUserData_t *pUserData;
bool_t procInProgress;
} plxConfig_t;
The service source file implements the service specific functionality. For example, in the PLX service, there are functions to record the different types of measurements, store a spot-check measurement in the database, execute a procedure for the RACP characteristic, validate a RACP procedure, etc. It implements the functions declared in the interface and some static functions that are needed to perform service specific tasks.
To initialize the service you use the start function. This function initializes some characteristic values. In the PLX profile, the Features characteristic is initialized and a timer is allocated to indicate the spot-check measurements periodically when the Report Stored Records procedure is written to the RACP characteristic. The subscribe and unsubscribe functions are used to update the device identification when a device is connected to the server or disconnected.
bleResult_t Plx_Start (plxConfig_t *pServiceConfig)
{
mReportTimerId = TMR_AllocateTimer();
return Plx_SetPLXFeatures(pServiceConfig->serviceHandle, pServiceConfig->plxFeatureFlags);
}
All of the services implementations follow a similar template, each service can have certain characteristics that need to implement its own custom functions. In the case of the PLX service, the Record Access Control Point characteristic will need many functions to provide the full functionality of this characteristic. It needs a control point handler, a function for each of the possible procedures, a function to validate the procedures, etc.
When the application makes a measurement it must fill the corresponding structure and call a function that will write the attribute in the database with the correct fields and then send an indication or notification. This function is called RecordMeasurement and is similar between the majority of the services. It receives the measurement structure and depending on the flags of the measurement, it writes the attribute in the GATT database in the correct format. One way to update a characteristic is to create an array of the maximum length of the characteristic and check which fields need to be added and keep an index to know how many bytes will be written to the characteristic by using the function GattDb_WriteAttribute(handle, index, &charValue[0]). The following function shows an example of how a characteristic can be updated. In the demo the function contains more fields, but the logic is the same.
static bleResult_t Plx_UpdatePLXContinuousMeasurementCharacteristic
(
uint16_t handle,
plxContinuousMeasurement_t *pMeasurement
)
{
uint8_t charValue[20];
uint8_t index = 0;
/* Add flags */
charValue[0] = pMeasurement->flags;
index++;
/* Add SpO2PR-Normal */
FLib_MemCpy(&charValue[index], &pMeasurement->SpO2PRNormal, sizeof(plxSpO2PR_t));
index += sizeof(plxSpO2PR_t);
/* Add SpO2PR-Fast */
if (pMeasurement->flags & gPlx_SpO2PRFastPresent_c)
{
FLib_MemCpy(&charValue[index], &pMeasurement->SpO2PRFast, sizeof(plxSpO2PR_t));
index += sizeof(plxSpO2PR_t);
}
return GattDb_WriteAttribute(handle, index, &charValue[0]);
}
The app.c handles the application specific functionality. In the PLX demo it handles the timer callback to make a PLX continuous measurement every second. It handles the key presses and makes a spot-check measurement each time the SW3 pushbutton is pressed. The GATT server callback receives an event when an attribute is written, and in our application the RACP characteristic is the only one that can be written by the client. When this event occurs, we call the Control Point Handler function. This function makes sure the indications are properly configured and check if another procedure is in progress. Then it calls the Send Procedure Response function, this function validates the procedure and calls the Execute Procedure function. This function will call one of the 4 possible procedures. It can call Report Stored Records, Report Number of Stored Records, Abort Operation or Delete Stored Records.
When the project is running, the 4 LEDs will blink indicating an idle state. To start advertising, press the SW4 button and the LED1 will start flashing. When the device has connected to a client the LED1 will stop flashing and turn on. To disconnect the device, hold the SW4 button for some seconds. The device will return to an advertising state.
In this demo, the spot-check measurement is made when the SW3 is pressed, and the continuous measurement is made every second. The spot-check measurement can be stored by the application if the Measurement Storage for spot-check measurements is supported (bit 2 of Supported Features Field in the PLX Features characteristic). The RACP characteristic lets the client control the database of the spot-check measurements, you can request the existing records, delete them, request the number of stored records or abort a procedure.
To test the demo you can download and install a BLE Scanner application to your smartphone that supports BLE. Whit this app you should be able to discover the services in the sensor and interact with each characteristic. Depending on the app that you installed, it will parse known characteristics, but because the PLX profile is relatively new, these characteristics will not be parsed and the values will be displayed in a raw format. In Figure 1, the USB-KW40Z was used with the sniffer application to analyze the data exchange between the PLX sensor and the client. You can see how the sensor sends the measurements, and how the client interacts with the RACP characteristic.
Figure 1. Sniffer log from USB-KW40Z