The QN9090 is a Bluetooth Low Energy device that achieves ultra-low-power consumption and integrates an Arm ® Cortex ® -M4 CPU with a comprehensive mix of analog and digital peripherals.
If the developer is working with the Development platform for QN9090 wireless MCUs for the first time, it is recommended to follow the QN9090-DK Development kit Getting Started (this guide can be found in QN9090DK Documentation section). This Quick Start document provides an overview about the QN9090 DK and its software tools and lists the steps to install the hardware and the software.
For this document, Temperature Sensor and Temperature Collector examples will be used to demonstrate the implementation of a custom profile (both examples can be found in the SDK). This article will explain how to add the Humidity Profile and how to modify the code to get the Humidity Sensor and Collector working.
Introduction
Generic Attribute Profile (GATT)
GATT defines the way that profile and user data are exchanged between devices over a BLE connection. GATT deals with actual data transfer procedures and formats. All standard BLE profiles are based on GATT and must comply with it to operate correctly, making it a key section of the BLE specification since every single item of data relevant to applications and users must be formatted, packed and sent according to the rules.
There are two GATT roles that define the devices exchanging data:
GATT Server
This device contains a GATT Database and stores the data transported over the Attribute Protocol (ATT). The Server accepts ATT requests, commands and confirmations sent by the Client; and it can be configured to send data on its own through notifications and indications.
GATT Client
This is the “active” device that accesses data on the remote GATT Server via read, write, notify, or indicate operations. Notify and indicate operations are enabled by the client but initiated by the server, providing a way to push data to the client. Notifications are unacknowledged, while indications are acknowledged. Notifications are therefore faster, but less reliable.
GATT Database establishes a hierarchy to organize attributes and is a collection of Services and Characteristics exposing meaningful data. Profiles are high level definitions that define how Services can be used to enable an application; Services are collections of Characteristics. Descriptors define attributes that describe a characteristic value.
Server (Sensor)
The Temperature Sensor project will be used as base to create our Humidity Custom Profile Server (Sensor).
BLE SIG profiles
Some Profiles or Services are already defined in the specification, and we can verify this in the Bluetooth SIG profiles document. Also, we need to check in the ble_sig_defines.h files (${workspace_loc:/${ProjName}/bluetooth/host/interface}) if this is already declared in the code.
In this case, the Service is not declared, but the Characteristic of the humidity is declared in the specification. Then, we need to check if the Characteristic is already included in ble_sig_defines.h. Since the characteristic is not included, we define it as shown:
/*! Humidity Characteristic UUID */ #define gBleSig_Humidity_d 0x2A6FU
GATT Database
The Humidity Sensor will act as GATT Server since it will be the device containing all the information for the GATT Client.
Temperature Sensor demo already implements the Battery Service and Device Information, so we only have to change the Temperature Service to Humidity Service.
In order to create the demo, we need to define a Service that must be the same as in the GATT Client, this is declared in the gatt_uuid128.h. If the new service is not the same, Client and Server will not be able to communicate each other. All macros, functions or structures in the SDK have a common template which helps the application to act accordingly. Hence, we need to define this service in the gatt_uui128.h as shown next:
/* Humidity */ UUID128(uuid_service_humidity, 0xfe, 0x34, 0x9b, 0x5f, 0x80, 0x00, 0x00, 0x80, 0x00, 0x10, 0x00, 0x02, 0x00, 0xfa, 0x10, 0x10)
Units
All the Services and Characteristics are declared in gatt_db.h. Descriptor are declared after the Characteristic Value declaration, but before the next Characteristic declaration. In this case the permission is the CharPresFormatDescriptor that have specific description by the standard.
The Units for Humidity Characteristic is Percentage, defined in the Bluetooth SIG profiles document as 0x27AD.
Descriptor
Client Characteristic Configuration Descriptor (CCCD) is a descriptor where Clients write some of the bits to activate Server notifications and/or indications.
PRIMARY_SERVICE_UUID128(service_humidity, uuid_service_humidity) CHARACTERISTIC(char_humidity, gBleSig_Humidity_d, (gGattCharPropNotify_c)) VALUE(value_humidity, gBleSig_Humidity_d, (gPermissionNone_c), 2, 0x00, 0x25) DESCRIPTOR(desc_humidity, gBleSig_CharPresFormatDescriptor_d, (gPermissionFlagReadable_c), 7, 0x0E, 0x00, 0xAD, 0x27, 0x00, 0x00, 0x00) CCCD(cccd_humidity)
Humidity service and interface
Create a folder named “humidity” in path ${workspace_loc:/${ProjName}/bluetooth/profiles}. In the same path you can find the “temperature” folder; copy the temperature_service.c file and paste it inside the “humidity” folder with another name (humidity_service.c). After this, go back to the “temperature” folder and copy the temperature_interface.h file; paste it inside the “humidity” folder and rename it (humidity_interface.h).
You will need to include the path of the created folder. Go to Project properties > C/C++ Build > Settings > Tool Settings > MCU C Compiler > Includes:
Humidity Interface
The humidity_interface.h file should include the following code, where the Service structure contains the Service handle and the initialization value:
/*! Humidity Service - Configuration */ typedef struct humsConfig_tag { uint16_t serviceHandle; int16_t initialHumidity; } humsConfig_t; /*! Humidity Client - Configuration */ typedef struct humcConfig_tag { uint16_t hService; uint16_t hHumidity; uint16_t hHumCccd; uint16_t hHumDesc; gattDbCharPresFormat_t humFormat; } humcConfig_t;
Humidity service
At minimum, humidity_service.c file must contain the following code:
/*! Humidity Service - Subscribed Client*/ static deviceId_t mHums_SubscribedClientId;
The Service stores the device identification for the connected client. This value is changed on subscription and non-subscription events.
Initialization
Initialization of the Service is made by calling the start procedure. This function is usually called when the application is initialized. In this case, this is done in the BleApp_Config() function.
bleResult_t Hums_Start(humsConfig_t *pServiceConfig) { mHums_SubscribedClientId = gInvalidDeviceId_c; /* Set the initial value of the humidity characteristic */ return Hums_RecordHumidityMeasurement(pServiceConfig->serviceHandle, pServiceConfig->initialHumidity); }
Stop & Unsubscribe
On stop function, the unsubscribe function is called.
bleResult_t Hums_Stop(humsConfig_t *pServiceConfig) { /* Stop functionality by unsubscribing */ return Hums_Unsubscribe(); } bleResult_t Hums_Unsubscribe(void) { /* Unsubscribe by invalidating the client ID */ mHums_SubscribedClientId = gInvalidDeviceId_c; return gBleSuccess_c; }
Subscribe
The subscribe function will be used in the main file to subscribe the GATT client to the Humidity Service.
bleResult_t Hums_Subscribe(deviceId_t clientDeviceId) { /* Subscribe by saving the client ID */ mHums_SubscribedClientId = clientDeviceId; return gBleSuccess_c; }
Record Humidity
Depending on the complexity of the Service, the API will implement additional functions. For the Humidity Sensor will only have one Characteristic.
The measurement will be saved on the GATT Database and send the notification to the Client. This function will need the Service handle and the new value as input parameters.
bleResult_t Hums_RecordHumidityMeasurement(uint16_t serviceHandle, int16_t humidity) { uint16_t handle; bleResult_t result; bleUuid_t uuid = Uuid16(gBleSig_Humidity_d); /* Get handle of Humidity characteristic */ result = GattDb_FindCharValueHandleInService(serviceHandle, gBleUuidType16_c, &uuid, &handle); if (result == gBleSuccess_c) { /* Update characteristic value */ result = GattDb_WriteAttribute(handle, sizeof(uint16_t), (uint8_t *)&humidity); if (result == gBleSuccess_c) { /* Notify the humidity value */ Hts_SendHumidityMeasurementNotification(handle); } } return result; }
Remember to add/update the prototype for Initialization, Subscribe, Unsubscribe, Stop and Record Humidity Measurement functions in humidity_interface.h.
Send notification
After saving the measurement on the GATT Database by using the GattDb_WriteAttribute function, we can send the notification. To send this notification, first we have to get the CCCD and check if the notification is active after that; if it is active, then we send the notification.
static void Hts_SendHumidityMeasurementNotification ( uint16_t handle ) { uint16_t hCccd; bool_t isNotificationActive; /* Get handle of CCCD */ if (GattDb_FindCccdHandleForCharValueHandle(handle, &hCccd) != gBleSuccess_c) return; if (gBleSuccess_c == Gap_CheckNotificationStatus (mHums_SubscribedClientId, hCccd, &isNotificationActive) && TRUE == isNotificationActive) { GattServer_SendNotification(mHums_SubscribedClientId, handle); } }
Remember to add or modify the prototype for Send Humidity Measurement Notification function.
Main file
There are some modifications that need to be done in the Sensor main file:
Add humidity_interface.h in main file
/* Profile / Services */ #include "humidity_interface.h"
Declare humidity service
There are some modifications that have to be done in order to use the new Humidity Profile in the Sensor example. First, we need to declare the Humidity Service:
static humsConfig_t humsServiceConfig = {(uint16_t)service_humidity, 0};
Rename BleApp_SendTemperature -> BleApp_SendHumidity
static void BleApp_SendHumidity(void);
After this, we need to add or modify the following functions and events:
Modify BleApp_Start
/* Device is connected, send humidity value */ BleApp_SendHumidity();
Ble_AppConfig
Start Humidity Service and modify the Serial_Print line.
/* Start services */ humsServiceConfig.initialHumidity = 0; (void)Hums_Start(&humsServiceConfig);
(void)Serial_Print(gAppSerMgrIf, "\n\rHumidity sensor -> Press switch to start advertising.\n\r", gAllowToBlock_d);
BleApp_ConnectionCallback events
- Event: gConnEvtConnected_c
(void)Hums_Subscribe(peerDeviceId);
- Event: gConnEvtDisconnected_c
(void)Hums_Unsubscribe();
Notify value in BleApp_GattServerCallback function
/* Notify the humidity value when CCCD is written */ BleApp_SendHumidity();
Add the Hums_RecordHumidityMeasurement function and modify the initial value update in BleApp_SendHumidity function
/* Update with initial humidity */ (void)Hums_RecordHumidityMeasurement((uint16_t)service_humidity, (int16_t)(BOARD_GetTemperature()));
Note: in this example, the Record Humidity uses the BOARD_GetTemperature to allow the developer to use the example without any external sensor and to be able to see a change in the collector, but in this section, there should be a GetHumidity function.
app_config.c file
There are some modifications that need to be done inside app_config.c file:
Modify Scanning and Advertising Data
{ .length = NumberOfElements(uuid_service_humidity) + 1, .adType = gAdComplete128bitServiceList_c, .aData = (uint8_t *)uuid_service_humidity }
*Optional* Modify name
{ .adType = gAdShortenedLocalName_c, .length = 9, .aData = (uint8_t*)"NXP_HUM" }
Modify Service Security Requirements
{ .requirements = { .securityModeLevel = gSecurityMode_1_Level_3_c, .authorization = FALSE, .minimumEncryptionKeySize = gDefaultEncryptionKeySize_d }, .serviceHandle = (uint16_t)service_humidity }
Client (Collector)
We will use the Temperature Collector project as base to create our Humidity Custom Profile Client (Collector).
BLE SIG profiles
As mentioned in the Server section, we need to verify if the Profile or Service is already defined in the specification. For this, we can take a look at the Bluetooth SIG profiles document and check in the ble_sig_defines.h file (${workspace_loc:/${ProjName}/bluetooth/host/interface}) if this is already declared in the code. In our case, the Service is not declared, but the Characteristic of the Humidity is declared in the specification. Then, we need to check if the Characteristic is already included in ble_sig_defines.h. Since the Characteristic is not included, we need to define it as shown:
/*! Humidity Characteristic UUID */ #define gBleSig_Humidity_d 0x2A6FU
GATT Database
The Humidity Collector is going to have the GATT client; this is the device that will receive all new information from the GATT Server.
The demo provided in this article works in the same way as the Temperature Collector. When the Collector enables the notifications from the Sensor, received notifications will be printed in the seral terminal.
In order to create the demo, we need to define or develop a Service that must be the same as in the GATT Server, this is declared in the gatt_uuid128.h file. If the new Service is no the same, Client and Server will not be able to communicate each other. All macros, functions or structures in the SDK have a common template which helps the application to act accordingly. Hence, we need to define this service in the gatt_uui128.h as shown next:
/* Humidity */ UUID128(uuid_service_humidity, 0xfe, 0x34, 0x9b, 0x5f, 0x80, 0x00, 0x00, 0x80, 0x00, 0x10, 0x00, 0x02, 0x00, 0xfa, 0x10, 0x10)
Includes
After that, copy the humidity profile folder from the Sensor project and paste it into the Collector project inside ${workspace_loc:/${ProjName}/bluetooth/profiles}. Also, include the path of the new folder.
Main file
In the Collector main file, we need to do some modifications to use the Humidity Profile
Include humidity_interface.h
/* Profile / Services */ #include "humidity_interface.h"
Modify the Custom Info of the Peer device
humcConfig_t humsClientConfig;
Modify BleApp_StoreServiceHandles function
static void BleApp_StoreServiceHandles { APP_DBG_LOG(""); uint8_t i,j; if ((pService->uuidType == gBleUuidType128_c) && FLib_MemCmp(pService->uuid.uuid128, uuid_service_humidity, 16)) { /* Found Humidity Service */ mPeerInformation.customInfo.humsClientConfig.hService = pService->startHandle; for (i = 0; i < pService->cNumCharacteristics; i++) { if ((pService->aCharacteristics[i].value.uuidType == gBleUuidType16_c) && (pService->aCharacteristics[i].value.uuid.uuid16 == gBleSig_Humidity_d)) { /* Found Humidity Char */ mPeerInformation.customInfo.humsClientConfig.hHumidity = pService->aCharacteristics[i].value.handle; for (j = 0; j < pService->aCharacteristics[i].cNumDescriptors; j++) { if (pService->aCharacteristics[i].aDescriptors[j].uuidType == gBleUuidType16_c) { switch (pService->aCharacteristics[i].aDescriptors[j].uuid.uuid16) { /* Found Humidity Char Presentation Format Descriptor */ case gBleSig_CharPresFormatDescriptor_d: { mPeerInformation.customInfo.humsClientConfig.hHumDesc = pService->aCharacteristics[i].aDescriptors[j].handle; break; } /* Found Humidity Char CCCD */ case gBleSig_CCCD_d: { mPeerInformation.customInfo.humsClientConfig.hHumCccd = pService->aCharacteristics[i].aDescriptors[j].handle; break; } default: ; /* No action required */ break; } } } } } } }
Modify BleApp_StoreDescValues function
if (pDesc->handle == mPeerInformation.customInfo.humsClientConfig.hHumDesc) { /* Store Humidity format*/ FLib_MemCpy(&mPeerInformation.customInfo.humsClientConfig.humFormat, pDesc->paValue, pDesc->valueLength); }
Implement BleApp_PrintHumidity function
static void BleApp_PrintHumidity ( uint16_t humidity ) { APP_DBG_LOG(""); shell_write("Humidity: "); shell_writeDec((uint32_t)humidity); /* Add '%' for Percentage - UUID 0x27AD. www.bluetooth.com/specifications/assigned-numbers/units */ if (mPeerInformation.customInfo.humsClientConfig.humFormat.unitUuid16 == 0x27ADU) { shell_write(" %\r\n"); } else { shell_write("\r\n"); } }
Modify BleApp_GattNotificationCallback function
if (characteristicValueHandle == mPeerInformation.customInfo.humsClientConfig.hHumidity) { BleApp_PrintHumidity(Utils_ExtractTwoByteValue(aValue)); }
Modify CheckScanEvent function
foundMatch = MatchDataInAdvElementList(&adElement, &uuid_service_humidity, 16);
Some events inside BleApp_StateMachineHandler need to be modified:
BleApp_StateMachineHandler
- Event: mAppIdle_c
if (mPeerInformation.customInfo.humsClientConfig.hHumidity != gGattDbInvalidHandle_d)
- Event: mAppServiceDisc_c
if (mPeerInformation.customInfo.humsClientConfig.hHumDesc != 0U)
mpCharProcBuffer->handle = mPeerInformation.customInfo.humsClientConfig.hHumDesc;
- Event: mAppReadDescriptor_c
if (mPeerInformation.customInfo.humsClientConfig.hHumCccd != 0U)
Modify BleApp_ConfigureNotifications function
mpCharProcBuffer->handle = mPeerInformation.customInfo.humsClientConfig.hHumCccd;
Demonstration
In order to print the relevant data in console, it may be necessary to disable Power Down mode in app_preinclude.h file. This file can be found inside source folder. For this, cPWR_UsePowerDownMode and cPWR_FullPowerDownMode should be set to 0.
Now, after connection, every time that you press the User Interface Button on QN9090 Humidity Sensor is going to send the value to QN9090 Humidity Collector.
Humidity Sensor
Humidity Collector
View full article