Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

Getting Started with Bluetooth Low Energy (BLE) and IPWorks BLE

0.00/5 (No votes)
15 Feb 2018CPOL21 min read 58.3K  
This article covers general Bluetooth Low Energy (BLE) concepts and gives practical instructions for using / nsoftware IPWorks BLE

We recommend downloading and installing IPWorks BLE to follow along with this article. There is a free trial available for .NET and Delphi.

Learn more about and download IPWorks BLE

Introduction to Bluetooth Low Energy (BLE) and IPWorks BLE

IPWorks BLE contains a single component, BLEClient, which provides straightforward access to Bluetooth Low Energy (or BLE) operations. The BLEClient component makes it easy to work with services, characteristics, and descriptors exposed by GATT servers on remote BLE devices.

This article will open with an overview of Bluetooth LE and the GATT data model, after which it will dive into how to use the BLEClient component. By the end of this article, you'll have learned how to:

  • Scan for, and connect to, GATT servers on remote BLE devices
  • Discover services, characteristics, and descriptors (collectively referred to as "GATT objects")
  • Navigate the discovered GATT object hierarchy in BLEClient
  • Read, write, and post data to and from the GATT server
  • Use characteristic subscriptions to leverage real-time data updates

A TI SensorTag is used as the remote device in this article (Model CC2650STK Firmware Ver. 1.43). All code snippets in this article are written in C#, and have the variables in the code block below available to them. All screenshots in the article depict the "bleclient" demo application included with the IPWorks BLE .NET Edition toolkit.

C#
//Global Variables  
const string TI_SENSORTAG_ID = "546C0E795800"; // The Server Id of our TI SensorTag device.
private Bleclient bleclient1 = new Bleclient();

Bluetooth LE Overview

What is Bluetooth LE?

Before discussing what Bluetooth LE (BLE) is, it's important to note what it isn't. Many are already familiar with what is colloquially referred to as "classic Bluetooth"; it's been around since the late 1990s, and has undergone many iterations. Since BLE shares the "Bluetooth" name, a common misconception is that it's a "feature" of classic Bluetooth. It may come as a surprise that, in reality, BLE has very little in common with classic Bluetooth; it's a standalone technology with its own protocols and features.

BLE, introduced around 2010, was designed from the ground up to operate using as little energy as possible, on components which cost as little as possible. Unlike classic Bluetooth, which can trasmit great amounts of data for long periods of time, BLE is intended for use-cases that require only periodic transmissions of small chunks of data.

BLE, then, is not targeted at areas where classic Bluetooth already excels; it's intended to enable new use-cases. The best example of this is the "Internet of Things" (IoT); the prevalence of IoT devices continues to rise, and BLE is the perfect technology for connecting them.

The BLE Data Model: GATT

The Attribute (ATT) Protocol

The BLE specification's most basic client/server relationship is defined by something called the Attribute (ATT) protocol, whose data model is built on the concept of attributes. In the ATT protocol's data model, there is a server which contains attributes, and a client which can send requests to the server to interact with those attributes. Attributes themselves are stored in a flat table, and they consist of these four things:

  • A 16-bit attribute handle (referred to as Id) which uniquely identifies an attribute from the others on the server
  • A value, which is the actual data that the attribute holds
  • A UUID, a universally unique Id which specifies the type of data contained in the value
  • A set of permissions, which apply to all clients

In practice, the ATT protocol (and its data model) are a bit too low-level to be useful. Instead, the component allows you to work with profiles which are based off of the Generic Attribute (GATT) profile. However, because the GATT profile is built atop the ATT protocol, it's helpful to understand the basics of attributes, as well as the fact that the GATT data model inherits the ATT protocol's client/server architecture.

The Generic Attribute (GATT) Profile

BLE Heart Rate Service

In BLE terms, a "profile" typically refers to a specification that, among other things, gives meaning to any services, characteristics, or descriptors that have one of the UUIDs associated with that profile. For example, the UUIDs associated with the Heart Rate profile are what give meaning to the Heart Rate service, its characteristics, and those characteristics' descriptors.

But let's take a step back - what exactly are services, characteristics, and descriptors; how do they relate to each other; and where do attributes fit in? The GATT profile answers all of these questions and more.

We've mentioned that the core BLE data unit is an attribute, that each attribute has a UUID, and that all attributes on a server are stored in a flat table. By giving meaning to certain UUIDs, the GATT profile is able to interpret ranges of attributes in a server's attribute table as being "a service", or "a characteristic", or "a descriptor" (which are commonly referred to as "GATT objects"). Said another way, the GATT profile specifies a generic definition for services, characteristics, and descriptors.

