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.
const string TI_SENSORTAG_ID = "546C0E795800";
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
bleclient1.OnStartScan += (s, e) => Console.WriteLine("Scanning has started");
bleclient1.OnStopScan += (s, e) => Console.WriteLine("Scanning has stopped");
bleclient1.OnAdvertisement += (s, e) => {
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 +
"\r\n\tManufacturerCompanyData: " + BitConverter.ToString(e.ManufacturerDataB) +
"\r\n\tIsConnectable: " + e.IsConnectable +
"\r\n\tIsScanResponse: " + e.IsScanResponse);
};
bleclient1.StartScanning("");
bleclient1.StopScanning();
Filtered Scanning Example
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
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
bleclient1.Connect(TI_SENSORTAG_ID);
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
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
bleclient1.OnDiscovered += (s, e) => {
Console.WriteLine("Service discovered:" +
"\r\n\tService Id: " + e.ServiceId +
"\r\n\tUUID: " + e.Uuid +
"\r\n\tDescription: " + e.Description);
};
bleclient1.DiscoverServices("", "");
bleclient1.DiscoverServices("", "000100000000");
Filtered Service Discovery Example
bleclient1.DiscoverServices("180A,F000AA70-0451-4000-B000-000000000000,F000AA20-0451-4000-B000-000000000000", "");
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
bleclient1.OnDiscovered += (s, e) => {
Console.WriteLine("Characteristic discovered:" +
"\r\n\tOwning Service Id: " + e.ServiceId +
"\r\n\tCharacteristic Id: " + e.CharacteristicId +
"\r\n\tUUID: " + e.Uuid +
"\r\n\tDescription: " + e.Description);
};
bleclient1.DiscoverCharacteristics("000900000000", "");
Filtered Characteristic Discovery Example
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
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 +
"\r\n\tUUID: " + e.Uuid +
"\r\n\tDescription: " + e.Description);
};
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.
Multi-Level Discovery Example
string serviceUUIDs = "180A,F000AA70-0451-4000-B000-000000000000,F000AA20-0451-4000-B000-000000000000";
string characteristicUUIDs = "";
bool discoverDescriptors = true;
string includedByServiceId = "";
bleclient1.Discover(serviceUUIDs, characteristicUUIDs, discoverDescriptors, includedByServiceId);
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
bleclient1.Config("AutoDiscoverIncludedServices=True");
bleclient1.DiscoverServices("", "");
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.
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
foreach (Service s in bleclient1.Services) {
Console.WriteLine("Service: " + s.Description + " (" + s.Uuid + ")");
bleclient1.Service = s.Id;
foreach (Characteristic c in bleclient1.Characteristics) {
Console.WriteLine("\tCharacteristic: " + c.Description + " (" + c.Uuid + ")");
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
byte[] rawLuxVal = luxChara.CachedValueB;
ushort luxVal = BitConverter.ToUInt16(rawLuxVal, 0);
Console.WriteLine("Luxometer Value: " + luxVal);
byte[] rawLuxCCCD = luxCCCD.CachedValueB;
Console.WriteLine("Luxometer CCCD bytes: " + BitConverter.ToString(rawLuxCCCD));
Reading Live Values Example
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);
}
};
byte[] rawBatteryVal = bleclient1.ReadValue("001C00000000", "001C001D0000", "");
Console.WriteLine("Battery Level Value: " + (int)rawBatteryVal[0]);
byte[] rawBatteryPF = bleclient1.ReadValue("001C00000000", "001C001D0000", "001C001D0021");
Console.WriteLine("Battery Level Presentation Format bytes: " + BitConverter.ToString(rawBatteryPF));
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
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);
}
};
bleclient1.WriteValue("004200000000", "004200460000", "", new byte[] { 0x1 });
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
bleclient1.PostValue("00AA00000000", "00AA00BB0000", new byte[] { 0x1, 0x2, 0x3 });
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
bleclient1.OnSubscribed += (s, e) => {
Console.WriteLine("Subscribed to characteristic:" +
"\r\n\tID: " + e.CharacteristicId +
"\r\n\tUUID: " + e.Uuid +
"\r\n\tDescription: " + e.Description);
};
bleclient1.OnUnsubscribed += (s, e) => {
Console.WriteLine("Unsubscribed from characteristic:" +
"\r\n\tID: " + e.CharacteristicId +
"\r\n\tUUID: " + e.Uuid +
"\r\n\tDescription: " + e.Description);
};
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));
};
bleclient1.Subscribe(luxSvc.Id, luxData.Id);
bleclient1.Unsubscribe(luxSvc.Id, luxData.Id);
luxData.Subscribed = true;
luxData.Subscribed = false;
bleclient1.WriteValue(luxSvc.Id, luxData.Id, luxCCCD.Id, new byte[] { 1, 0 });
bleclient1.WriteValue(luxSvc.Id, luxData.Id, luxCCCD.Id, new byte[] { 0, 0 });