Bluetooth Low Energy (a.k.a Bluetooth LE, BTLE, Bluetooth Smart)

template for an actuator service

18 Feb 2015

With mbed/BLE, we offer a growing set of SIG defined BLE services implemented as C++ headers to ease application development. These can be found under https://github.com/mbedmicro/BLE_API/tree/master/services.

We expect our users to be developing applications for custom sensors and actuators, often outside the scope of the standard Bluetooth services, or the service templates offered by mbed-BLE. For custom applications, users will need to use the BLE_API to setup services (and characteristics) for the Bluetooth stack, or even model their services as C++ classes for ease of use (and reuse).

In a recent post, we went over the process of setting up a custom BLE service encapsulating a read-only characteristic. In this document, we'd like to capture the creation of a service with a write-only characteristic. Taken together, these would then form the basis for most BLE services.

Let's work our way towards creating a service for a trivial actuator: an LED. We'll assume a use-case where a phone-app would like to connect to this mbed application and set the LED state. In the non-connected state, the application simply advertises its capability of providing an LED service.

Here's some template code to get started with the very basics. We've thrown in a blinking LED to indicate program aliveness. We'll soon introduce a second LED to represent the actuator.

#include "mbed.h"
#include "BLEDevice.h"

BLEDevice   ble;
DigitalOut  alivenessLED(LED1);

const static char DEVICE_NAME[] = "LED";

void disconnectionCallback(Gap::Handle_t handle, Gap::DisconnectionReason_t reason)
{
    ble.startAdvertising();
}

void periodicCallback(void)
{
    alivenessLED = !alivenessLED; /* Do blinky on LED1 to indicate system aliveness. */
}

int main(void)
{
    alivenessLED = 0;
    Ticker ticker;
    ticker.attach(periodicCallback, 1);

    ble.init();
    ble.onDisconnection(disconnectionCallback);

    /* setup advertising */
    ble.accumulateAdvertisingPayload(GapAdvertisingData::BREDR_NOT_SUPPORTED | GapAdvertisingData::LE_GENERAL_DISCOVERABLE);
    ble.accumulateAdvertisingPayload(GapAdvertisingData::COMPLETE_LOCAL_NAME, (uint8_t *)DEVICE_NAME, sizeof(DEVICE_NAME));
    ble.setAdvertisingType(GapAdvertisingParams::ADV_CONNECTABLE_UNDIRECTED);
    ble.setAdvertisingInterval(Gap::MSEC_TO_ADVERTISEMENT_DURATION_UNITS(1000)); /* 1000ms. */
    ble.startAdvertising();

    while (true) {
        ble.waitForEvent();
    }
}

The above code doesn't create any custom service. It advertises "LED" as the device-name through the advertisement payload. The application is discoverable (LE_GENERAL_DISCOVERABLE) and connectible (ADV_CONNECTABLE_UNDIRECTED), and offers only the standard GAP and GATT services. 'disconnectionCallback' re-starts advertisements following a disconnection. This trivial template just gets you off the ground.

<<add snapshots of how the services/application appears when scanned from a phone>>

Now, let's get down to the business of creating a BLE Service for an LED. This service will have a single write-only Characteristic holding a boolean for LED state.

Bluetooth Smart requires the use of UUIDs to identify 'types' for all involved entities. We'll need UUIDs for the LED service and the encapsulated characteristic. If we had been creating one of the standard SIG defined services, we'd have followed the standard UUID definitions https://developer.bluetooth.org/gatt/services/Pages/ServicesHome.aspx.

We've chosen a custom UUID space for our LED service: 0xA000 for the service, and 0xA001 for the contained characteristic. This avoids collision with the standard UUIDs.

#define LED_SERVICE_UUID              0xA000
#define LED_STATE_CHARACTERISTIC_UUID 0xA001

...

static const uint16_t uuid16_list[] = {LED_SERVICE_UUID};