All other BLE profiles extend from the GATT profile, so they all use the concepts of services, characteristics, and descriptors that the GATT profile defines. By mandating a common framework for interpreting attributes, discovering GATT objects, data manipulation, and more; the GATT profile ensures that any GATT client can talk to any GATT server. It is for this reason that all BLE devices must support the GATT profile.

We'll discuss discovery, data manipulation, etc., later in the article. For now, we'll wrap up our BLE overview by describing what services, characteristics, and descriptors are.

GATT Server Example Diagram

Services

Services are the top-level "container" objects of the GATT hierarchy, and are fairly simple. Every service has an Id (recall GATT objects are just ranges of attributes at the ATT layer) and a UUID, and contains at least one characteristic. A service can also reference other services, which are referred to as "included services". Profiles typically define a small number of services (many of them only define one).

Note that it is possible, and completely legal, for a GATT server to contain multiple instances of the same service (and in fact, similar duplications can also occur for characteristics within a service, or for descriptors on a characteristic). It is for this reason that BLEClient uses an Id when unique identification is needed rather than UUIDs.

Characteristics

Characteristics, which are always owned by a service, are where data actually lives in the GATT hierarchy. They have an Id, a UUID, properties (which are referred to as "flags"), and a value. They can also have zero or more descriptors attached to them, each providing some piece of additional metadata about the characteristic.

Characteristics are both the most complex and the most diverse type of GATT object; profiles tend to define more than one characteristic for each service.

Descriptors

Descriptors are the simplest and least diverse of the GATT objects, and have only an Id, a UUID, and a value. They are attached to characteristics in order to augment them with specific pieces of metadata.

Many descriptors are actually defined by the Core BLE specification (rather than by a specific profile), and are used by many profiles; very few profiles define their own descriptors. For example, the "Characteristic User Description" descriptor can be applied to a characteristic to expose a user-friendly string describing what that characteristic is.

Scanning

The first step in interacting with any remote BLE device is to have BLEClient begin scanning for advertisements. An advertisement is a packet of data sent out by a BLE server to inform clients of various pieces of information. The BLE specification defines many different advertisement fields; some of the more commonly seen are the server's Id and name, UUIDs of supported services, whether or not the server is accepting connections, and manufacturer data.

BLEClient exposes the following API for scanning and advertisements:

  • The StartScanning and StopScanning methods control the scanning state.
  • The Scanning property returns the current scanning state.
  • The StartScan and StopScan events fire to indicate changes in the scanning state.
  • The Advertisement event fires during scanning each time an advertisement is received.
  • The ActiveScanning property may be enabled to request a scan response from devices with more detailed information.
  • The ServiceData and ManufacturerData* configuration settings may be used to obtain additional information from an advertisement (if applicable).

To begin, call the StartScanning method, which takes a single string parameter; if you pass a comma-separated list of service UUIDs, BLEClient will instruct the system to filter out any devices which are not advertising support for all of the specified UUIDs. The StartScan event will fire when scanning starts.

To scan for all services pass empty string to StartScanning. To scan for only specific services pass a comma-separated list of service UUIDs to StartScanning.

While BLEClient is scanning, the Advertisement event will fire each time an advertisement event is received by the system. At the very least, the ServerId event parameter will be populated; the rest of the event parameters are populated based on what data is actually in the advertisement packet.

Call the StopScanning method to end scanning. Note that scanning is stopped automatically, if necessary, when attempting to connect to a server. Scanning may also be stopped if the application goes into the background, or in other system-specific situations. The StopScan event will fire when scanning stops.

Basic Scanning and Advertisement Handling Example

