Getting Started with IPWorks BLE and BLEClient
Requirements: IPWorks BLE
Contents
- Introduction
- Prerequisites
- Bluetooth LE Overview
- Scanning
- Connecting & Disconnecting
- Pairing
- Discovery
- Navigating Discovered Data
- Reading Data
- Writing & Posting Data
- Characteristic Subscriptions
Prerequisites
Windows Notes:
Please note that the BLEClient component is only supported by Windows 10 Creator's Update (10.0.15063.0) and later. In addition, the BLEClient component requires .NET 4.5 or higher be installed on the system.
Android Notes:
The BLEClient component requires Android API level 21 or higher. Note that on Android, even when pairing is not explicitly enabled in the component, the operating system may automatically perform pairing as it deems appropriate.
iOS/macOS Notes:
The BLEClient component has the following operating system requirements:
- iOS 5.0+
- iPadOS 5.0+
- Mac Catalyst 13.0+
- macOS 10.10+
Linux Notes:
On Linux, the BLE components rely on BlueZ. This package must be installed in order to use the components. Many Linux distributions come with BlueZ pre-installed.
Introduction to IPWorks BLE and BLEClient
IPWorks BLE is a suite of components which provide straightforward access to Bluetooth Low Energy (or BLE) operations.
This article will focus specifically on the BLEClient component, a simple yet flexible BLE GATT client implementation. 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 IPWorks BLE .NET Edition
//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
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.
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
// 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
// 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
// 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: 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
// 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.
Pairing
Pairing is the process by which two devices establish a secure connection and share encryption keys to ensure that their communication is private and secure. The BLEClient component supports automatic pairing as part of the connection process.
By default, pairing is turned off and no pairing will be attempted by the component. To enable pairing, set the PairingMode configuration setting to a value of 1 (Automatic). When this is set, the component will attempt to pair if pairing is supported by the device.
During the pairing process, the component will fire the PairingRequest event, where the pairing request can be accepted or rejected. This event must be handled in order for pairing to succeed, and more information can be found in the PairingRequest Event section below. If automatic pairing is enabled, a pairing failure will result in a failed connection to the device.
Note that on Android, even when pairing is not explicitly enabled in the component, the operating system may automatically perform pairing as it deems appropriate. For iOS and macOS, the operating system handles all pairing, and this is not controllable from the component.
PairingRequest Event
The PairingRequest event will fire while pairing, allowing the client to decide whether or not to continue with the pairing process.
The ServerId parameter indicates the server for which pariring is being performed, while PairingKind indicates the kind of pairing being requested. Possible values are:
1 | ConfirmOnly |
4 | ProvidedPin |
8 | ConfirmPinMatch |
Based on the PairingKind, different actions should be taken by the application. More information on each kind can be found below:
ConfirmOnly
The application must confirm they wish to perform the pairing action. An optional confirmation dialog may be presented to the user. Set Accept to true to confirm the pairing request.
ProvidePin
The application must request a PIN from the user. The PIN will typically be displayed on the target device. For this kind of pairing, along with setting Accept to true, the Pin parameter will need to be set to the provided PIN.
ConfirmPinMatch
The application must display the given PIN to the user and ask the user to confirm that the PIN matches the one shown on the target device. Set Accept to true to indicate the PIN values match.
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).
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
// 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
// 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: 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
// 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
// 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: 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
// 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
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.
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
// 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
Navigating Discovered Data
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
// 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
Lazily-Initialized Characteristic Fields
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
// 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
// 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));
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
// 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
// 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 });
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.
// 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 });
We appreciate your feedback. If you have any questions, comments, or suggestions about this article please contact our support team at support@nsoftware.com.