...

    ble.accumulateAdvertisingPayload(GapAdvertisingData::COMPLETE_LIST_16BIT_SERVICE_IDS, (uint8_t *)uuid16_list, sizeof(uuid16_list));

The part above where we advertise the LEDService UUID in the advertising payload is purely optional. Having it is good practice, since it gives an early and cheap indication to interested client apps regarding the capabilities of the mbed application. Caveat: interpreting non-standard service UUIDs has limited use, and may only work with custom phone apps. While developing your demo, you could still use generic BLE apps capable of service- discovery and simple read/write transactions on characteristics; one example of this would be Nordic's Master Control Panel.

The LED-state characteristic can be defined with the following snippet from some context where memory allocations remain persistent for the lifetime of the application. This could be from within the main() function, for example. The code only looks complicated, but it is essentially a simple use of C++ templates to instantiate a write-only characteristic encapsulating boolean state. The constructor for LEDState takes in the UUID and a pointer to the initial value of the characteristic.

    bool initialValueForLEDCharacteristic = false;
    WriteOnlyGattCharacteristic<bool> ledState(LED_STATE_CHARACTERISTIC_UUID, &initialValueForLEDCharacteristic);

There are several variants of GattCharacterisitc available to ease instantiation. Other common types are: ReadOnlyGattCharacterisitc<T> and ReadWriteGattCharacteristic<T>. Refer to template declarations at the bottom of https://github.com/mbedmicro/BLE_API/blob/master/public/GattCharacteristic.h.

We could just as easily have used a ReadWriteGattCharacterisitc<T> for the ledState, to make it readable as well. This will allow a phone app to connect and probe the ledState.

    bool initialValueForLEDCharacteristic = false;
    ReadWriteGattCharacteristic<bool> ledState(LED_STATE_CHARACTERISTIC_UUID, &initialValueForLEDCharacteristic);

The ledState characteristic can be used to construct a GattService, which we'll call 'ledService'. This is done through a little bit of C/C++ syntax to create a one-element array using an initializer list of pointers to GattCharacteristics. This service can be added to the BLE stack using BLEDevice::addService().

    GattCharacteristic *charTable[] = {&ledState};
    GattService         ledService(LED_SERVICE_UUID, charTable, sizeof(charTable) / sizeof(GattCharacteristic *));
    ble.addService(ledService);

So, now we have the following code which defines a custom led service containing a readable and writable characteristic.

#include "mbed.h"
#include "BLEDevice.h"

BLEDevice   ble;
DigitalOut  alivenessLED(LED1);

#define LED_SERVICE_UUID              0xA000
#define LED_STATE_CHARACTERISTIC_UUID 0xA001

const static char     DEVICE_NAME[] = "LED";
static const uint16_t uuid16_list[] = {LED_SERVICE_UUID};

void disconnectionCallback(Gap::Handle_t handle, Gap::DisconnectionReason_t reason)
{
    ble.startAdvertising();
}

void periodicCallback(void)
{
    alivenessLED = !alivenessLED; /* Do blinky on LED1 to indicate system aliveness. */
}

int main(void)
{
    alivenessLED = 0;
    Ticker ticker;
    ticker.attach(periodicCallback, 1);

    ble.init();
    ble.onDisconnection(disconnectionCallback);

    bool initialValueForLEDCharacteristic = false;
    ReadWriteGattCharacteristic<bool> ledState(LED_STATE_CHARACTERISTIC_UUID, &initialValueForLEDCharacteristic);

    GattCharacteristic *charTable[] = {&ledState};
    GattService         ledService(LED_SERVICE_UUID, charTable, sizeof(charTable) / sizeof(GattCharacteristic *));
    ble.addService(ledService);

    /* setup advertising */
    ble.accumulateAdvertisingPayload(GapAdvertisingData::BREDR_NOT_SUPPORTED | GapAdvertisingData::LE_GENERAL_DISCOVERABLE);
    ble.accumulateAdvertisingPayload(GapAdvertisingData::COMPLETE_LIST_16BIT_SERVICE_IDS, (uint8_t *)uuid16_list, sizeof(uuid16_list));
    ble.accumulateAdvertisingPayload(GapAdvertisingData::COMPLETE_LOCAL_NAME, (uint8_t *)DEVICE_NAME, sizeof(DEVICE_NAME));
    ble.setAdvertisingType(GapAdvertisingParams::ADV_CONNECTABLE_UNDIRECTED);
    ble.setAdvertisingInterval(Gap::MSEC_TO_ADVERTISEMENT_DURATION_UNITS(1000)); /* 1000ms. */
    ble.startAdvertising();

    while (true) {
        ble.waitForEvent();
    }
}