C#
// StartScan event handler.
bleclient1.OnStartScan += (s, e) => Console.WriteLine("Scanning has started");
// StopScan event handler.
bleclient1.OnStopScan += (s, e) => Console.WriteLine("Scanning has stopped");
// Advertisement event handler.
bleclient1.OnAdvertisement += (s, e) => {
  // Your application should make every effort to handle the Advertisement event quickly.
  // BLEClient fires it as often as necessary, often multiple times per second.
  Console.WriteLine("Advertisement Received:" +
    "\r\n\tServerId: " + e.ServerId +
    "\r\n\tName: " + e.Name +
    "\r\n\tRSSI: " + e.RSSI +
    "\r\n\tTxPower: " + e.TxPower +
    "\r\n\tServiceUuids: " + e.ServiceUuids +
    "\r\n\tServicesWithData: " + e.ServicesWithData +
    "\r\n\tSolicitedServiceUuids: " + e.SolicitedServiceUuids +
    "\r\n\tManufacturerCompanyId:  " + e.ManufacturerCompanyId +
    // We use BitConverter.ToString() to print data as hex bytes; this also prevents
    // an issue where the string could be cut off early if the data has a 0 byte in it.
    "\r\n\tManufacturerCompanyData:  " + BitConverter.ToString(e.ManufacturerDataB) +
    "\r\n\tIsConnectable:  " + e.IsConnectable +
    "\r\n\tIsScanResponse:  " + e.IsScanResponse);
};

// Scan for all devices.
bleclient1.StartScanning("");
// Wait a while...
bleclient1.StopScanning();

Filtered Scanning Example

C#
// Scan for devices which are advertising at least these UUIDs. You can use a mixture 
// of 16-, 32-, and 128-bit UUID strings, they'll be converted to 128-bit internally.
bleclient1.StartScanning("180A,0000180F,00001801-0000-1000-8000-00805F9B34FB");
// ...
bleclient1.StopScanning();

The ActiveScanning property may be set before StartScanning is called. Active scanning differs from passive scanning in that, for each advertisement packet received, the system will request that an extra "scan response" packet be sent as well. The remote device will typically place different data in the scan response packet. The Advertisement event's IsScanResponse parameter indicates whether the packet the event fired for is a normal advertisement or a scan response.

Active Scanning Example

C#
// Enable active scanning.
bleclient1.ActiveScanning = true;
bleclient1.StartScanning("");
// ...
bleclient1.StopScanning();

Note that the IsConnectable event parameter is always false for scan response packets. Also note that not all platforms support the ability to specify whether to use active or passive scanning. For such platforms, the UseActiveScanning configuration setting won't be available, and the IsScanResponse event parameter will always be false.

Below are some examples of how to scan for devices using the BLEClient demo from IPWorks BLE .NET Edition.

BLEClient Demo: Basic and Filtered Scanning

BLEClient Demo: Basic and Filtered Scanning

BLEClient Demo: Active Scanning

BLEClient Demo: Active Scanning

Connecting & Disconnecting

BLEClient exposes the following API for managing connections and getting information about the currently connected server:

  • The Connect and Disconnect methods.
  • The Connected and Disconnected events.
  • The ServerId and ServerName properties.
  • The ServerUpdate event, which fires when the server's name changes.

Connecting is as simple as calling the Connect method and passing the Id of the server you wish to connect to. If necessary, BLEClient will automatically stop scanning and disconnect from the currently connected server, then it will attempt to connect to the desired server.

Keep in mind that the server Id assigned to a BLE device is not necessarily stable; it might have changed during the time you were not connected. Stale server Ids and out-of-range devices are two of the most common reasons that a connection attempt could fail.

To disconnect from a BLE server call Disconnect. This will clear the discovered GATT objects from the Services, Characteristics, and Descriptors collection properties, and let the system know that it can clean up the resources associated with the connection to the device.

Connecting and Disconnecting Example

C#
// Connect to our TI SensorTag device.
bleclient1.Connect(TI_SENSORTAG_ID);
// Use BLEClient...
// Disconnect from the device.
bleclient1.Disconnect();

Note that, on some platforms, the system might not actually open a connection to the device until you attempt some sort of operation against it, such as discovery. In a similar vein, the system might not close the connection to the device immediately when you call Disconnect, especially if other applications are still using it.

If a connection cannot be established with a device that that you know you should be able to connect to, make sure that it's in range, powered on, and configured to accept connections. Some devices will advertise themselves as being connectable, but will deny most connections anyway (for example, a BLE mouse might advertise as being connectable, but won't allow connections if it's already connected to another client).

Below is an example showing how to connect to a device using the BLEClient demo from IPWorks BLE .NET Edition.

BLEClient Demo: Connecting

BLEClient Demo: Connecting

Discovery

As alluded to in our discussion of the GATT profile, a GATT client must discover the services, characteristics, and descriptors exposed by the GATT server before it can work with them. Since power efficiency is a core focus of BLE, clients should typically only attempt to discover the GATT objects that they need, as they need them.

