Bluetooth LE (BLE)

BleCharacteristic

BleCharacteristic

In a BLE peripheral role, each service has one or more characteristics. Each characteristic may have one of more values.

In a BLE central role, you typically have a receive handler to be notified when the peripheral updates each characteristic value that you care about.

For assigned characteristics, the data will be in a defined format as defined by the Bluetooth SIG. They are listed here.

For more information about characteristics, see the BLE tutorial.

BleCharacteristic()

You typically construct a characteristic as a global variable with no parameters when you are using central mode and will be receiving values from the peripheral. For example, this is done in the heart rate central tutorial to receive values from a heart rate sensor. It's associated with a specific characteristic UUID after making the BLE connection.

// PROTOTYPE
BleCharacteristic();

// EXAMPLE
// Global variable
BleCharacteristic myCharacteristic;

Once you've created your characteristic in setup() you typically hook in its onDataReceived handler.

// In setup():
myCharacteristic.onDataReceived(onDataReceived, NULL);

The onDataReceived function has this prototype:

void onDataReceived(const uint8_t* data, size_t len, const BlePeerDevice& peer, void* context) 

The BlePeerDevice object is described below.

The context parameter can be used to pass extra data to the callback. It's typically used when you implement the callback in a C++ class to pass the object instance pointer (this).

The onDataReceived() handler is run from the BLE thread. You should avoid lengthy or blocking operations since it will affect other BLE processing. Additionally, the BLE thread has a smaller stack than the main application (loop) thread, so you avoid functions that require a large amount of stack space. To prevent these issues, you should set a flag in the data received handler and perform lengthy or stack-intensive operations from the loop() instead. For example, you should not call Particle.publish(), WiFi.clearCredentials(), and many other functions directly from the onDataReceived handler.

Since 3.0.0:

// PROTOTYPES
typedef std::function<void(const uint8_t*, size_t, const BlePeerDevice& peer)> BleOnDataReceivedStdFunction;

void onDataReceived(const BleOnDataReceivedStdFunction& callback);

template<typename T>
void onDataReceived(void(T::*callback)(const uint8_t*, size_t, const BlePeerDevice& peer), T* instance);

In Device OS 3.0.0 and later, the onDataReceived callback can be a C++ member function or C++11 lambda.

BleCharacteristic (peripheral)

In a peripheral role, you typically define a value that you send out using this constructor. The parameters are:

// PROTOTYPE
BleCharacteristic(const char* desc, BleCharacteristicProperty properties, BleOnDataReceivedCallback callback = nullptr, void* context = nullptr);

BleCharacteristic(const String& desc, BleCharacteristicProperty properties, BleOnDataReceivedCallback callback = nullptr, void* context = nullptr);

// EXAMPLE
// Global variable
BleCharacteristic batteryLevelCharacteristic("bat", BleCharacteristicProperty::NOTIFY, BleUuid(0x2A19), batteryLevelService);
  • "bat" a short string to identify the characteristic
  • BleCharacteristicProperty::NOTIFY The BLE characteristic property. This is typically NOTIFY for values you send out. See also BleCharacteristicProperty.
  • BleUuid(0x2A19) The UUID of this characteristic. In this example it's an assigned (short) UUID. See BleUuid.
  • batteryLevelService The UUID of the service this characteristic is part of.

The UUIDs for the characteristic and service can be a number of formats but are typically either:

  • Explicit BleUuid, like BleUuid(0x2A19)
  • String literal or const char *, like "6E400001-B5A3-F393-E0A9-E50E24DCCA9E"

Both must be the same, so if you use a string literal service UUID, you must also use a string literal for the characteristic UUID as well.

You typically register your characteristic in setup():

// In setup():
BLE.addCharacteristic(batteryLevelCharacteristic);

BleCharacteristic (peripheral with data received)

In a peripheral role if you are receiving data from the central device, you typically assign your characteristic like this.

// PROTOTYPE
// Type T is any type that can be passed to BleUuid, such as const char * or a string-literal.
template<typename T>
BleCharacteristic(const char* desc, BleCharacteristicProperty properties, T charUuid, T svcUuid, BleOnDataReceivedCallback callback = nullptr, void* context = nullptr)

template<typename T>
BleCharacteristic(const String& desc, BleCharacteristicProperty properties, T charUuid, T svcUuid, BleOnDataReceivedCallback callback = nullptr, void* context = nullptr)


