Bluetooth Low Energy, through the Generic Attribute Profile (GATT), supports various ways to send and receive data between clients and servers. Data can be transmitted through indications, notifications, write requests and read requests. Data can also be transmitted through the Generic Access Profile (GAP) by using broadcasts. Here however, I'll focus on write and read requests.
Write and read requests are made by a client to a server, to ask for data (read request) or to send data (write request). In these cases, the client first makes the request, and the server then responds, by either acknowledging the write request (and thus, writing the data) or by sending back the value requested by the client.
To be able to make write and read requests, we must first understand how BLE handles the data it transmits. To transmit data back and forth between devices, BLE uses the GATT protocol. The GATT protocol handles data using a GATT database. A GATT database implements profiles, and each profile is made from a collection of services. These services each contain one or more characteristics. A BLE characteristic is made of attributes. These attributes constitute the data itself, and the handle to reference, access or modify said data.
To have a characteristic that is able to be both written and read, it must be first created. This is done precisely in the GATT database file ( gatt_db.h
/* gatt_db.h */
/* Custom service*/
PRIMARY_SERVICE_UUID128(service_custom, uuid_custom_service)
/* Custom characteristic with read and write properties */
CHARACTERISTIC_UUID128(char_custom, uuid_custom_char, (gGattCharPropRead_c | gGattCharPropWrite_c))
/* Custom length attribute with read and write permissions*/
VALUE_UUID128_VARLEN(value_custom, uuid_custom_char, (gPermissionFlagReadable_c | gPermissionFlagWritable_c), 50, 1, 0x00)
The custom UUIDs are defined in the gatt_uuid128.h file:
/* gatt_uuid128.h */
/* Custom 128 bit UUIDs*/
UUID128(uuid_custom_service, 0xE0, 0x1C, 0x4B, 0x5E, 0x1E, 0xEB, 0xA1, 0x5C, 0xEE, 0xF4, 0x5E, 0xBA, 0x00, 0x01, 0xFF, 0x01)
UUID128(uuid_custom_char, 0xA1, 0xB2, 0xC3, 0xD4, 0xE5, 0xF6, 0x17, 0x28, 0x39, 0x4A, 0x5B, 0x6C, 0x7D, 0x8E, 0x9F, 0x00)
With this custom characteristic, we can write and read a value of up to 50 bytes (as defined by the variable length value declared in the gatt_db.h file, see code above). Remember that you also need to implement the interface and functions for the service. For further information and guidance in how to make a custom profile, please refer to the BLE application developer's guide (BLEDAG.pdf, located in <KW40Z_connSw_install_dir>\ConnSw\doc\BLEADG.pdf.
Once a connection has been made, and you've got two (or more) devices connected, read and write requests can be made. I'll first cover how to make a write and read request from the client side, then from the server side.
To make a write request to a server, you'll need to have the handle for the characteristic you want to modify. This handle should be stored once the characteristic discovery is done. Obviously, you also need the data that is going to be written.
The following function needs a pointer to the data and the size of the data. It also uses the handle to tell the server what characteristic is going to be written:
static void SendWriteReq(uint8_t* data, uint8_t dataSize)
{
gattCharacteristic_t characteristic;
characteristic.value.handle = charHandle; // Previously stored characteristic handle
GattClient_WriteCharacteristicValue( mPeerInformation.deviceId, &characteristic,
dataSize, data, FALSE,
FALSE, FALSE, NULL);
}
uint8_t wdata[15] = {"Hello world!\r"};
uint8_t size = sizeof(wdata);
SendWriteReq(wdata, size);
The data is send with the GattClient_WriteCharacteristicValue() API. This function has various configurable parameters to establish how to send the data. The function's parameters are described with detail on the application developer's guide, but basically, you can determine whether you need or not a response for the server, whether the data is signed or not, etc.
Whenever a client makes a read or write request to the server, there is a callback procedure triggered, to which the program then goes. This callback function has to be registered though. You can register the client callback function using the App_RegisterGattClientProcedureCallback() API:
App_RegisterGattClientProcedureCallback(gattClientProcedureCallback);
void gattClientProcedureCallback ( deviceId_t deviceId,
gattProcedureType_t procedureType,
gattProcedureResult_t procedureResult,
bleResult_t error )
{
switch (procedureType)
{
/* ... */
case gGattProcWriteCharacteristicValue_c:
if (gGattProcSuccess_c == procedureResult)
{
/* Continue */
}
else
{
/* Handle error */
}
break;
/* ... */
}
}
Reading an attribute is somewhat similar to writing an attribute, you still need the handle for the characteristic, and a buffer in which to store the read value:
#define size 17
static void SendReadReq(uint8_t* data, uint8_t dataSize)
{
/* Memory has to be allocated for the characteristic because the
GattClient_ReadCharacteristicValue() API runs in a different task, so
it has a different stack. If memory were not allocated, the pointer to
the characteristic would point to junk. */
characteristic = MEM_BufferAlloc(sizeof(gattCharacteristic_t));
data = MEM_BufferAlloc(dataSize);
characteristic->value.handle = charHandle;
characteristic->value.paValue = data;
bleResult_t result = GattClient_ReadCharacteristicValue(mPeerInformation.deviceId, characteristic, dataSize);
}
uint8_t rdata[size];
SendReadReq(rdata, size);
As mentioned before, a callback procedure is triggered whenever there is a write or read request. This is the same client callback procedure used for the write request, but the event generates a different procedure type:
void gattClientProcedureCallback ( deviceId_t deviceId,
gattProcedureType_t procedureType,
gattProcedureResult_t procedureResult,
bleResult_t error )
{
switch (procedureType)
{
/* ... */
case gGattProcReadCharacteristicValue_c:
if (gGattProcSuccess_c == procedureResult)
{
/* Read value length */
PRINT(characteristic.value.valueLength);
/* Read data */
for (uint16_t j = 0; j < characteristic.value.valueLength; j++)
{
PRINT(characteristic.value.paValue[j]);
}
}
else
{
/* Handle error */
}
break;
/* ... */
}
}
There are some other methods to read an attribute. For further information, refer to the application developer's guide chapter 5, section 5.1.4 Reading and Writing Characteristics.
Naturally, every time there is a request to either read or write by a client, there must be a response from the server. Similar to the callback procedure from the client, with the server there is also a callback procedure triggered when the client makes a request. This callback function will handle both the write and read requests, but the procedure type changes. This function should also be registered using the App_RegisterGattServerCallback() API.
When there is a read request from a client, the server responds with the read status:
App_RegisterGattServerCallback( gattServerProcedureCallback );
void gattServerProcedureCallback ( deviceId_t deviceId,
gattServerEvent_t* pServerEvent )
{
switch (pServerEvent->eventType)
{
/* ... */
case gEvtAttributeRead_c:
GattServer_SendAttributeReadStatus(deviceId, value_custom, gAttErrCodeNoError_c);
break;
/* ... */
}
}
When there is a write request however, the server should write the received data in the corresponding attribute in the GATT database. To do this, the function GattDb_WriteAttribute() can be used:
void gattServerProcedureCallback ( deviceId_t deviceId,
gattServerEvent_t* pServerEvent )
{
switch (pServerEvent->eventType)
{
/* ... */
case gEvtAttributeWritten_c:
if (pServerEvent->eventData.attributeWrittenEvent.handle == value_custom)
{
GattDb_WriteAttribute( pServerEvent->eventData.attributeWrittenEvent.handle,
pServerEvent->eventData.attributeWrittenEvent.cValueLength,
pServerEvent->eventData.attributeWrittenEvent.aValue );
GattServer_SendAttributeWrittenStatus(deviceId, value_custom, gAttErrCodeNoError_c);
}
break;
/* ... */
}
}
If you do not register the server callback function, the attribute can still be written in the GATT database (it is actually done automatically), however, if you want something else to happen when you receive a request (turning on a LED, for example), you will need the server callback procedure.
I have modified the temperature_sensor (SERVER) and temperature_collector(CLIENT) to implement a custom counter profile for the purposes of understanding coding requirements for future work. The modifications work and I can execute notifications from the server to client.
To execute the SendReadReq and SendWriteReq functions above the question is how to execute these functions that is from which task or event. The task that executes the finding of services, callbacks etc. is the startup_task which appears just to be waiting for events and is a wrapper for the main_task function. It appears that creating a FreeRTOS task is not the way to do it because the tasking is done via the framework OSAbstraction code. Part of the answer is to do it from a task that executes every 1000ms or a key event. If I was just dealing with FreeRTOS, the answer would be simple. Perhaps it may be the App_Thread function in the main_task will have to be utilized. Could you or anyone suggest the best approach, with some detail?
I’ll answer my question. It was easier to go about testing like this. Add your custom service in the server first. Then test the server by using a mobile app such as Nordic Semiconductor’s RF-Connect or NXP’s equivalent.
By enabling the count characteristic for notifications, read and write, one can test each of these with the permissions set to readable. With notifications enabled, I could then test the SendReadReq and SendWriteReq functions on the CLIENT within BleApp_GattNotificationCallback function. Things compile nicer this way. See below.
static void BleApp_GattNotificationCallback(deviceId_t serverDeviceId,
uint16_t characteristicValueHandle, uint8_t* aValue,
uint16_t valueLength) {
if (characteristicValueHandle
== mPeerInformation.customInfo.cntClientConfig.cCounter) {
BleApp_PrintCounter(*(uint32_t*) aValue);
//SendWriteReq(wdata, wsize); //255###
SendReadReq(rdata, rsize); // . . . . . . . . .code continues
For printing to a terminal window, I used functions like shell_write() and shell_writeDec and printed the notification value first, then the value read.
I would note that when a characteristic value UUID is 128-bit, then the FindCharValueHandleInService() function is as below:
bleResult_t Cnts_RecordCounterMeasurement (uint16_t serviceHandle, uint32_t counterValue)
{
uint16_t handle;
bleResult_t result;
/* Get handle of characteristic */
result = GattDb_FindCharValueHandleInService(serviceHandle,
gBleUuidType128_c, (bleUuid_t*)&uuid_char_counter_d, &handle);
if (result != gBleSuccess_c)
return result;
/* Update characteristic value and send notification */
result = GattDb_WriteAttribute(handle, sizeof(uint32_t), (uint8_t*)&counterValue);
if (result != gBleSuccess_c)
return result;
Cnts_SendNotifications(handle);
Cnts_SendIndications(handle);
return gBleSuccess_c;
}
The SendWriteReg() function changes.
Within BleApp_Config() function, I added:
characteristic = MEM_BufferAlloc(sizeof(gattCharacteristic_t)); //Allocate memory once
Within Count_sensor.c add:
void SendReadReq(uint8_t* data, uint8_t dataSize)
{
/*Memory has to be allocated for the characteristic because the
GattClient_ReadCharacteristicValue() API runs in a different task, so
it has a different stack. If memory were not allocated, the pointer to
the characteristic would point to junk.*/
extern gattCharacteristic_t* characteristic;
characteristic->value.handle = mPeerInformation.customInfo.cntClientConfig.cCounter; //charHandle;
characteristic->value.paValue = data;
bleResult_t result = GattClient_ReadCharacteristicValue(mPeerInformation.deviceId, characteristic, dataSize);
}