BLEClient exposes the following API for discovering GATT objects:

  • The DiscoverServices, DiscoverCharacteristics, and DiscoverDescriptors methods, for fine control over discovery processes.
  • The Discover method, for initiating multi-level discovery processes.
  • The Discovered event, which fires each time a service, characteristic, or descriptor is discovered.
  • The IncludeRediscovered configuration setting, which specifies whether the Discovered event should fire again for GATT objects which have already been discovered (enabled by default).
  • The AutoDiscoverCharacteristics, AutoDiscoverDescriptors, and AutoDiscoverIncludedServices configuration settings, which can be enabled to cause BLEClient to auto-discover additional GATT objects during discovery processes (all disabled by default).

Note: See Navigating Discovered Data for information about where discovered GATT objects are stored.

It is important to note that services must be discovered before characteristics. And characteristics must be discovered before descriptors. Attempting to discover a characteristic before discovering the containing service will result in an error.

Service Discovery

Services are the first GATT objects which must be discovered. The DiscoverServices method is used to discover both root services and included services, and accepts a comma-separated list of service UUIDs by which to filter the discovery process.

For each new service discovered, be it a root service or an included service, BLEClient adds an item to the Services collection property and fires the Discovered event. Services can be discovered at any time. There is no need to discover all services immediately after connecting; and it is recommended to discover services only as needed to conserve energy.

Service Discovery Example

C#
// Discovered event handler.
bleclient1.OnDiscovered += (s, e) => {
  Console.WriteLine("Service discovered:" +
    "\r\n\tService Id: " + e.ServiceId +
    // The discovered service's 128-bit UUID string, in the format
    // "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
    "\r\n\tUUID: " + e.Uuid +
    // For standard services whose UUIDs are defined by the Bluetooth SIG,
    // the Description event parameter will contain the name of the service.
    "\r\n\tDescription: " + e.Description);
};

// Discover all root services.
bleclient1.DiscoverServices("", "");

// Discover all services included by a discovered service whose Id is "000100000000".
// (The TI SensorTag doesn't have any included services, this is just an example.)
bleclient1.DiscoverServices("", "000100000000");

Filtered Service Discovery Example

C#
// Discover specific root services. These three UUIDs will cause the Device Information,
// Luxometer, and Humidity services to be discovered on our CC2650STK TI SensorTag.
// (Since the latter two are non-standard, you have to use their full UUIDs.)
bleclient1.DiscoverServices("180A,F000AA70-0451-4000-B000-000000000000,F000AA20-0451-4000-B000-000000000000", "");

// Discover specific services included by a discovered service whose Id is "000100000000".
// (The TI SensorTag doesn't have any included services, this is just an example.)
bleclient1.DiscoverServices("FFFFFFFF-9000-4000-B000-000000000000", "000100000000")

Below are some examples of how to discover services using the BLEClient demo from IPWorks BLE .NET Edition.

BLEClient Demo: Service Discovery

BLEClient Demo: Service Discovery

BLEClient Demo: Filtered Service Discovery

BLEClient Demo: Filtered Service Discovery

Characteristic Discovery

After discovering services, you can discover their characteristics using the DiscoverCharacteristics method. It accepts the Id of the service whose characteristics you wish to discover, and a comma-separated list of characteristic UUIDs by which to filter the discovery process.

For each new characteristic discovered, BLEClient adds an item to the Characteristics collection property (see the Navigating Discovered Data section for more information about how this works) and fires the Discovered event. Characteristics can be discovered at any time; you are not limited to discovering them right after discovering a service, and should prefer to discover them only as needed to conserve energy.

Characteristic Discovery Example

C#
// Discovered event handler.
bleclient1.OnDiscovered += (s, e) => {
  Console.WriteLine("Characteristic discovered:" +
    "\r\n\tOwning Service Id: " + e.ServiceId +
    "\r\n\tCharacteristic Id: " + e.CharacteristicId +
    // The discovered characteristic's 128-bit UUID string, in the format
    // "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
    "\r\n\tUUID: " + e.Uuid +
    // For standard characteristics whose UUIDs are defined by the Bluetooth SIG,
    // the Description event parameter will contain the name of the characteristic.
    "\r\n\tDescription: " + e.Description);
};

// Discover all characteristics for the service whose Id is "000900000000".
// (On our CC2650STK TI SensorTag, this is the Device Information Service.)
bleclient1.DiscoverCharacteristics("000900000000", "");

Filtered Characteristic Discovery Example