<<based on this state of the code, add some snapshots on how the service/characteristic appears from a generic phone app capable of service discovery and simple transactions on the characteristic>>

Thus far, the ledState characteristic within the service has no binding to a physical LED. The user may be able to write to this characteristic from a phone app, but it would not actuate anything in the application.

We can introduce a real LED with the following:

DigitalOut  actuatedLed(LED2);

We can now add some code to respond to client-writes to the ledState characteristic and update the physical led. BLE_API provides a onDataWritten callback, which can be set to an application specific handler.

The following bits of code setup callbacks for when the phone app attempts to write to the ledState characteristic of the LEDService:

DigitalOut  actuatedLed(LED2);

...

/**
 * This callback allows the LEDService to receive updates to the ledState Characteristic.
 *
 * @param[in] params
 *     Information about the characterisitc being updated.
 */
void onDataWrittenCallback(const GattCharacteristicWriteCBParams *params) {
    if (params->charHandle == ledState.getValueHandle()) {
        /* Do something here to the actuated LED based on the received params. */
    }
}

int main(void)
{
...
    ble.onDataWritten(onDataWrittenCallback);

Note that within the onDataWritten callback, we can identify the characteristic being updated using a 'value handle'. The ledState characteristic needs to be moved into a global context in order for the onDataWritten callback to access it. Here's the full code.

#include "mbed.h"
#include "BLEDevice.h"

BLEDevice   ble;
DigitalOut  alivenessLED(LED1);
DigitalOut  actuatedLED(LED2);

#define LED_SERVICE_UUID              0xA000
#define LED_STATE_CHARACTERISTIC_UUID 0xA001

const static char     DEVICE_NAME[] = "LED";
static const uint16_t uuid16_list[] = {LED_SERVICE_UUID};

bool initialValueForLEDCharacteristic = false;
ReadWriteGattCharacteristic<bool> ledState(LED_STATE_CHARACTERISTIC_UUID, &initialValueForLEDCharacteristic);

void disconnectionCallback(Gap::Handle_t handle, Gap::DisconnectionReason_t reason)
{
    ble.startAdvertising();
}

void periodicCallback(void)
{
    alivenessLED = !alivenessLED; /* Do blinky on LED1 to indicate system aliveness. */
}

/**
 * This callback allows the LEDService to receive updates to the ledState Characteristic.
 *
 * @param[in] params
 *     Information about the characterisitc being updated.
 */
void onDataWrittenCallback(const GattCharacteristicWriteCBParams *params) {
    if ((params->charHandle == ledState.getValueHandle()) && (params->len == 1)) {
        actuatedLED = *(params->data);
    }
}

int main(void)
{
    alivenessLED = 0;
    actuatedLED  = 0;

    Ticker ticker;
    ticker.attach(periodicCallback, 1);

    ble.init();
    ble.onDisconnection(disconnectionCallback);
    ble.onDataWritten(onDataWrittenCallback);

    GattCharacteristic *charTable[] = {&ledState};
    GattService         ledService(LED_SERVICE_UUID, charTable, sizeof(charTable) / sizeof(GattCharacteristic *));
    ble.addService(ledService);

    /* setup advertising */
    ble.accumulateAdvertisingPayload(GapAdvertisingData::BREDR_NOT_SUPPORTED | GapAdvertisingData::LE_GENERAL_DISCOVERABLE);
    ble.accumulateAdvertisingPayload(GapAdvertisingData::COMPLETE_LIST_16BIT_SERVICE_IDS, (uint8_t *)uuid16_list, sizeof(uuid16_list));
    ble.accumulateAdvertisingPayload(GapAdvertisingData::COMPLETE_LOCAL_NAME, (uint8_t *)DEVICE_NAME, sizeof(DEVICE_NAME));
    ble.setAdvertisingType(GapAdvertisingParams::ADV_CONNECTABLE_UNDIRECTED);
    ble.setAdvertisingInterval(Gap::MSEC_TO_ADVERTISEMENT_DURATION_UNITS(1000)); /* 1000ms. */
    ble.startAdvertising();

    while (true) {
        ble.waitForEvent();
    }
}

<<add snapshots showing how ledState can be polled and updated; and the corresponding effect on the actuatedLED>>

The above application is complete in functionality, but has grown to be a bit messy. In particular, most of the plumbing around creating the led service could be substituted with a simple initialization of a 'LEDService' class while retaining the functionality.

Here's something to get started with the LEDService:

#ifndef __BLE_LED_SERVICE_H__
#define __BLE_LED_SERVICE_H__

class LEDService {
public:
    const static uint16_t LED_SERVICE_UUID              = 0xA000;
    const static uint16_t LED_STATE_CHARACTERISTIC_UUID = 0xA001;

private:
    /* private members to come */
};

#endif /* #ifndef __BLE_LED_SERVICE_H__ */

Nearly all BLE APIs require a reference to the BLEDevice; so we must require this in the constructor. The ledState characteristic should be encapsulated as well.

#ifndef __BLE_LED_SERVICE_H__
#define __BLE_LED_SERVICE_H__

class LEDService {
public:
    const static uint16_t LED_SERVICE_UUID              = 0xA000;
    const static uint16_t LED_STATE_CHARACTERISTIC_UUID = 0xA001;

    LEDService(BLEDevice &_ble, bool initialValueForLEDCharacteristic) :
        ble(_ble), ledState(LED_STATE_CHARACTERISTIC_UUID, &initialValueForLEDCharacteristic)
    {
        /* empty */
    }

private:
    BLEDevice                         &ble;
    ReadWriteGattCharacteristic<bool>  ledState;
};

#endif /* #ifndef __BLE_LED_SERVICE_H__ */

We can move more of the setup of the service into the constructor.

#ifndef __BLE_LED_SERVICE_H__
#define __BLE_LED_SERVICE_H__

class LEDService {
public:
    const static uint16_t LED_SERVICE_UUID              = 0xA000;
    const static uint16_t LED_STATE_CHARACTERISTIC_UUID = 0xA001;

    LEDService(BLEDevice &_ble, bool initialValueForLEDCharacteristic) :
        ble(_ble), ledState(LED_STATE_CHARACTERISTIC_UUID, &initialValueForLEDCharacteristic)
    {
        GattCharacteristic *charTable[] = {&ledState};
        GattService         ledService(LED_SERVICE_UUID, charTable, sizeof(charTable) / sizeof(GattCharacteristic *));
        ble.addService(ledService);
    }

private:
    BLEDevice                         &ble;
    ReadWriteGattCharacteristic<bool>  ledState;
};

#endif /* #ifndef __BLE_LED_SERVICE_H__ */

And here's a small extension with a helper API to fetch the value handle for the sake of the onDataWritten callback:

#ifndef __BLE_LED_SERVICE_H__
#define __BLE_LED_SERVICE_H__

class LEDService {
public:
    const static uint16_t LED_SERVICE_UUID              = 0xA000;
    const static uint16_t LED_STATE_CHARACTERISTIC_UUID = 0xA001;

    LEDService(BLEDevice &_ble, bool initialValueForLEDCharacteristic) :
        ble(_ble), ledState(LED_STATE_CHARACTERISTIC_UUID, &initialValueForLEDCharacteristic)
    {
        GattCharacteristic *charTable[] = {&ledState};
        GattService         ledService(LED_SERVICE_UUID, charTable, sizeof(charTable) / sizeof(GattCharacteristic *));
        ble.addService(ledService);
    }

    GattAttribute::Handle_t getValueHandle() const {
        return ledState.getValueHandle();
    }

private:
    BLEDevice                         &ble;
    ReadWriteGattCharacteristic<bool>  ledState;
};

#endif /* #ifndef __BLE_LED_SERVICE_H__ */

And now with this encapsulated away in the LEDService, the main application reads slightly better.

#include "mbed.h"
#include "BLEDevice.h"
#include "LEDService.h"

BLEDevice   ble;
DigitalOut  alivenessLED(LED1);
DigitalOut  actuatedLED(LED2);

const static char     DEVICE_NAME[] = "LED";
static const uint16_t uuid16_list[] = {LEDService::LED_SERVICE_UUID};

LEDService *ledServicePtr;

void disconnectionCallback(Gap::Handle_t handle, Gap::DisconnectionReason_t reason)
{
    ble.startAdvertising();
}

void periodicCallback(void)
{
    alivenessLED = !alivenessLED; /* Do blinky on LED1 to indicate system aliveness. */
}

/**
 * This callback allows the LEDService to receive updates to the ledState Characteristic.
 *
 * @param[in] params
 *     Information about the characterisitc being updated.
 */
void onDataWrittenCallback(const GattCharacteristicWriteCBParams *params) {
    if ((params->charHandle == ledServicePtr->getValueHandle()) && (params->len == 1)) {
        actuatedLED = *(params->data);
    }
}

int main(void)
{
    alivenessLED = 0;
    actuatedLED  = 0;

    Ticker ticker;
    ticker.attach(periodicCallback, 1);

    ble.init();
    ble.onDisconnection(disconnectionCallback);
    ble.onDataWritten(onDataWrittenCallback);

    bool initialValueForLEDCharacteristic = false;
    LEDService ledService(ble, initialValueForLEDCharacteristic);
    ledServicePtr = &ledService;

    /* setup advertising */
    ble.accumulateAdvertisingPayload(GapAdvertisingData::BREDR_NOT_SUPPORTED | GapAdvertisingData::LE_GENERAL_DISCOVERABLE);
    ble.accumulateAdvertisingPayload(GapAdvertisingData::COMPLETE_LIST_16BIT_SERVICE_IDS, (uint8_t *)uuid16_list, sizeof(uuid16_list));
    ble.accumulateAdvertisingPayload(GapAdvertisingData::COMPLETE_LOCAL_NAME, (uint8_t *)DEVICE_NAME, sizeof(DEVICE_NAME));
    ble.setAdvertisingType(GapAdvertisingParams::ADV_CONNECTABLE_UNDIRECTED);
    ble.setAdvertisingInterval(Gap::MSEC_TO_ADVERTISEMENT_DURATION_UNITS(1000)); /* 1000ms. */
    ble.startAdvertising();

    while (true) {
        ble.waitForEvent();
    }
}

And now a little thing at the end. Notice that we've setup a 'ledServicePtr'. This was necessary because onDataWritten callback needs to refer to the ledService object, but the ledService object is instantiated within the context of main() because it needs to come after ble.init().

08 Mar 2015

Hi guys,

I have a question for you please, i don't understand this line in the code:

L.46: ble.onDataWritten(onDataWrittenCallback);

when the function onDataWritten call the function onDataWrittenCallback, the pointer (to struct GattCharacteristicWriteCBParams) is not initalized:

L.30: const GattCharacteristicWriteCBParams *params ??

For me it's like this:

GattCharacteristicWriteCBParams Object_Struct; params = &Objetc_Struct;

ble.onDataWritten( ??? ); Here i don't know what is the parameter of the function onDataWritten... Maybe this (?) ble.onDataWritten(*(onDataWrittenCallback(params)));

Thanks for your help, Yacire.

08 Mar 2015

I use that in my bleIO sample, have a look to see the parameters used:

http://developer.mbed.org/users/prussell/code/bleIOv04_pr/wiki/Homepage

09 Mar 2015

Thanks Paul for your help,

But i don't understand how the function onDataWritten() in the class BLEDevice works ...

She needs a pointer to a function but the function onDataWrittenCallback needs a pointer too, but i don't see where the pointer is initialized ...

I see your code in your profile but i don't understand here:

void Callback_BLE_onDataWritten(const GattCharacteristicWriteCBParams *pParams) { GattAttribute::Handle_t tHandle=pParams->charHandle; etc...

and here: ble.onDataWritten(Callback_BLE_onDataWritten);

Thanks.

09 Mar 2015
  • The ble system operations are handled in the background: In Interrupts, and while ble.waitForEvent() is active.
  • You tell the ble system which operations you want to act on by passing pointers to the ble.onxxxx calls
    • ble.onDataWritten(Callback_BLE_onDataWritten);
  • the ble system will then call your callback functions at the appropriate times, passing them the appropriate data
    • Callback_BLE_onDataWritten( data provided by ble system)
    • You never call Callback_BLE_onDataWritten()
    • best to treat these callback functions as you would any interrupt function so as not to delay in the ble protocol or other system operations (i.e. not lots of serial debug).
09 Mar 2015

Thanks for your help Paul,

I understand better now. best regards, Yacire.

23 Mar 2015

Irit has written some documentation along these lines at http://ble-intros.readthedocs.org/en/latest/InDepth/Events/ Surely she should be posting these updates herself.

17 Sep 2015

Hi, Rohit

Thank you for your article, and it's very useful for me. I have one question about it. Could you have a look?

If I want to write some value from app to mbed(HW is nRF51), how can I do?

On nRF Master Control Panel app, I write2 uint8 value(12 and 15) and send them to BLE device(nRF51), but the nRF51 can not receive any data. But If I write only 1 value on nRF Master Control Panel app then send it, the nRF51 can receive the value.

When I send 2 values from app, the onDataWrittenCallback() is not be called.

My code:

LED.h

class LEDService
{
public:
    const static uint16_t LED_SERVICE_UUID              = 0xA000;
    const static uint16_t LED_WRITE_CHARACTERISTIC_UUID = 0xA002;

    LEDService(BLEDevice &_ble, uint8_t write) :
        ble(_ble), ledWrite(LED_WRITE_CHARACTERISTIC_UUID, &write) {
        GattCharacteristic *charTable[] = {&ledWrite};
        GattService   ledService(LED_SERVICE_UUID, charTable, sizeof(charTable) / sizeof(GattCharacteristic *));
        ble.addService(ledService);
    }

    GattAttribute::Handle_t getValueHandle() const {
        return ledWrite.getValueHandle();
    }

private:
    BLEDevice                         &ble;
    ReadWriteGattCharacteristic<uint8_t>  ledWrite;
};

Main.cpp

void onDataWrittenCallback(const GattWriteCallbackParams *params)
{
    printf("onDataWrittenCallback \n");
    uint8_t i = 0;
    uint8_t res = 0;
    printf("params->len:%d \n", params->len);
    if ((params->handle == ledServicePtr->getValueHandle()) && (params->len >= 1)) {
        for (; i < params->len; i++) {
            res = *((params)->data + i);
            printf("data:%d \n", res);
        }
    }
}
16 Sep 2015

Liu,

When you say you write two uint8_t values, do you mean a 2-byte value? As you know, the ledWrite characteristic has been defined using the template parameter uint8_t (see line 20 of the LED.h above); so you can't be successfully writing anything larger than 8-bits.

regards,

17 Sep 2015

Hi, Rohit

Thank you for your reply. It's helpful for me.

BRs

Please log in to post a reply.