// EXAMPLE
BleCharacteristic rxCharacteristic("rx", BleCharacteristicProperty::WRITE_WO_RSP, rxUuid, serviceUuid, onDataReceived, NULL);
  • "rx" a short string to identify the characteristic
  • BleCharacteristicProperty::WRITE_WO_RSP The BLE characteristic property. This is typically WRITE_WO_RSP for values you receive. See also BleCharacteristicProperty.
  • rxUuid The UUID of this characteristic.
  • serviceUuid The UUID of the service this characteristic is part of.
  • onDataReceived The function that is called when data is received.
  • NULL Context pointer. If your data received handler is part of a C++ class, this is a good place to put the class instance pointer (this).

The data received handler has this prototype.

void onDataReceived(const uint8_t* data, size_t len, const BlePeerDevice& peer, void* context)

The onDataReceived() handler is run from the BLE thread. You should avoid lengthy or blocking operations since it will affect other BLE processing. Additionally, the BLE thread has a smaller stack than the main application (loop) thread, so you avoid functions that require a large amount of stack space. To prevent these issues, you should set a flag in the data received handler and perform lengthy or stack-intensive operations from the loop() instead. For example, you should not call Particle.publish(), WiFi.clearCredentials(), and many other functions directly from the onDataReceived handler.

You typically register your characteristic in setup() in peripheral devices:

// In setup():
BLE.addCharacteristic(rxCharacteristic);

The callback is called from the BLE thread. It has a smaller stack than the normal loop stack, and you should avoid doing any lengthy operations that block from the callback. For example, you should not try to use functions like Particle.publish() and you should not use delay(). You should beware of thread safety issues. For example you should use Log.info() and instead of Serial.print() as Serial is not thread-safe.

UUID()

BleCharacteristic::UUID, UUID, BleCharacteristic.UUID

Get the UUID of this characteristic.

// PROTOTYPE
BleUuid UUID() const;


// EXAMPLE
BleUuid uuid = batteryLevelCharacteristic.UUID();

See also BleUuid.

properties()

BleCharacteristic::properties, properties, BleCharacteristic.properties

Get the BLE characteristic properties for this characteristic. This indicates whether it can be read, written, etc..

// PROTOTYPE
BleCharacteristicProperty properties() const;

// EXAMPLE
BleCharacteristicProperty prop = batteryLevelCharacteristic.properties();

See also BleCharacteristicProperty.

getValue(buf, len)

BleCharacteristic::getValue, getValue, BleCharacteristic.getValue

This overload of getValue() is typically used when you have a complex characteristic with a packed data structure that you need to manually extract.

For example, the heart measurement characteristic has a flags byte followed by either a 8-bit or 16-bit value. You typically extract that to a uint8_t buffer using this method and manually extract the data.

// PROTOTYPE
ssize_t getValue(uint8_t* buf, size_t len) const;

getValue(String)

If your characteristic has a string value, you can read it out using this method.

// PROTOTYPE
ssize_t getValue(String& str) const;

// EXAMPLE
String value;
characteristic.getValue(value);

getValue(pointer)

You can read out arbitrary data types (int8_t, uint8_t, uint16_t, uint32_t, struct, etc.) by passing a pointer to the object.

// PROTOTYPE
template<typename T>
ssize_t getValue(T* val) const;

// EXAMPLE
uint16_t value;
characteristic.getValue(&value);

setValue(buf, len)

BleCharacteristic::setValue, setValue, BleCharacteristic.setValue

To set the value of a characteristic to arbitrary data, you can use this function.

// PROTOTYPE
ssize_t setValue(const uint8_t* buf, size_t len);

setValue(string)

To set the value of the characteristic to a string value, use this method. The terminating null is not included in the characteristic; the length is set to the actual string length.

// PROTOTYPE
ssize_t setValue(const String& str);
ssize_t setValue(const char* str);

setValue(pointer)

You can write out arbitrary data types (int8_t, uint8_t, uint16_t, uint32_t, struct, etc.) by passing the value to setValue.

// PROTOTYPE
template<typename T>
ssize_t setValue(T val) const;

// EXAMPLE
uint16_t value = 0x1234;
characteristic.setValue(value);

onDataReceived()

BleCharacteristic::onDataReceived, onDataReceived, BleCharacteristic.onDataReceived

To be notified when a value is received to add a data received callback.

The context is used when you've implemented the data received handler in a C++ class. You can pass the object instance (this) in context. If you have a global function, you probably don't need the context and can pass NULL.

// PROTOTYPE
void onDataReceived(BleOnDataReceivedCallback callback, void* context);

// BleOnDataReceivedCallback
void myCallback(const uint8_t* data, size_t len, const BlePeerDevice& peer, void* context);

The callback is called from the BLE thread. It has a smaller stack than the normal loop stack, and you should avoid doing any lengthy operations that block from the callback. For example, you should not try to use functions like Particle.publish() and you should not use delay(). You should beware of thread safety issues. For example you should use Log.info() and instead of Serial.print() as Serial is not thread-safe.