C#
// Discover specific characteristics for the service whose Id is "000900000000".
// (On our CC2650STK TI SensorTag, this is the Device Information Service.)
// This will cause the System Id, Model Number String, and Manufacturer Name String 
// characteristics to be discovered, respectively.
bleclient1.DiscoverCharacteristics("000900000000", "2A23,2A24,2A29");

Below are some examples of how to discover characteristics using the BLEClient demo from IPWorks BLE .NET Edition.

BLEClient Demo: Characteristic Discovery

BLEClient Demo: Characteristic Discovery

BLEClient Demo: Filtered Characteristic Discovery

BLEClient Demo: Filtered Characteristic Discovery

Descriptor Discovery

Finally, you can discover descriptors for discovered characteristics using the DiscoverDescriptors method, which takes the Ids of an owning service and characteristic.

For each new descriptor discovered, BLEClient adds an item to the Descriptors collection property (see the Navigating Discovered Data section for more information about how this works) and fires the Discovered event. Descriptors can be discovered at any time; you are not limited to discovering them right after discovering a characteristic, and should prefer to discover them only as needed to conserve energy.

Note that, under certain conditions, BLEClient may automatically attempt to discover specific descriptors for a characteristic. See the Lazily-Initialized Characteristic Fields section for more information.

Descriptor Discovery Example

C#
// Discovered event handler.
bleclient1.OnDiscovered += (s, e) => {
  Console.WriteLine("Descriptor discovered:" +
    "\r\n\tOwning Service Id: " + e.ServiceId +
    "\r\n\tOwning Characteristic Id: " + e.CharacteristicId +
    "\r\n\tDescriptor Id: " + e.DescriptorId +
    // The discovered descriptor's 128-bit UUID string, in the format
    // "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
    "\r\n\tUUID: " + e.Uuid +
    // For standard descriptors whose UUIDs are defined by the Bluetooth SIG,
    // the Description event parameter will contain the name of the descriptor.
    "\r\n\tDescription: " + e.Description);
};

// Discover all descriptors for characteristic Id "001C001D0000", owned by 
// service Id "001C00000000". (On our CC2650STK TI SensorTag, this is the 
// Battery Level characteristic, which is owned by the Battery Service.)
bleclient1.DiscoverDescriptors("001C00000000", "001C001D0000");

Below is an example of how to discover descriptors using the BLEClient demo from IPWorks BLE .NET Edition.

BLEClient Demo: Descriptor Discovery

BLEClient Demo: Descriptor Discovery

Multi-Level Discovery

While it is most energy efficient to only discover the GATT objects that you need, as you need them, there are some cases where it's helpful to be able to discover multiple levels of GATT objects at once. For those cases, BLEClient offers a couple of options.

The first option is to use the Discover method, which takes these four arguments:

  • ServiceUuids - A comma-separated list of service UUIDs used to limit the service discovery step.
  • CharacteristicUuids - A comma-separated list of characteristic UUIDs used to limit the characteristic discovery step, for all services discovered.
  • DiscoverDescriptors - A boolean which determines whether to perform a descriptor discovery step, for all characteristics discovered.
  • IncludedByServiceId - The Id of an already-discovered service which, if present, indicates that this multi-level discovery should attempt to discover services (and so on) included by that one, rather than root services.

 

Multi-Level Discovery Example

C#
string serviceUUIDs = "180A,F000AA70-0451-4000-B000-000000000000,F000AA20-0451-4000-B000-000000000000";
string characteristicUUIDs = ""; //All Characteristics
bool discoverDescriptors = true;
string includedByServiceId = ""; //No included services  
  
// This will discover all characteristics and descriptors for specific services.
bleclient1.Discover(serviceUUIDs, characteristicUUIDs, discoverDescriptors, includedByServiceId);

// This will discover everything on the server, but won't discover
// the "includes"/"included by" service relationships.
bleclient1.Discover("", "", true, "");

The second option is to use one or more of the AutoDiscoverCharacteristics, AutoDiscoverDescriptors, and AutoDiscoverIncludedServices configuration settings so that calls to the DiscoverService, DiscoverCharacteristics, and DiscoverDescriptors methods will trigger additional discovery steps.

AutoDiscoverIncludedServices Example

C#
// This will cause BLEClient to attempt to discover all root and included services.
bleclient1.Config("AutoDiscoverIncludedServices=True");
bleclient1.DiscoverServices("", "");

// Note that these configuration settings also technically affect the Discover() method too.
// So with the "AutoDiscoverIncludedServices" setting enabled, this call will now discover
// everything on the server, _with_ the "includes"/"included by" service relationships.
bleclient1.Discover("", "", true, "");

Below is an example of how to discover everything with multi-level discovery using the BLEClient demo from IPWorks BLE .NET Edition.

BLEClient Demo: Multi-Level Discovery

After you've finished discovering the GATT objects you're interested in, it's time to start working with them. BLEClient exposes the following API for navigating between, and getting information about, the GATT objects that you've discovered:

 

For Services

  • The Services collection property, which contains a list of items representing all discovered services (root or included). Each item has the following fields:
    • Id - The Id of this service.
    • Uuid - The UUID of this service.
    • Description - This service's user-friendly name, if it is a standard service defined by the Bluetooth SIG.
    • IncludedSvcIds - A comma-separated list of Ids of services which this service includes.
    • ParentSvcIds - A comma-separated list of Ids of services which include this service.
  • The Service property, which can be set to the Id of a discovered service to select it.

For Characteristics

  • The Characteristics collection property, which contains a list of items representing all characteristics discovered for the service currently selected by the Service property. Each item has the following fields:
    • Id - The Id of this characteristic.
    • Uuid - The UUID of this characteristic.
    • Description - This characteristic's user-friendly name, if it is a standard characteristic defined by the Bluetooth SIG.
    • Flags and CanSubscribe - A bitfield of this characteristic's flags, and a convenience field which is true if this characteristic has either of the Notify or Indicate flags.
    • Subscribed - Whether or not you are currently subscribed to this characteristic.
    • CachedValue - The latest value cached by the system for this characteristic.
    • UserDescription - If a User Description descriptor is present for this characteristic, this field will contain its value.
    • ValueFormatCount, ValueFormatIndex, ValueFormat, ValueExponent, and ValueUnit - If any Characteristic Presentation Format and/or Characteristic Aggregate Format descriptors are present for this characteristic, these fields can be used to get their values.
  • The Characteristic property, which can be set to the Id of a discovered characteristic to select it.

For Descriptors

  • The Descriptors collection property, which contains a list of items representing all descriptors discovered for the characteristic currently selected by the Characteristic property. Each item has the following fields:
    • Id - The Id of this descriptor.
    • Uuid - The UUID of this descriptor.
    • Description - This descriptor's user-friendly name, if it is a standard descriptor defined by the Bluetooth SIG.
    • CachedValue - The latest value cached by the system for this descriptor.

As you can see, the GATT objects are organized into a tree-like hierarchy. To navigate that hierarchy, you start by looping through the Services collection property. This allows you to inspect your discovered services' information.

If you wish to work with the characteristics you've discovered for a service, set the Service property to that service's Id. This will cause the Characteristics collection property to be populated, and you can loop over it to inspect the characteristics' information.

Finally, you can set the Characteristic property to the Id of a discovered characteristic. This will populate the Descriptors collection property with items representing the descriptors discovered for that characteristic.

Navigating Discovered Data Example

C#
// Loop through all discovered GATT objects and print out their UUIDs and descriptions.
foreach (Service s in bleclient1.Services) {
  Console.WriteLine("Service: " + s.Description + " (" + s.Uuid + ")");
  // Select this service and loop through its characteristics.
  bleclient1.Service = s.Id;

  foreach (Characteristic c in bleclient1.Characteristics) {
    Console.WriteLine("\tCharacteristic: " + c.Description + " (" + c.Uuid + ")");
    // Select this characteristic and loop through its descriptors.
    bleclient1.Characteristic = c.Id;

    foreach (Descriptor d in bleclient1.Descriptors) {
      Console.WriteLine("\t\tDescriptor: " + d.Description + " (" + d.Uuid + ")");
    }
  }
}

BLEClient Demo: Navigating Discovered Data

BLEClient Demo: Navigating Discovered Data

As alluded to above, some fields on items in the Characteristics collection property are initialized based on values held by specific descriptors (if said descriptors are present for a characteristic). Since an attempt must be made to discover the descriptor(s) associated with these fields before initializing them, they are initialized lazily.

BLEClient keeps track of descriptor discovery attempts for each characteristic over the duration of a connection to a device. When you first access a lazily-initialized field, BLEClient will automatically attempt to discover its associated descriptor(s) (firing the Discovered event as necessary), unless such an attempt has already been made. In either case, once the attempt has been made, the field can be initialized.

A good example of this behavior can be seen in screen grab of the BLEClient demo used in the Descriptor Discovery section; when the "Battery Level" characteristic is clicked, the "Characteristic Presentation Format" and "Client Characteristic Configuration" descriptors are automatically discovered.

The following table shows which fields on items in the Characteristics collection property are initialized lazily, and which descriptor(s) are associated with them:

Fields Associated Descriptors
Flags (and any fields which rely on it) Characteristic Extended Properties (0x2900)
Subscribed Client Characteristic Configuration (0x2902)
UserDescription Characteristic User Description (0x2901)
ValueFormatCount, ValueFormatIndex,
ValueFormat, ValueExponent, and ValueUnit
Characteristic Presentation Format (0x2904),
Characteristic Aggregate Format (0x2905)

Reading Data

While you can find out a lot just by navigating the GATT object tree, the ability to work with the actual data on a server is no less important. BLEClient provides two methods of reading characteristics' and descriptors' values, intended to be used in different situations.

The first method of reading values, mentioned previously, is to use the CachedValue fields exposed by items in the Characteristics and Descriptors collection properties. When you query a CachedValue field, BLEClient returns whatever the system's built-in value cache currently has stored.

The second method of reading values is, naturally, reading them directly from the server device. This is done using the ReadValue method, which takes three arguments. To read from a characteristic, pass its Id as well as the Id of its owning service and an empty string for the descriptor Id. To read from a descriptor, pass both of those Ids, as well as a descriptor Id. If the read request is successful, the value will be returned by the ReadValue method, and the Value event will be fired as well.

If the value of a characteristic changes often, consider subscribing to that characteristic (if possible) rather than polling in order to reduce power consumption. See the Characteristic Subscriptions section for more information.

Reading Cached Values Example

C#
// Print the cached value for the Luxometer Data characteristic (which you can
// assume we've already found and assigned to a variable called "luxChara").
byte[] rawLuxVal = luxChara.CachedValueB;
ushort luxVal = BitConverter.ToUInt16(rawLuxVal, 0);
Console.WriteLine("Luxometer Value: " + luxVal);

// Print the cached value for the Client Characteristic Configuration descriptor on
// the Luxometer Data characteristic (again, assume it's stored in "luxCCCD").
byte[] rawLuxCCCD = luxCCCD.CachedValueB;
Console.WriteLine("Luxometer CCCD bytes: " + BitConverter.ToString(rawLuxCCCD));

Reading Live Values Example

C#
// Value event handler.
bleclient1.OnValue += (s, e) => {
  if (string.IsNullOrEmpty(e.DescriptorId)) {
    Console.WriteLine("Read value {" + BitConverter.ToString(e.ValueB) +
      "} for characteristic with UUID " + e.Uuid);
  } else {
    Console.WriteLine("Read value {" + BitConverter.ToString(e.ValueB) +
    "} for descriptor with UUID " + e.Uuid);
  }
};

// Print the live value for the Battery Level characteristic. These Ids 
// are correct for our CC2650STK TI SensorTag, but yours might differ.
byte[] rawBatteryVal = bleclient1.ReadValue("001C00000000", "001C001D0000", "");
Console.WriteLine("Battery Level Value: " + (int)rawBatteryVal[0]);

// Print the live value for the Characteristic Presentation Format descriptor on
// the Battery Level characteristic. Again, your Ids might differ.
byte[] rawBatteryPF = bleclient1.ReadValue("001C00000000", "001C001D0000", "001C001D0021");
Console.WriteLine("Battery Level Presentation Format bytes: " + BitConverter.ToString(rawBatteryPF));

BLEClient Demo: Reading Data

BLEClient Demo: Reading Data

Writing & Posting Data

The WriteValue method may be used to write values to characteristics and descriptors which support it. To write to a characteristic, pass its Id, the Id of its owning service, and the value you wish to write. To write to a descriptor instead, pass its Id too. If the write succeeds, the WriteResponse event will fire.

Writing Values Example

C#
// WriteResponse event handler.
bleclient1.OnWriteResponse += (s, e) => {
  if (string.IsNullOrEmpty(e.DescriptorId)) {
    Console.WriteLine("Successfully wrote to characteristic with UUID " + e.Uuid);
  } else {
    Console.WriteLine("Successfully wrote to descriptor with UUID " + e.Uuid);
  }
};

// Write to the Luxometer Config characteristic. These Ids are correct
// for our CC2650STK TI SensorTag, but yours might differ.
bleclient1.WriteValue("004200000000", "004200460000", "", new byte[] { 0x1 });

// Write to the Client Characteristic Configuration descriptor on the
// Luxometer Data characteristic. Again, your Ids might differ.
bleclient1.WriteValue("004200000000", "004200430000", "004200430045", new byte[] { 0x1 });

For characteristics which have the Write Without Response flag, you may also use the PostValue method to write values (descriptors don't support this functionality). PostValue differs from WriteValue in that the server will not send any sort of response, even if the write request fails. If your use-case can accommodate this behavior, consider using PostValue since the lack of response makes it more energy efficient than WriteValue.

Posting Values Example

C#
// The CC2650STK TI SensorTag doesn't have any characteristics which support
// write without response, so this is just an example.
bleclient1.PostValue("00AA00000000", "00AA00BB0000", new byte[] { 0x1, 0x2, 0x3 });

BLEClient Demo: Writing Data

BLEClient Demo: Writing Data

Characteristic Subscriptions

One of the most important features of the BLE GATT data model is the ability for a GATT server to send characteristic value updates to interested GATT clients in real-time. This push-based model prevents the need for polling, which results in greater energy efficiency.

For characteristics which support subscriptions, a GATT client can subscribe to either notifications or indications to get value updates. The difference between the two is simple: notifications are not acknowledged by GATT clients, while indications are. Note that characteristics do not have to support both types of subscriptions; for example, many only support notifications.

BLEClient makes it simple to work with characteristic subscriptions. The first thing to do is to check whether a characteristic supports subscriptions by querying the CanSubscribe field for the characteristic in question. If that returns true, you can subscribe to and unsubscribe from the characteristic using one of three methods:

  • Calling the Subscribe and Unsubscribe methods.
  • Setting the characteristic's Subscribed field.
  • Writing directly to the characteristic's Client Characteristic Configuration Descriptor (CCCD) using the WriteValue method.

No matter which method you choose, the Subscribed and Unsubscribed events will fire as the characteristic's subscription state changes. While you're subscribed to a characteristic, the Value event will fire anytime BLEClient receives a value update for it.

There are a few things to keep in mind when working with characteristic subscriptions:

  • Subscriptions typically do not survive between connections.
  • BLEClient doesn't limit the number of concurrent characteristic subscriptions you can have, but your system might.
  • For characteristics which support both notifications and indications, BLEClient will prefer notifications by default. You can enable the PreferIndications configuration setting to change this behavior for new subscriptions.
    • Note that when writing directly to the CCCD BLEClient does not have a preference.

Subscriptions Example

C#
// Subscribed event handler.
bleclient1.OnSubscribed += (s, e) => {
  Console.WriteLine("Subscribed to characteristic:" +
    "\r\n\tID: " + e.CharacteristicId +
    "\r\n\tUUID: " + e.Uuid +
    "\r\n\tDescription: " + e.Description);
};
// Unsubscribed event handler.
bleclient1.OnUnsubscribed += (s, e) => {
  Console.WriteLine("Unsubscribed from characteristic:" +
    "\r\n\tID: " + e.CharacteristicId +
    "\r\n\tUUID: " + e.Uuid +
    "\r\n\tDescription: " + e.Description);
};
// Value event handler.
bleclient1.OnValue += (s, e) => {
  Console.WriteLine("Value update received for characteristic: " +
    "\r\n\tID: " + e.CharacteristicId +
    "\r\n\tUUID: " + e.Uuid +
    "\r\n\tDescription: " + e.Description +
    "\r\n\tValue: " + BitConverter.ToString(e.ValueB));
};

// Assume that we've already found the Luxometer Data characteristic,
// its owning service, and the Client Characteristic Configuration
// descriptor on it; and we've stored them in variables called "luxSvc",
// "luxData", and "luxCCCD".

// Subscribe and unsubscribe using methods.
bleclient1.Subscribe(luxSvc.Id, luxData.Id);
// ...
bleclient1.Unsubscribe(luxSvc.Id, luxData.Id);

// Subscribe and unsubscribe using the "Subscribed" field.
luxData.Subscribed = true;
// ...
luxData.Subscribed = false;

// Subscribe and unsubscribe by writing directly to the CCCD.
bleclient1.WriteValue(luxSvc.Id, luxData.Id, luxCCCD.Id, new byte[] { 1, 0 });
// ...
bleclient1.WriteValue(luxSvc.Id, luxData.Id, luxCCCD.Id, new byte[] { 0, 0 });

BLEClient Demo: Characteristic Subscriptions

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)