Bluetooth Low Energy (Bluetooth LE, BLE), also known as Bluetooth Smart is a wireless personal area network technology aimed at applications in the healthcare, fitness, beacons, security, and home entertainment industries. Compared to Classic Bluetooth, Bluetooth LE is intended to provide considerably reduced power consumption and cost while maintaining a similar communication range.

This article describes how to use Bluetooth Framework with Bluetooth LE GATT Profiles. To learn how to use Bluetooth Framework with Bluetooth LE Beacons refer to the Bluetooth Framework and Bluetooth LE Beacons article.

Note for Windows 10 1609 and above
If you run Windows 10 1609 and above and your Bluetooth LE device is paired you may face issue with receiving OnDisconnect event. Unapir device before connect.
Note for Windows 10 1703 and above
If you run Windows 10 1703 and above and your Bluetooth LE device is paired you may face issue with reading device services, characteristics and other properties. Unpair device before connect.
Note for Windows 10 1803 and above
if you run Windows 10 1803 and above and your Bluetooth LE devuce is paired you may get randomly device disconnection. Unpair device before connect.

Table Of Contents

Generic Attributes

Beacon The Generic Attributes (GATT) define a hierarchical data structure that is exposed to connected Bluetooth Low Energy (LE) devices.

The GATT profiles describe a use case, roles and general behaviors based on the GATT functionality. Services are collections of characteristics and relationships to other services that encapsulate the behavior of part of a device. This also includes hierarchy of services, characteristics and attributes used in the attribute server.

On top of the GATT hierarchy is a profile, which is composed of one or more services necessary to fulfill a use case. A service is composed of characteristics or references to other services. A characteristic consists of a type (represented by a UUID), a value, a set of properties indicating the operations the characteristic supports and a set of permissions relating to security. It may also include one or more descriptors—metadata or configuration flags relating to the owning characteristic.

GATT defines client (BLE Central) and server (BLE Peripheral) roles. The GATT server stores the data transported over the air to the GATT client and accepts requests, commands and confirmations from the GATT client. The GATT server sends responses to requests and sends indications and notifications asynchronously to the GATT client when specified events occur on the GATT server. GATT also specifies the format of data contained on the GATT server.

Current version of the Bluetooth Framework supports only GATT Client (Central) role with BlueSoleil and Microsoft Bluetooth drivers. BlueSoleil Bluetooth drivers allows to use Bluetooth LE GATT features on any Windows platform starting from Windows XP. Microsoft Bluetooth drivers supports BLE GATT features starting from Windows 8.

Universally Unique ID

A Universally Unique ID (UUID) is an abbreviation you will see a lot in the BLE world. It is a unique number used to identify services, characteristics and descriptors, also known as attributes. These IDs are transmitted over the air so that e.g. a peripheral can inform a central what services it provides. To save transmitting air time and memory space there are two kinds of UUIDs:

  • The first type is a short 16-bit UUID. The predefined Heart rate service, e.g., has the UUID 0x180D and one of its enclosed characteristics, the Heart Rate Measurement characteristic, has the UUID 0x2A37. The 16-bit UUID is energy and memory efficient, but since it only provides a relatively limited number of unique IDs there is a rule; a device can only transmit the predefined Bluetooth SIG UUIDs directly over the air. Hence there is a need for a second type of UUID so vendors can transmit their own custom UUIDs as well.
  • The second type is a long 128-bit UUID, sometimes referred to as a vendor specific UUID. This is the type of UUID vendors need to use when they are making their own custom services and characteristics.

Bluetooth Framework supports both types of the UUIDs through wclGattUuid structure.

Bluetooth LE Device Address

To communicate with a Bluetooth LE device your application must know the Bluetooth Device Address, the 48-bit (6-byte) number that uniquely identifies a device among peers. There are two types of device addresses: Public and Random Device Address.

Public Device Address is the standard, IEEE-assigned 48-bit universal LAN MAC address which must be obtained from the IEEE Registration Authority. It is divided into two fields: IEEE-assigned company ID held in the 24 most-significant bits; Company-assigned device ID held in the 24 least significant bits.

Static Random Device Address is a 48-bit randomly generated address. A new value is generated after each power cycle. If the static address of a device is changed, then the address stored in peer devices will not be valid and the ability to reconnect using the old address will be lost.

Private Random Device Address is used when a device wants to remain private. These are addresses that can be periodically changed so that the device can not be tracked. These may be resolvable or not. Resolvable Private Addresse is an address that can be resolved through a pre-shared hash key: Only the trusted entities that have your pre-shared key can identify you. For all other entities, the address seems to be randomly changing and untrackable. These addresses are generated by a mathematical algorithm using the Identity Resolving Key (IRK) - this is one of the keys exchanged during pairing. Non-Resolvable Private Addresse is an address that is random and can not be "expected". A possible use case: a device that already communicated a non-resolvable address to a peer for a reconnection.

Bluetooth Framework supports any type of the Bluetooth LE Device Addresses. However there is no easy way to resolve the Resolvable Private Addresse to real Device MAC at the moment.

Bluetooth Radio Object

To be able to communicate with a Bluetooth LE device an application must get currently available Bluetooth Radio object. The Bluetooth Radio object represents a Bluetooth hardware connected to your computer. It can be built-in or external (USB, Serial) Bluetooth adapter that works with BlueSoleil or Microsoft Bluetooth drivers (Bluetooth Framework does not support Bluetooth LE communication with Toshiba Bluetooth drivers).

To get the active Bluetooth Radio object an application should use the wclBluetoothManager class. First it must be opened by calling the Open method. It is good idea to place the code into your main window (form) initialization.

wclBluetoothManager: TwclBluetoothManager;

procedure TfmMain.FormCreate(Sender: TObject);
begin
	wclBluetoothManager.Open;
end;
TwclBluetoothManager *wclBluetoothManager;

void __fastcall TfmMain::FormCreate(TObject *Sender)
{
	wclBluetoothManager->Open();
}
private wclBluetoothManager Manager;

private void fmMain_Load(object sender, EventArgs e)
{
	Manager = new wclBluetoothManager();
	Manager.Open();
}
Private WithEvents Manager As wclBluetoothManager

Private Sub main_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
	Manager = New wclBluetoothManager()
	Manager.Open()
End Sub
CwclBluetoothManager wclBluetoothManager;

BOOL CGattClientDlg::OnInitDialog()
{
	CDialog::OnInitDialog();
	wclBluetoothManager.Open();
}


Once the wclBluetoothManager opened you can try to find the working Bluetooth Radio object. Note: Bluetooth Framework always detects Microsoft Bluetooth Radio even there is other drivers installed. Your application must not destroy the Bluetooth Radio Object returned by the wclBluetoothManager class.

function TfmMain.GetRadio: TwclBluetoothRadio;
var
	i: Integer;
begin
	Result := nil;
	if wclBluetoothManager.Count = 1 then
		Result := wclBluetoothManager.Radios[0]
	else
		for i := 0 to wclBluetoothManager.Count - 1 do
			if wclBluetoothManager.Radios[i].Api <> baMicrosoft then begin
				Result := wclBluetoothManager.Radios[i];
				Break;
			end;
end;
TwclBluetoothRadio* __fastcall TfmMain::GetRadio()
{
	if (wclBluetoothManager->Count == 1)
		return wclBluetoothManager->Radios[0];
	
	for (int i = 0; i < wclBluetoothManager->Count; i++)
		if (wclBluetoothManager->Radios[i]->Api != baMicrosoft)
			return wclBluetoothManager->Radios[i];
	
	return NULL;
}
private wclBluetoothRadio GetRadio()
{
	if (Manager.Count == 1)
		return Manager[0];
	
	for (Int32 i = 0; i < Manager.Count; i++)
		if (Manager[i].Api != wclBluetoothApi.baMicrosoft)
			return Manager[i];
	
	return null;
}
Private Function GetRadio() As wclBluetoothRadio
	If Manager.Count = 1 Then
		Return Manager(0)
	End If
	
	Dim i As Int32
	For i = 0 To Manager.Count - 1
		If Manager(i).Api <> wclBluetoothApi.baMicrosoft Then
			Return Manager(i)
		End If
	Next i
	
	Return Nothing
End Function
CwclBluetoothRadio* CGattClientDlg::GetRadio()
{
	if (wclBluetoothManager.GetCount() == 1)
		return wclBluetoothManager.GetRadios(0);
	
	for (size_t i = 0; i < wclBluetoothManager.GetCount(); i++)
		if (wclBluetoothManager.GetRadios(i)->GetApi() != baMicrosoft)
			return wclBluetoothManager.GetRadios(i);
	
	return NULL;
}


To find more detailed information and code example about working with the wclBluetoothManager and how to get the working Bluetooth Radio Object refer to the GattDemo application from the Bluetooth Framework package.

Discovering Bluetooth LE Devices

Once you get the working Bluetooth Radio Object you need to discover nearby Bluetooth LE devices. Depending on the used Bluetooth drivers in-app discovering can be different.

In-app Bluetooth LE discovering available with BlueSoleil Bluetooth drivers on any Windows platform. With Microsoft Bluetooth drivers:

  • On Windows 8 you must discovr and pair with Bluetooth LE device through Windows UI. After device paired you can discover it using in-app discovering methods.
  • On Windows 10 12040 and below you also have to pair with Bluetooth LE device through Windows UI. After device paired you can discover it using in-app discovering methods.
  • On Windows 10 14393 and above you do not need to pair with your device manually. You can discover nearby Bluetooth LE device using in-app discovering.

To discover nearby Bluetooth LE device call the Discover method of the wclBluetoothRadio object.

procedure TfmMain.btDiscoverClick(Sender: TObject);
var
	Radio: TwclBluetoothRadio;
	Res: Integer;
begin
	Radio := GetRadio;
	if Radio <> nil then begin
		Res := Radio.Discover(10, dkBle);
		if Res <> WCL_E_SUCCESS then
			MessageDlg('Error starting discovering: 0x' + IntToHex(Res, 8), mtError, [mbOK], 0);	
	end;
end;

procedure TfmMain.wclBluetoothManagerDiscoveringStarted(Sender: TObject;const Radio: TwclBluetoothRadio);
begin
	lvDevices.Items.Clear;
end;

procedure TfmMain.wclBluetoothManagerDeviceFound(Sender: TObject; const Radio: TwclBluetoothRadio; const Address: Int64);
var
	Item: TListItem;
	DevType: TwclBluetoothDeviceType;
	Res: Integer;
begin
	DevType := dtMixed;
	Res := Radio.GetRemoteDeviceType(Address, DevType);
	
	Item := lvDevices.Items.Add;
	Item.Caption := IntToHex(Address, 12);
	Item.SubItems.Add(''); // We can not read a device's name here.
	Item.Data := Radio; // To use it later.
	if Res <> WCL_E_SUCCESS then
		Item.SubItems.Add('Error: 0x' + IntToHex(Res, 8))
	else
		case DevType of
			dtClassic:
				Item.SubItems.Add('Classic');
			dtBle:
				Item.SubItems.Add('BLE');
			dtMixed:
				Item.SubItems.Add('Mixed');
			else
				Item.SubItems.Add('Unknown');
		end;
end;

procedure TfmMain.wclBluetoothManagerDiscoveringCompleted(Sender: TObject; const Radio: TwclBluetoothRadio; const Error: Integer);
var
	i: Integer;
	Item: TListItem;
	Address: Int64;
	Res: Integer;
	DevName: string;
begin
	if lvDevices.Items.Count = 0 then
		MessageDlg('No BLE devices were found.', mtInformation, [mbOK], 0)
	else
		// Here we can update found devices names.
		for i := 0 to lvDevices.Items.Count - 1 do begin
			Item := lvDevices.Items[i];
			
			Address := StrToInt64('$' + Item.Caption);
			Res := Radio.GetRemoteName(Address, DevName);
			if Res <> WCL_E_SUCCESS then
				Item.SubItems[0] := 'Error: 0x' + IntToHex(Res, 8)
			else
				Item.SubItems[0] := DevName;
		end;
end;
void __fastcall TfmMain::btDiscoverClick(TObject *Sender)
{
	TwclBluetoothRadio* Radio = GetRadio();
	if (Radio != NULL)
	{
		int Res = Radio->Discover(10, dkBle);
		if (Res != WCL_E_SUCCESS)
			MessageDlg("Error starting discovering: 0x" + IntToHex(Res, 8), mtError, TMsgDlgButtons() << mbOK, 0);
	}
}

void __fastcall TfmMain::wclBluetoothManagerDiscoveringStarted(TObject *Sender, const TwclBluetoothRadio *Radio)
{
	lvDevices->Items->Clear();
}

void __fastcall TfmMain::wclBluetoothManagerDeviceFound(TObject *Sender, const TwclBluetoothRadio *Radio, const __int64 Address)
{
	TwclBluetoothDeviceType DevType = dtMixed;
	int Res = ((TwclBluetoothRadio*)Radio)->GetRemoteDeviceType(Address, DevType);
	
	TListItem* Item = lvDevices->Items->Add();
	Item->Caption = IntToHex(Address, 12);
	Item->SubItems->Add(""); // We can not read a device's name here.
	Item->Data = (void*)Radio; // To use it later.
	if (Res != WCL_E_SUCCESS)
		Item->SubItems->Add("Error: 0x" + IntToHex(Res, 8));
	else
		switch (DevType)
		{
			case dtClassic:
				Item->SubItems->Add("Classic");
				break;
			case dtBle:
				Item->SubItems->Add("BLE");
				break;
			case dtMixed:
				Item->SubItems->Add("Mixed");
				break;
			default:
				Item->SubItems->Add("Unknown");
				break;
		}
}

void __fastcall TfmMain::wclBluetoothManagerDiscoveringCompleted( TObject *Sender, const TwclBluetoothRadio *Radio, const int Error)
{
	if (lvDevices->Items->Count == 0)
		MessageDlg("No BLE devices were found.", mtInformation, TMsgDlgButtons() << mbOK, 0);
	else
		// Here we can update found devices names.
		for (int i = 0; i < lvDevices->Items->Count; i++)
		{
			TListItem* Item = lvDevices->Items->Item[i];
			
			__int64 Address = StrToInt64("$" + Item->Caption);
			String DevName = "";
			int Res = ((TwclBluetoothRadio*)Radio)->GetRemoteName(Address, DevName);
			if (Res != WCL_E_SUCCESS)
				Item->SubItems->Strings[0] = "Error: 0x" + IntToHex(Res, 8);
			else
				Item->SubItems->Strings[0] = DevName;
		}
}
private void btDiscover_Click(object sender, EventArgs e)
{
	wclBluetoothRadio Radio = GetRadio();
	if (Radio != null)
	{
		Int32 Res = Radio.Discover(10, wclBluetoothDiscoverKind.dkBle);
		if (Res != wclErrors.WCL_E_SUCCESS)
			MessageBox.Show("Error starting discovering: 0x" + Res.ToString("X8"), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
	}
}

void Manager_OnDiscoveringStarted(object Sender, wclBluetoothRadio Radio)
{
	lvDevices.Items.Clear();
}

void Manager_OnDeviceFound(object Sender, wclBluetoothRadio Radio, long Address)
{
	wclBluetoothDeviceType DevType = wclBluetoothDeviceType.dtMixed;
	Int32 Res = Radio.GetRemoteDeviceType(Address, out DevType);
	
	ListViewItem Item = lvDevices.Items.Add(Address.ToString("X12"));
	Item.SubItems.Add(""); // We can not read a device's name here.
	Item.Tag = Radio; // To use it later.
	if (Res != wclErrors.WCL_E_SUCCESS)
		Item.SubItems.Add("Error: 0x" + Res.ToString("X8"));
	else
		switch (DevType)
		{
			case wclBluetoothDeviceType.dtClassic:
				Item.SubItems.Add("Classic");
				break;
			case wclBluetoothDeviceType.dtBle:
				Item.SubItems.Add("BLE");
				break;
			case wclBluetoothDeviceType.dtMixed:
				Item.SubItems.Add("Mixed");
				break;
			default:
				Item.SubItems.Add("Unknown");
				break;
		}
}

void Manager_OnDiscoveringCompleted(object Sender, wclBluetoothRadio Radio, int Error)
{
	if (lvDevices.Items.Count == 0)
		MessageBox.Show("No BLE devices were found.", "Discovering for BLE devices", MessageBoxButtons.OK, MessageBoxIcon.Information);
	else
		// Here we can update found devices names.
		for (Int32 i = 0; i i <  lvDevices.Items.Count; i++)
		{
			ListViewItem Item = lvDevices.Items[i];
			
			Int64 Address = Convert.ToInt64(Item.Text, 16);
			String DevName;
			Int32 Res = Radio.GetRemoteName(Address, out DevName);
			if (Res != wclErrors.WCL_E_SUCCESS)
				Item.SubItems[1].Text = "Error: 0x" + Res.ToString("X8");
			else
				Item.SubItems[1].Text = DevName;
		}
}
Private Sub btDiscover_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btDiscover.Click
	Dim Radio As wclBluetoothRadio = GetRadio()
	If Radio IsNot Nothing Then
		Dim Res As Int32 = Radio.Discover(10, wclBluetoothDiscoverKind.dkBle)
		If Res <> wclErrors.WCL_E_SUCCESS Then
			MessageBox.Show("Error starting discovering: 0x" + Res.ToString("X8"), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error)
		End If
	End If
End Sub

Private Sub Manager_OnDiscoveringStarted(ByVal Sender As Object, ByVal Radio As wclBluetooth.wclBluetoothRadio) Handles Manager.OnDiscoveringStarted
	lvDevices.Items.Clear()
End Sub

Private Sub Manager_OnDeviceFound(ByVal Sender As Object, ByVal Radio As wclBluetooth.wclBluetoothRadio, ByVal Address As Long) Handles Manager.OnDeviceFound
	Dim DevType As wclBluetoothDeviceType = wclBluetoothDeviceType.dtMixed
	Dim Res As Int32 = Radio.GetRemoteDeviceType(Address, DevType)
	
	Dim Item As ListViewItem = lvDevices.Items.Add(Address.ToString("X12"))
	Item.SubItems.Add("") ' We can not read a device's name here.
	Item.Tag = Radio 'To use it later.
	If Res <> wclErrors.WCL_E_SUCCESS Then
		Item.SubItems.Add("Error: 0x" + Res.ToString("X8"))
	Else
		Select Case DevType
			Case wclBluetoothDeviceType.dtClassic
				Item.SubItems.Add("Classic")
			Case wclBluetoothDeviceType.dtBle
				Item.SubItems.Add("BLE")
			Case wclBluetoothDeviceType.dtMixed
				Item.SubItems.Add("Mixed")
			Case Else
				Item.SubItems.Add("Unknown")
		End Select
	End If
End Sub

Private Sub Manager_OnDiscoveringCompleted(ByVal Sender As Object, ByVal Radio As wclBluetooth.wclBluetoothRadio, ByVal [Error] As Integer) Handles Manager.OnDiscoveringCompleted
	If lvDevices.Items.Count = 0 Then
		MessageBox.Show("No BLE devices were found.", "Discovering for BLE devices", MessageBoxButtons.OK, MessageBoxIcon.Information)
	Else
		' Here we can update found devices names.
		Dim i As Int32
		For i = 0 To lvDevices.Items.Count - 1
			Dim Item As ListViewItem = lvDevices.Items(i)
			
			Dim Address As Int64 = Convert.ToInt64(Item.Text, 16)
			Dim DevName As String = ""
			Dim Res As Int32 = Radio.GetRemoteName(Address, DevName)
			If Res <> wclErrors.WCL_E_SUCCESS Then
				Item.SubItems(1).Text = "Error: 0x" + Res.ToString("X8")
			Else
				Item.SubItems(1).Text = DevName
			End If
		Next i
	End If
End Sub
void CGattClientDlg::OnBnClickedButtonDiscover()
{
	CwclBluetoothRadio* Radio = GetRadio();
	if (Radio != NULL)
	{
		int Res = Radio->Discover(10, dkBle);
		if (Res != WCL_E_SUCCESS)
			AfxMessageBox(_T("Error starting discovering: 0x") + IntToHex(Res));
	}
}

void CGattClientDlg::wclBluetoothManagerDiscoveringStarted(void* Sender, CwclBluetoothRadio* Radio)
{
	lvDevices.DeleteAllItems();
}

void CGattClientDlg::wclBluetoothManagerDeviceFound(void* Sender, CwclBluetoothRadio* Radio, __int64 Address)
{
	wclBluetoothDeviceType DevType = dtMixed;
	int Res = Radio->GetRemoteDeviceType(Address, DevType);
	
	int Item = lvDevices.GetItemCount();
	lvDevices.InsertItem(Item, IntToHex(Address));
	lvDevices.SetItemText(Item, 1, _T("")); // We can not read a device's name here.
	lvDevices.SetItemData(Item, (DWORD_PTR)Radio); // To use it later.
	if (Res != WCL_E_SUCCESS)
		lvDevices.SetItemText(Item, 2, _T("Error: 0x") + IntToHex(Res));
	else
		switch (DevType)
		{
			case dtClassic:
				lvDevices.SetItemText(Item, 2, _T("Classic"));
				break;
			case dtBle:
				lvDevices.SetItemText(Item, 2, _T("BLE"));
				break;
			case dtMixed:
				lvDevices.SetItemText(Item, 2, _T("Mixed"));
				break;
			default:
				lvDevices.SetItemText(Item, 2, _T("Unknown"));
		}
}

void CGattClientDlg::wclBluetoothManagerDiscoveringCompleted(void* Sender, CwclBluetoothRadio* Radio, int Error)
{
	if (lvDevices.GetItemCount() == 0)
		AfxMessageBox(_T("No BLE devices were found."));
	else
		// Here we can update found devices names.
		for (int i = 0; i < lvDevices.GetItemCount(); i++)
		{
			__int64 Address = StrToInt64(lvDevices.GetItemText(i, 0));
			tstring DevName;
			int Res = Radio->GetRemoteName(Address, DevName);
			if (Res != WCL_E_SUCCESS)
				lvDevices.SetItemText(i, 1, _T("Error: 0x") + IntToHex(Res));
			else
				lvDevices.SetItemText(i, 1, DevName.c_str());
		}
}


If you need to know the Bluetooth LE device name or other information you should read it only after discovering completed. The OnDiscoveringCompleted event handler is good place to do that (as it is shown in code above).

To find more detailed information and code example about discovering nearby Bluetooth LE devices refer to the GattDemo application for the Bluetooth Framework package.

Connecting To And Disconnect From BLE Devices

After discovering Bluetooth Low Energy devices you can connect to it. To connect to a remote Bluetooth LE device you need to know its Address (which is discovered on the previous step) and the working Bluetooth Radio Object. Note: Connection procedure is asynchronouse. The call to the Connect method of the wclGattClient class simple starts the connection procedure. After connection is established (with or without success) the OnConnect event fires with the real connection result code. In case case if connection was established with success (the Error parameter passed into the OnConnect event handler is WCL_E_SUCCESS) the wclGattClient stays in csConnected state and you can read device's GATT services, characteristics and other attributes. If the connection was not success the wclGattClient switches back to the csDisconnected state and your application can try to connect to it once again or try to connect to other BLE device.

To disconnect from the connected BLE device call the Disconnect method of the wclGattClient class. After disconnecting the OnDisconnect event fires. The event also fires if the device terminates the connection from its side.

procedure TfmMain.btConnectClick(Sender: TObject);
var
	Res: Integer;
	Item: TListItem;
begin
	if lvDevices.Selected = nil then
		MessageDlg('Select device', mtWarning, [mbOK], 0)
	else begin
		Item := lvDevices.Selected;
		wclGattClient.Address := StrToInt64('$' + Item.Caption);
		Res := wclGattClient.Connect(TwclBluetoothRadio(Item.Data));
		if Res <> WCL_E_SUCCESS then
			MessageDlg('Error: 0x' + IntToHex(Res, 8), mtError, [mbOK], 0);
	end;
end;

procedure TfmMain.wclGattClientConnect(Sender: TObject; const Error: Integer);
begin
	// Connection property is valid here.
	TraceEvent(TwclGattClient(Sender).Address, 'Connected', 'Error', '0x' + IntToHex(Error, 8));
end;

procedure TfmMain.btDisconnectClick(Sender: TObject);
var
	Res: Integer;
begin
	Res := wclGattClient.Disconnect;
	if Res <> WCL_E_SUCCESS then
		MessageDlg('Error: 0x' + IntToHex(Res, 8), mtError, [mbOK], 0);
end;

procedure TfmMain.wclGattClientDisconnect(Sender: TObject; const Reason: Integer);
begin
	// Connection property is valid here.
	TraceEvent(TwclGattClient(Sender).Address, 'Disconnected', 'Reason', '0x' + IntToHex(Reason, 8));
end;
void __fastcall TfmMain::btConnectClick(TObject *Sender)
{
	if (lvDevices->Selected == NULL)
		MessageDlg("Select device", mtWarning, TMsgDlgButtons() << mbOK, 0);
	else
	{
		TListItem* Item = lvDevices->Selected;
		wclGattClient->Address = StrToInt64("$" + Item->Caption);
		int Res = wclGattClient->Connect((TwclBluetoothRadio*)Item->Data);
		if (Res != WCL_E_SUCCESS)
			MessageDlg("Error: 0x" + IntToHex(Res, 8), mtError, TMsgDlgButtons() << mbOK, 0);
	}
}

void __fastcall TfmMain::wclGattClientConnect(TObject *Sender, const int Error)
{
	// Connection property is valid here.
	TraceEvent(((TwclGattClient*)Sender)->Address, "Connected", "Error", "0x" + IntToHex(Error, 8));
}

void __fastcall TfmMain::btDisconnectClick(TObject *Sender)
{
	int Res = wclGattClient->Disconnect();
	if (Res != WCL_E_SUCCESS)
		MessageDlg("Error: 0x" + IntToHex(Res, 8), mtError, TMsgDlgButtons() << mbOK, 0);
}

void __fastcall TfmMain::wclGattClientDisconnect(TObject *Sender, const int Reason)
{
	// Connection property is valid here.
	TraceEvent(((TwclGattClient*)Sender)->Address, "Disconnected", "Reason", "0x" + IntToHex(Reason, 8));
}
private void btConnect_Click(object sender, EventArgs e)
{
	if (lvDevices.SelectedItems.Count == 0)
		MessageBox.Show("Select device", "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning);
	else
	{
		ListViewItem Item = lvDevices.SelectedItems[0];
		Client.Address = Convert.ToInt64(Item.Text, 16);
		Int32 Res = Client.Connect((wclBluetoothRadio)Item.Tag);
		if (Res != wclErrors.WCL_E_SUCCESS)
			MessageBox.Show("Error: 0x" + Res.ToString("X8"), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
	}
}

void Client_OnConnect(object Sender, int Error)
{
	// Connection property is valid here.
	TraceEvent(((wclGattClient)Sender).Address, "Connected", "Error", "0x" + Error.ToString("X8"));
}

private void btDisconnect_Click(object sender, EventArgs e)
{
	Int32 Res = Client.Disconnect();
	if (Res != wclErrors.WCL_E_SUCCESS)
		MessageBox.Show("Error: 0x" + Res.ToString("X8"), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
}

void Client_OnDisconnect(object Sender, int Reason)
{
	// Connection property is valid here.
	TraceEvent(((wclGattClient)Sender).Address, "Disconnected", "Reason", "0x" + Reason.ToString("X8"));
}
Private Sub btConnect_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btConnect.Click
	If lvDevices.SelectedItems.Count = 0 Then
		MessageBox.Show("Select device", "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning)
	Else
		Dim Item As ListViewItem = lvDevices.SelectedItems(0)
		Client.Address = Convert.ToInt64(Item.Text, 16)
		Dim Res As Int32 = Client.Connect(CType(Item.Tag, wclBluetoothRadio))
		If Res <> wclErrors.WCL_E_SUCCESS Then
			MessageBox.Show("Error: 0x" + Res.ToString("X8"), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error)
		End If
	End If
End Sub
    
Private Sub Client_OnConnect(ByVal Sender As Object, ByVal [Error] As Integer) Handles Client.OnConnect
	' Connection property is valid here.
	TraceEvent(CType(Sender, wclGattClient).Address, "Connected", "Error", "0x" + [Error].ToString("X8"))
End Sub

Private Sub btDisconnect_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btDisconnect.Click
	Dim Res As Int32 = Client.Disconnect()
	If Res <> wclErrors.WCL_E_SUCCESS Then
		MessageBox.Show("Error: 0x" + Res.ToString("X8"), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error)
	End If
End Sub

Private Sub Client_OnDisconnect(ByVal Sender As Object, ByVal Reason As Integer) Handles Client.OnDisconnect
	' Connection property is valid here.
	TraceEvent(CType(Sender, wclGattClient).Address, "Disconnected", "Reason", "0x" + Reason.ToString("X8"))
End Sub
void CGattClientDlg::OnBnClickedButtonConnect()
{
	POSITION Pos = lvDevices.GetFirstSelectedItemPosition();
	if (Pos == NULL)
		AfxMessageBox(_T("Select device"));
	else
	{
		int Item = lvDevices.GetNextSelectedItem(Pos);
		wclGattClient.SetAddress(StrToInt64(lvDevices.GetItemText(Item, 0)));
		int Res = wclGattClient.Connect((CwclBluetoothRadio*)lvDevices.GetItemData(Item));
		if (Res != WCL_E_SUCCESS)
			AfxMessageBox(_T("Error: 0x") + IntToHex(Res));
	}
}

void CGattClientDlg::wclGattClientConnect(void* Sender, int Error)
{
	// Connection property is valid here.
	TraceEvent(((CwclGattClient*)Sender)->GetAddress(), _T("Connected"), _T("Error"), _T("0x") + IntToHex(Error));
}

void CGattClientDlg::OnBnClickedButtonDisconnect()
{
	int Res = wclGattClient.Disconnect();
	if (Res != WCL_E_SUCCESS)
		AfxMessageBox(_T("Error: 0x") + IntToHex(Res));
}

void CGattClientDlg::wclGattClientDisconnect(void* Sender, int Reason)
{
	// Connection property is valid here.
	TraceEvent(((CwclGattClient*)Sender)->GetAddress(), _T("Disconnected"), _T("Reason"), _T("0x") + IntToHex(Reason));
}


To find more detailed information and code example about connecting to the selected Bluetooth LE devices refer to the GattDemo application from the Bluetooth Framework package.

Reading Services

Once connection to the Bluetooth Low Energy device has been established you can read the devices services. The GATT services are collections of characteristics and relationships to other services that encapsulate the behavior of part of a device. Each service has UUID that unique identifies the services. Bluetooth SIG defines the List Of the Service Assigned Numbers. To read GATT services Bluetooth Framework provides ReadServices method of the wclGattClient class. Note: services reading is synchronouse procedure.

procedure TfmMain.btGetServicesClick(Sender: TObject);
var
	Res: Integer;
	Item: TListItem;
	i: Integer;
	Service: TwclGattService;
begin
	lvServices.Items.Clear;
	FServices := nil;
	
	Res := wclGattClient.ReadServices(OpFlag, FServices);
	if Res <> WCL_E_SUCCESS then begin
		MessageDlg('Error: 0x' + IntToHex(Res, 8), mtError, [mbOK], 0);
		Exit;
	end;
	
	if FServices = nil then
		Exit;
	
	for i := 0 to Length(FServices) - 1 do begin
		Service := FServices[i];
		
		Item := lvServices.Items.Add;
		if Service.Uuid.IsShortUuid then
			Item.Caption := IntToHex(Service.Uuid.ShortUuid, 4)
		else
			Item.Caption := GUIDToString(Service.Uuid.LongUuid);
		Item.SubItems.Add(BoolToStr(Service.Uuid.IsShortUuid, True));
		Item.SubItems.Add(IntToHex(Service.Handle, 4));
	end;
end;
void __fastcall TfmMain::btGetServicesClick(TObject *Sender)
{
	lvServices->Items->Clear();
	FServices.Length = 0;
	
	int Res = wclGattClient->ReadServices(OpFlag(), FServices);
	if (Res != WCL_E_SUCCESS)
	{
		MessageDlg("Error: 0x" + IntToHex(Res, 8), mtError, TMsgDlgButtons() << mbOK, 0);
		return;
	}
	
	if (FServices.Length == 0)
		return;
	
	for (int i = 0;  i <  FServices.Length; i++)
	{
		TwclGattService Service = FServices[i];
		
		TListItem* Item = lvServices->Items->Add();
		if (Service.Uuid.IsShortUuid)
			Item->Caption = IntToHex(Service.Uuid.ShortUuid, 4);
		else
			Item->Caption = Sysutils::GUIDToString(Service.Uuid.LongUuid);
		Item->SubItems->Add(BoolToStr(Service.Uuid.IsShortUuid, true));
		Item->SubItems->Add(IntToHex(Service.Handle, 4));
	}
}
private void btGetServices_Click(object sender, EventArgs e)
{
	lvServices.Items.Clear();
	FServices = null;
	
	Int32 Res = Client.ReadServices(OpFlag(), out FServices);
	if (Res != wclErrors.WCL_E_SUCCESS)
	{
		MessageBox.Show("Error: 0x" + Res.ToString("X8"), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
		return;
	}
	
	if (FServices == null)
		return;
	
	foreach(wclGattService Service in FServices)
	{
		String s;
		if (Service.Uuid.IsShortUuid)
			s = Service.Uuid.ShortUuid.ToString("X4");
		else
			s = Service.Uuid.LongUuid.ToString();
		ListViewItem Item = lvServices.Items.Add(s);
		Item.SubItems.Add(Service.Uuid.IsShortUuid.ToString());
		Item.SubItems.Add(Service.Handle.ToString("X4"));
	}
}
Private Sub btGetServices_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btGetServices.Click
	lvServices.Items.Clear()
	FServices = Nothing
	
	Dim Res As Int32 = Client.ReadServices(OpFlag(), FServices)
	If Res <> wclErrors.WCL_E_SUCCESS Then
		MessageBox.Show("Error: 0x" + Res.ToString("X8"), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error)
		Return
	End If
	
	If FServices Is Nothing Then
		Return
	End If
	
	For Each Service As wclGattService In FServices
		Dim s As String
		If Service.Uuid.IsShortUuid Then
			s = Service.Uuid.ShortUuid.ToString("X4")
		Else
			s = Service.Uuid.LongUuid.ToString()
		End If
		Dim Item As ListViewItem = lvServices.Items.Add(s)
		Item.SubItems.Add(Service.Uuid.IsShortUuid.ToString())
		Item.SubItems.Add(Service.Handle.ToString("X4"))
	Next Service
End Sub
void CGattClientDlg::OnBnClickedButtonGetServices()
{
	lvServices.DeleteAllItems();
	FServices.clear();
	
	int Res = wclGattClient.ReadServices(OpFlag(), FServices);
	if (Res != WCL_E_SUCCESS)
	{
		AfxMessageBox(_T("Error: 0x") + IntToHex(Res));
		return;
	}
	
	if (FServices.size() == 0)
		return;
	
	for (wclGattServices::iterator i = FServices.begin(); i != FServices.end(); i++)
	{
		wclGattService Service = (*i);
		
		int Item = lvServices.GetItemCount();
		if (Service.Uuid.IsShortUuid)
			lvServices.InsertItem(Item, IntToHex(Service.Uuid.ShortUuid));
		else
			lvServices.InsertItem(Item, GUIDToString(Service.Uuid.LongUuid));
		lvServices.SetItemText(Item, 1, BoolToStr(Service.Uuid.IsShortUuid));
		lvServices.SetItemText(Item, 2, IntToHex(Service.Handle));
	}
}


To find more detailed information and code example about reading Bluetooth LE GATT services refer to the GattDemo application from the Bluetooth Framework package.

Reading Characteristics

Once you read services you can read Characteristics of the selected service. The Characteristic encapsulates a single data point (though it may contain an array of related data, such as X/Y/Z values from a 3-axis accelerometer, etc.). Each Characteristic described via a pre-defined 16-bit or 128-bit UUID. Here you can find the standard characteristics defined by the Bluetooth SIG.

Characteristics are the main point that you will interact with your BLE peripheral, so it iss important to understand the concept. They are also used to send data back to the BLE peripheral, since you are also able to write to characteristic.

Each Characteristic in addition to the UUID has few properties that describes the Characteristic's "features". Bluetooth Framework provides all the Characteristic's information through the wclGattCharacteristic structure. Below is the descriptions of the important Characteristic's properties:

  • IsBroadcastable - True indicates that the Characteristic can be broadcast.
  • IsReadable - True indicates that the Characteristic can be read.
  • IsWritable - True indicates that the Characteristic can be written to.
  • IsWritableWithoutResponse - True indicates that the Characteristic can be written to without requiring a response.
  • IsSignedWritable - True indicates that the Characteristic can be signed writable.
  • IsNotifiable - True indicates that the Characteristic can be updated by the device through Handle Value Notifications, and the new value will be returned through the event.
  • IsIndicatable - True indicates that the Characteristic can be updated by the device through Handle Value Indications, and the new value will be returned through the event.
  • HasExtendedProperties - True indicates that the Characteristic has extended properties, which will be presented through a Characteristic Extended Properties descriptor.
procedure TfmMain.btGetCharacteristicsClick(Sender: TObject);
var
	Service: TwclGattService;
	Res: Integer;
	Item: TListItem;
	i: Integer;
	Character: TwclGattCharacteristic;
begin
	FCharacteristics := nil;
	lvCharacteristics.Items.Clear;
	
	if lvServices.Selected = nil then begin
		MessageDlg('Select service', mtWarning, [mbOK], 0);
		Exit;
	end;
	
	Service := FServices[lvServices.Selected.Index];
	Res := wclGattClient.ReadCharacteristics(Service, OpFlag, FCharacteristics);
	if Res <> WCL_E_SUCCESS then begin
		MessageDlg('Error: 0x' + IntToHex(Res, 8), mtError, [mbOK], 0);
		Exit;
	end;
	
	if FCharacteristics = nil then
		Exit;
	
	for i := 0 to Length(FCharacteristics) - 1 do begin
		Character := FCharacteristics[i];
		
		Item := lvCharacteristics.Items.Add;
		if Character.Uuid.IsShortUuid then
			Item.Caption := IntToHex(Character.Uuid.ShortUuid, 4)
		else
			Item.Caption := GUIDToString(Character.Uuid.LongUuid);
		Item.SubItems.Add(BoolToStr(Character.Uuid.IsShortUuid, True));
		Item.SubItems.Add(IntToHex(Character.ServiceHandle, 4));
		Item.SubItems.Add(IntToHex(Character.Handle, 4));
		Item.SubItems.Add(IntToHex(Character.ValueHandle, 4));
		Item.SubItems.Add(BoolToStr(Character.IsBroadcastable, True));
		Item.SubItems.Add(BoolToStr(Character.IsReadable, True));
		Item.SubItems.Add(BoolToStr(Character.IsWritable, True));
		Item.SubItems.Add(BoolToStr(Character.IsWritableWithoutResponse, True));
		Item.SubItems.Add(BoolToStr(Character.IsSignedWritable, True));
		Item.SubItems.Add(BoolToStr(Character.IsNotifiable, True));
		Item.SubItems.Add(BoolToStr(Character.IsIndicatable, True));
		Item.SubItems.Add(BoolToStr(Character.HasExtendedProperties, True));
	end;
end;
void __fastcall TfmMain::btGetCharacteristicsClick(TObject *Sender)
{
	FCharacteristics.Length = 0;
	lvCharacteristics->Items->Clear();
	
	if (lvServices->Selected == NULL)
	{
		MessageDlg("Select service", mtWarning, TMsgDlgButtons() << mbOK, 0);
		return;
	}
	
	TwclGattService Service = FServices[lvServices->Selected->Index];
	int Res = wclGattClient->ReadCharacteristics(Service, OpFlag(), FCharacteristics);
	if (Res != WCL_E_SUCCESS)
	{
		MessageDlg("Error: 0x" + IntToHex(Res, 8), mtError, TMsgDlgButtons() << mbOK, 0);
		return;
	}
	
	if (FCharacteristics.Length == 0)
		return;
	
	for (int i = 0;  i <  FCharacteristics.Length; i++)
	{
		TwclGattCharacteristic Character = FCharacteristics[i];
		
		TListItem* Item = lvCharacteristics->Items->Add();
		if (Character.Uuid.IsShortUuid)
			Item->Caption = IntToHex(Character.Uuid.ShortUuid, 4);
		else
			Item->Caption = Sysutils::GUIDToString(Character.Uuid.LongUuid);
		Item->SubItems->Add(BoolToStr(Character.Uuid.IsShortUuid, true));
		Item->SubItems->Add(IntToHex(Character.ServiceHandle, 4));
		Item->SubItems->Add(IntToHex(Character.Handle, 4));
		Item->SubItems->Add(IntToHex(Character.ValueHandle, 4));
		Item->SubItems->Add(BoolToStr(Character.IsBroadcastable, true));
		Item->SubItems->Add(BoolToStr(Character.IsReadable, true));
		Item->SubItems->Add(BoolToStr(Character.IsWritable, true));
		Item->SubItems->Add(BoolToStr(Character.IsWritableWithoutResponse, true));
		Item->SubItems->Add(BoolToStr(Character.IsSignedWritable, true));
		Item->SubItems->Add(BoolToStr(Character.IsNotifiable, true));
		Item->SubItems->Add(BoolToStr(Character.IsIndicatable, true));
		Item->SubItems->Add(BoolToStr(Character.HasExtendedProperties, true));
	}
}
private void btGetCharacteristics_Click(object sender, EventArgs e)
{
	FCharacteristics = null;
	lvCharacteristics.Items.Clear();
	
	if (lvServices.SelectedItems.Count == 0)
	{
		MessageBox.Show("Select service", "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning);
		return;
	}
	
	wclGattService Service = FServices[lvServices.SelectedItems[0].Index];
	Int32 Res = Client.ReadCharacteristics(Service, OpFlag(), out FCharacteristics);
	if (Res != wclErrors.WCL_E_SUCCESS)
	{
		MessageBox.Show("Error: 0x" + Res.ToString("X8"), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
		return;
	}
	
	if (FCharacteristics == null)
		return;
	
	foreach(wclGattCharacteristic Character in FCharacteristics)
	{
		String s;
		if (Character.Uuid.IsShortUuid)
			s = Character.Uuid.ShortUuid.ToString("X4");
		else
			s = Character.Uuid.LongUuid.ToString();
		ListViewItem Item = lvCharacteristics.Items.Add(s);
		
		Item.SubItems.Add(Character.Uuid.IsShortUuid.ToString());
		Item.SubItems.Add(Character.ServiceHandle.ToString("X4"));
		Item.SubItems.Add(Character.Handle.ToString("X4"));
		Item.SubItems.Add(Character.ValueHandle.ToString("X4"));
		Item.SubItems.Add(Character.IsBroadcastable.ToString());
		Item.SubItems.Add(Character.IsReadable.ToString());
		Item.SubItems.Add(Character.IsWritable.ToString());
		Item.SubItems.Add(Character.IsWritableWithoutResponse.ToString());
		Item.SubItems.Add(Character.IsSignedWritable.ToString());
		Item.SubItems.Add(Character.IsNotifiable.ToString());
		Item.SubItems.Add(Character.IsIndicatable.ToString());
		Item.SubItems.Add(Character.HasExtendedProperties.ToString());
	}
}
Private Sub btGetCharacteristics_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btGetCharacteristics.Click
	FCharacteristics = Nothing
	lvCharacteristics.Items.Clear()
	
	If lvServices.SelectedItems.Count = 0 Then
		MessageBox.Show("Select service", "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning)
		Return
	End If
	
	Dim Service As wclGattService = FServices(lvServices.SelectedItems(0).Index)
	Dim Res As Int32 = Client.ReadCharacteristics(Service, OpFlag(), FCharacteristics)
	If Res <> wclErrors.WCL_E_SUCCESS Then
		MessageBox.Show("Error: 0x" + Res.ToString("X8"), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error)
		Return
	End If
	
	If FCharacteristics Is Nothing Then
		Return
	End If
	
	For Each Character As wclGattCharacteristic In FCharacteristics
		Dim s As String
		If Character.Uuid.IsShortUuid Then
			s = Character.Uuid.ShortUuid.ToString("X4")
		Else
			s = Character.Uuid.LongUuid.ToString()
		End If
		Dim Item As ListViewItem = lvCharacteristics.Items.Add(s)
		
		Item.SubItems.Add(Character.Uuid.IsShortUuid.ToString())
		Item.SubItems.Add(Character.ServiceHandle.ToString("X4"))
		Item.SubItems.Add(Character.Handle.ToString("X4"))
		Item.SubItems.Add(Character.ValueHandle.ToString("X4"))
		Item.SubItems.Add(Character.IsBroadcastable.ToString())
		Item.SubItems.Add(Character.IsReadable.ToString())
		Item.SubItems.Add(Character.IsWritable.ToString())
		Item.SubItems.Add(Character.IsWritableWithoutResponse.ToString())
		Item.SubItems.Add(Character.IsSignedWritable.ToString())
		Item.SubItems.Add(Character.IsNotifiable.ToString())
		Item.SubItems.Add(Character.IsIndicatable.ToString())
		Item.SubItems.Add(Character.HasExtendedProperties.ToString())
	Next Character
End Sub
void CGattClientDlg::OnBnClickedButtonGetCharacteristics()
{
	FCharacteristics.clear();
	lvCharacteristics.DeleteAllItems();
	
	POSITION Pos = lvServices.GetFirstSelectedItemPosition();
	if (Pos == NULL)
	{
		AfxMessageBox(_T("Select service"));
		return;
	}
	
	wclGattServices::iterator it = FServices.begin();
	std::advance(it, lvServices.GetNextSelectedItem(Pos));
	wclGattService Service = (*it);
	
	int Res = wclGattClient.ReadCharacteristics(Service, OpFlag(), FCharacteristics);
	if (Res != WCL_E_SUCCESS)
	{
		AfxMessageBox(_T("Error: 0x") + IntToHex(Res));
		return;
	}
	
	if (FCharacteristics.size() == 0)
		return;
	
	for (wclGattCharacteristics::iterator i = FCharacteristics.begin(); i != FCharacteristics.end(); i++)
	{
		wclGattCharacteristic Character = (*i);
		
		int Item = lvCharacteristics.GetItemCount();
		if (Character.Uuid.IsShortUuid)
			lvCharacteristics.InsertItem(Item, IntToHex(Character.Uuid.ShortUuid));
		else
			lvCharacteristics.InsertItem(Item, GUIDToString(Character.Uuid.LongUuid));
		lvCharacteristics.SetItemText(Item, 1, BoolToStr(Character.Uuid.IsShortUuid));
		lvCharacteristics.SetItemText(Item, 2, IntToHex(Character.ServiceHandle));
		lvCharacteristics.SetItemText(Item, 3, IntToHex(Character.Handle));
		lvCharacteristics.SetItemText(Item, 4, IntToHex(Character.ValueHandle));
		lvCharacteristics.SetItemText(Item, 5, BoolToStr(Character.IsBroadcastable));
		lvCharacteristics.SetItemText(Item, 6, BoolToStr(Character.IsReadable));
		lvCharacteristics.SetItemText(Item, 7, BoolToStr(Character.IsWritable));
		lvCharacteristics.SetItemText(Item, 8, BoolToStr(Character.IsWritableWithoutResponse));
		lvCharacteristics.SetItemText(Item, 9, BoolToStr(Character.IsSignedWritable));
		lvCharacteristics.SetItemText(Item, 10, BoolToStr(Character.IsNotifiable));
		lvCharacteristics.SetItemText(Item, 11, BoolToStr(Character.IsIndicatable));
		lvCharacteristics.SetItemText(Item, 12, BoolToStr(Character.HasExtendedProperties));
	}
}


To find more detailed information and code example about reading Bluetooth LE GATT characteristics refer to the GattDemo application from the Bluetooth Framework package.

Reading And Writing Characteristic Value

If the Characteristic has the IsReadable property set to True an application can read the Characteristic's value. Bluetooth Framework represents the Characteristic value as a raw bytes array. The meaning of the value depends on the characteristic's UUID. Bluetooth SIG defines the meaning for the Characteristics Assigned Numbers. The meaning of the value of the vendor defined characteristics can be different and defined by the vendor.

procedure TfmMain.btGetCharValueClick(Sender: TObject);
var
	Characteristic: TwclGattCharacteristic;
	Res: Integer;
	Value: TwclGattCharacteristicValue;
	Str: string;
	i: Integer;
begin
	if lvCharacteristics.Selected = nil then begin
		MessageDlg('Select characteristic', mtWarning, [mbOK], 0);
		Exit;
	end;
	
	Characteristic := FCharacteristics[lvCharacteristics.Selected.Index];
	Res := wclGattClient.ReadCharacteristicValue(Characteristic, OpFlag, Value);
	if Res <> WCL_E_SUCCESS then begin
		MessageDlg('Error: 0x' + IntToHex(Res, 8), mtError, [mbOK], 0);
		Exit;
	end;
	
	if Value = nil then
		Exit;
	
	try
		if Length(Value) = 0 then
			MessageDlg('Value is empty', mtInformation, [mbOK], 0)
		else begin
			Str := '';
			
			for i := Low(Value) to High(Value) do
				Str := Str + IntToHex(Value[i], 2);
			
			MessageDlg('Value: ' + Str, mtInformation, [mbOK], 0);
		end;
	finally
		Value := nil;
	end;
end;
void __fastcall TfmMain::btGetCharValueClick(TObject *Sender)
{
	if (lvCharacteristics->Selected == NULL)
	{
		MessageDlg("Select characteristic", mtWarning, TMsgDlgButtons() << mbOK, 0);
		return;
	}
	
	TwclGattCharacteristic Characteristic = FCharacteristics[lvCharacteristics->Selected->Index];
	TwclGattCharacteristicValue Value;
	int Res = wclGattClient->ReadCharacteristicValue(Characteristic, OpFlag(), Value);
	if (Res != WCL_E_SUCCESS)
	{
		MessageDlg("Error: 0x" + IntToHex(Res, 8), mtError, TMsgDlgButtons() << mbOK, 0);
		return;
	}
	
	if (Value.Length == 0)
	{
		MessageDlg("Value is empty", mtInformation, TMsgDlgButtons() << mbOK, 0);
		return;
	}
	
	try
	{
		String Str = "";
		
		for (int i = 0;  i <  Value.Length; i++)
			Str = Str + IntToHex(Value[i], 2);
		
		MessageDlg("Value: " + Str, mtInformation, TMsgDlgButtons() << mbOK, 0);
	}
	__finally
	{
		Value.Length = 0;
	}
}
private void btGetCharValue_Click(object sender, EventArgs e)
{
	if (lvCharacteristics.SelectedItems.Count == 0)
	{
		MessageBox.Show("Select characteristic", "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning);
		return;
	}
	
	wclGattCharacteristic Characteristic = FCharacteristics[lvCharacteristics.SelectedItems[0].Index];
	Byte[] Value;
	Int32 Res = Client.ReadCharacteristicValue(Characteristic, OpFlag(), out Value);
	if (Res != wclErrors.WCL_E_SUCCESS)
	{
		MessageBox.Show("Error: 0x" + Res.ToString("X8"), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
		return;
	}
	
	if (Value == null)
		return;
	
	try
	{
		if (Value.Length == 0)
			MessageBox.Show("Value is empty", "Information", MessageBoxButtons.OK, MessageBoxIcon.Information);
		else
		{
			String Str = "";
			for (Int32 i = 0;  i <  Value.Length; i++)
				Str = Str + Value[i].ToString("X2");
			
			MessageBox.Show("Value: " + Str, "Characterist value", MessageBoxButtons.OK, MessageBoxIcon.Information);
		}
	}
	finally
	{
		Value = null;
	}
}
Private Sub btGetCharValue_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btGetCharValue.Click
	If lvCharacteristics.SelectedItems.Count = 0 Then
		MessageBox.Show("Select characteristic", "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning)
		Return
	End If
	
	Dim Characteristic As wclGattCharacteristic = FCharacteristics(lvCharacteristics.SelectedItems(0).Index)
	Dim Value As Byte() = Nothing
	Dim Res As Int32 = Client.ReadCharacteristicValue(Characteristic, OpFlag(), Value)
	If Res <> wclErrors.WCL_E_SUCCESS Then
		MessageBox.Show("Error: 0x" + Res.ToString("X8"), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error)
		Return
	End If
	
	If Value Is Nothing Then
		Return
	End If
	
	Try
		If Value.Length = 0 Then
			MessageBox.Show("Value is empty", "Information", MessageBoxButtons.OK, MessageBoxIcon.Information)
		Else
			Dim Str As String = ""
			For i As Int32 = 0 To Value.Length - 1
				Str = Str + Value(i).ToString("X2")
			Next i
			MessageBox.Show("Value: " + Str, "Characterist value", MessageBoxButtons.OK, MessageBoxIcon.Information)
		End If
	Finally
		Value = Nothing
	End Try
End Sub
void CGattClientDlg::OnBnClickedButtonGetCharValue()
{
	POSITION Pos = lvCharacteristics.GetFirstSelectedItemPosition();
	if (Pos == NULL)
	{
		AfxMessageBox(_T("Select characteristic"));
		return;
	}
	
	wclGattCharacteristics::iterator it = FCharacteristics.begin();
	std::advance(it, lvCharacteristics.GetNextSelectedItem(Pos));
	wclGattCharacteristic Characteristic = (*it);
	
	wclGattCharacteristicValue Value;
	int Res = wclGattClient.ReadCharacteristicValue(Characteristic, OpFlag(), Value);
	if (Res != WCL_E_SUCCESS)
	{
		AfxMessageBox(_T("Error: 0x") + IntToHex(Res));
		return;
	}
	
	if (Value.size() == 0)
	{
		AfxMessageBox(_T("Value is empty"));
		return;
	}
	
	try
	{
		CString Str = _T("");
		
		for (wclGattCharacteristicValue::iterator i = Value.begin(); i != Value.end(); i++)
			Str = Str + IntToHex((*i));
		
		AfxMessageBox(_T("Value: ") + Str);
	}
	finally(
		Value.clear();
	)
}


If the Characteristic has either IsWritable or IsWritableWithoutResponse or IsSignedWritable property set to True an application can Write to the Characteristic.

procedure TfmMain.btSetValueClick(Sender: TObject);
var
	Str: string;
	Characteristic: TwclGattCharacteristic;
	Val: TwclGattCharacteristicValue;
	i: Integer;
	j: Integer;
	Res: Integer;
begin
	if lvCharacteristics.Selected = nil then begin
		MessageDlg('Select characteristic', mtWarning, [mbOK], 0);
		Exit;
	end;
	
	Characteristic := FCharacteristics[lvCharacteristics.Selected.Index];
	
	Str := edCharVal.Text;
	if Length(Str) mod 2 <> 0 then
		Str := '0' + Str;
	SetLength(Val, Length(Str) div 2);
	i := 1;
	j := 0;
	while i < Length(Str) do begin
		Val[j] := StrToInt('$' + Copy(Str, i, 2));
		Inc(j);
		Inc(i, 2);
	end;
	
	Res := wclGattClient.WriteCharacteristicValue(Characteristic, Val);
	if Res <> WCL_E_SUCCESS then
		MessageDlg('Error: 0x' + IntToHex(Res, 8), mtError, [mbOK], 0);
end;
void __fastcall TfmMain::btSetValueClick(TObject *Sender)
{
	if (lvCharacteristics->Selected == NULL)
	{
		MessageDlg("Select characteristic", mtWarning, TMsgDlgButtons() << mbOK, 0);
		return;
	}
	
	TwclGattCharacteristic Characteristic = FCharacteristics[lvCharacteristics->Selected->Index];
	
	String Str = edCharVal->Text;
	if (Str.Length() % 2 != 0)
		Str = "0" + Str;
	TwclGattCharacteristicValue Val;
	Val.Length = Str.Length() / 2;
	int i = 0;
	int j = 0;
	while (i < Str.Length())
	{
		Val[j] = StrToInt("$" + Str.SubString(i, 2));
		j++;
		i += 2;
	}
	
	int Res = wclGattClient->WriteCharacteristicValue(Characteristic, Val);
	if (Res != WCL_E_SUCCESS)
		MessageDlg("Error: 0x" + IntToHex(Res, 8), mtError, TMsgDlgButtons() << mbOK, 0);
}
private void btSetValue_Click(object sender, EventArgs e)
{
	if (lvCharacteristics.SelectedItems.Count == 0)
	{
		MessageBox.Show("Select characteristic", "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning);
		return;
	}
	
	wclGattCharacteristic Characteristic = FCharacteristics[lvCharacteristics.SelectedItems[0].Index];
	
	String Str = edCharVal.Text;
	if (Str.Length % 2 != 0)
		Str = "0" + Str;
	
	Byte[] Val = new Byte[Str.Length / 2];
	for (Int32 i = 0;  i <  Val.Length; i++)
	{
		String b = Str.Substring(i * 2, 2);
		Val[i] = Convert.ToByte(b, 16);
	}
	
	Int32 Res = Client.WriteCharacteristicValue(Characteristic, Val);
	if (Res != wclErrors.WCL_E_SUCCESS)
		MessageBox.Show("Error: 0x" + Res.ToString("X8"), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
Private Sub btSetValue_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btSetValue.Click
	If lvCharacteristics.SelectedItems.Count = 0 Then
		MessageBox.Show("Select characteristic", "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning)
		Return
	End If
	
	Dim Characteristic As wclGattCharacteristic = FCharacteristics(lvCharacteristics.SelectedItems(0).Index)
	
	Dim Str As String = edCharVal.Text
	If Str.Length Mod 2 <> 0 Then
		Str = "0" + Str
	End If
	
	Dim Val(Str.Length \ 2 - 1) As Byte
	For i As Int32 = 0 To Val.Length - 1
		Dim b As String = Str.Substring(i * 2, 2)
		Val(i) = Convert.ToByte(b, 16)
	Next i
	
	Dim Res As Int32 = Client.WriteCharacteristicValue(Characteristic, Val)
	If Res <> wclErrors.WCL_E_SUCCESS Then
		MessageBox.Show("Error: 0x" + Res.ToString("X8"), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error)
	End If
End Sub
void CGattClientDlg::OnBnClickedButtonSetCharValue()
{
	POSITION Pos = lvCharacteristics.GetFirstSelectedItemPosition();
	if (Pos == NULL)
	{
		AfxMessageBox(_T("Select characteristic"));
		return;
	}
	
	wclGattCharacteristics::iterator it = FCharacteristics.begin();
	std::advance(it, lvCharacteristics.GetNextSelectedItem(Pos));
	wclGattCharacteristic Characteristic = (*it);
	
	CString Str;
	edCharValue.GetWindowText(Str);
	if (Str.GetLength() % 2 != 0)
		Str = _T("0") + Str;
	
	wclGattCharacteristicValue Val;
	int i = 0;
	while (i < Str.GetLength())
	{
		unsigned char b = LOBYTE(LOWORD(_tcstol(Str.Mid(i, 2), NULL, 16)));
		Val.push_back(b);
		i+=2;
	}
	
	int Res = wclGattClient.WriteCharacteristicValue(Characteristic, Val);
	if (Res != WCL_E_SUCCESS)
		AfxMessageBox(_T("Error: 0x") + IntToHex(Res));
}


To find more detailed information and code example about reading and writing Bluetooth LE GATT characteristics refer to the GattDemo application from the Bluetooth Framework package.

Reading Descriptors

In addition to the Properties the Characteristic may have Descriptors that are defined attributes that describe a characteristic value. The Descriptor is described by the UUID. Bluetooth SIG defines Descriptors Assigned Numbers for predefined descriptors. The vendors can use custom UUID for the vendor specified Descriptors.

procedure TfmMain.btGetDescriptorsClick(Sender: TObject);
var
	Characteristic: TwclGattCharacteristic;
	Res: Integer;
	i: Integer;
	Item: TListItem;
	Descriptor: TwclGattDescriptor;
begin
	FDescriptors := nil;
	lvDescriptors.Items.Clear;
	
	if lvCharacteristics.Selected = nil then begin
		MessageDlg('Select characteristic', mtWarning, [mbOK], 0);
		Exit;
	end;
	
	Characteristic := FCharacteristics[lvCharacteristics.Selected.Index];
	Res := wclGattClient.ReadDescriptors(Characteristic, OpFlag, FDescriptors);
	if Res <> WCL_E_SUCCESS then begin
		MessageDlg('Error: 0x' + IntToHex(Res, 8), mtError, [mbOK], 0);
		Exit;
	end;
	
	if FDescriptors = nil then
		Exit;
	
	for i := 0 to Length(FDescriptors) - 1 do begin
		Descriptor := FDescriptors[i];
		
		Item := lvDescriptors.Items.Add;
		if Descriptor.Uuid.IsShortUuid then
			Item.Caption := IntToHex(Descriptor.Uuid.ShortUuid, 4)
		else
			Item.Caption := GUIDToString(Descriptor.Uuid.LongUuid);
		Item.SubItems.Add(BoolToStr(Descriptor.Uuid.IsShortUuid, True));
		Item.SubItems.Add(IntToHex(Descriptor.ServiceHandle, 4));
		Item.SubItems.Add(IntToHex(Descriptor.CharacteristicHandle, 4));
		Item.SubItems.Add(IntToHex(Descriptor.Handle, 4));
		case Descriptor.DescriptorType of
			dtCharacteristicExtendedProperties:
				Item.SubItems.Add('dtCharacteristicExtendedProperties');
			dtCharacteristicUserDescription:
				Item.SubItems.Add('dtCharacteristicUserDescription');
			dtClientCharacteristicConfiguration:
				Item.SubItems.Add('dtClientCharacteristicConfiguration');
			dtServerCharacteristicConfiguration:
				Item.SubItems.Add('dtServerCharacteristicConfiguration');
			dtCharacteristicFormat:
				Item.SubItems.Add('dtCharacteristicFormat');
			dtCharacteristicAggregateFormat:
				Item.SubItems.Add('dtCharacteristicAggregateFormat');
			else
				Item.SubItems.Add('dtCustomDescriptor');
		end;
	end;
end;
void __fastcall TfmMain::btGetDescriptorsClick(TObject *Sender)
{
	FDescriptors.Length = 0;
	lvDescriptors->Items->Clear();
	
	if (lvCharacteristics->Selected == NULL)
	{
		MessageDlg("Select characteristic", mtWarning, TMsgDlgButtons() << mbOK, 0);
		return;
	}
	
	TwclGattCharacteristic Characteristic = FCharacteristics[lvCharacteristics->Selected->Index];
	int Res = wclGattClient->ReadDescriptors(Characteristic, OpFlag(), FDescriptors);
	if (Res != WCL_E_SUCCESS)
	{
		MessageDlg("Error: 0x" + IntToHex(Res, 8), mtError, TMsgDlgButtons() << mbOK, 0);
		return;
	}
	
	if (FDescriptors.Length == 0)
		return;
	
	for (int i = 0;  i <  FDescriptors.Length; i++)
	{
		TwclGattDescriptor Descriptor = FDescriptors[i];
		
		TListItem* Item = lvDescriptors->Items->Add();
		if (Descriptor.Uuid.IsShortUuid)
			Item->Caption = IntToHex(Descriptor.Uuid.ShortUuid, 4);
		else
			Item->Caption = Sysutils::GUIDToString(Descriptor.Uuid.LongUuid);
		Item->SubItems->Add(BoolToStr(Descriptor.Uuid.IsShortUuid, true));
		Item->SubItems->Add(IntToHex(Descriptor.ServiceHandle, 4));
		Item->SubItems->Add(IntToHex(Descriptor.CharacteristicHandle, 4));
		Item->SubItems->Add(IntToHex(Descriptor.Handle, 4));
		switch (Descriptor.DescriptorType)
		{
			case dtCharacteristicExtendedProperties:
				Item->SubItems->Add("dtCharacteristicExtendedProperties");
				break;
			case dtCharacteristicUserDescription:
				Item->SubItems->Add("dtCharacteristicUserDescription");
				break;
			case dtClientCharacteristicConfiguration:
				Item->SubItems->Add("dtClientCharacteristicConfiguration");
				break;
			case dtServerCharacteristicConfiguration:
				Item->SubItems->Add("dtServerCharacteristicConfiguration");
				break;
			case dtCharacteristicFormat:
				Item->SubItems->Add("dtCharacteristicFormat");
				break;
			case dtCharacteristicAggregateFormat:
				Item->SubItems->Add("dtCharacteristicAggregateFormat");
				break;
			default:
				Item->SubItems->Add("dtCustomDescriptor");
				break;
		}
	}
}
private void btGetDesc_Click(object sender, EventArgs e)
{
	FDescriptors = null;
	lvDescriptors.Items.Clear();
	
	if (lvCharacteristics.SelectedItems.Count == 0)
	{
		MessageBox.Show("Select characteristic", "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning);
		return;
	}
	
	wclGattCharacteristic Characteristic = FCharacteristics[lvCharacteristics.SelectedItems[0].Index];
	Int32 Res = Client.ReadDescriptors(Characteristic, OpFlag(), out FDescriptors);
	if (Res != wclErrors.WCL_E_SUCCESS)
	{
		MessageBox.Show("Error: 0x" + Res.ToString("X8"), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
		return;
	}
	
	if (FDescriptors == null)
		return;
	
	foreach (wclGattDescriptor Descriptor in FDescriptors)
	{
		String s;
		if (Descriptor.Uuid.IsShortUuid)
			s = Descriptor.Uuid.ShortUuid.ToString("X4");
		else
			s = Descriptor.Uuid.LongUuid.ToString();
		ListViewItem Item = lvDescriptors.Items.Add(s);
		
		Item.SubItems.Add(Descriptor.Uuid.IsShortUuid.ToString());
		Item.SubItems.Add(Descriptor.ServiceHandle.ToString("X4"));
		Item.SubItems.Add(Descriptor.CharacteristicHandle.ToString("X4"));
		Item.SubItems.Add(Descriptor.Handle.ToString("X4"));
		switch (Descriptor.DescriptorType)
		{
			case wclGattDescriptorType.dtCharacteristicExtendedProperties:
				Item.SubItems.Add("dtCharacteristicExtendedProperties");
				break;
			case wclGattDescriptorType.dtCharacteristicUserDescription:
				Item.SubItems.Add("dtCharacteristicUserDescription");
				break;
			case wclGattDescriptorType.dtClientCharacteristicConfiguration:
				Item.SubItems.Add("dtClientCharacteristicConfiguration");
				break;
			case wclGattDescriptorType.dtServerCharacteristicConfiguration:
				Item.SubItems.Add("dtServerCharacteristicConfiguration");
				break;
			case wclGattDescriptorType.dtCharacteristicFormat:
				Item.SubItems.Add("dtCharacteristicFormat");
				break;
			case wclGattDescriptorType.dtCharacteristicAggregateFormat:
				Item.SubItems.Add("dtCharacteristicAggregateFormat");
				break;
			default:
				Item.SubItems.Add("dtCustomDescriptor");
				break;
		}
	}
}
Private Sub btGetDesc_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btGetDesc.Click
	FDescriptors = Nothing
	lvDescriptors.Items.Clear()
	
	If lvCharacteristics.SelectedItems.Count = 0 Then
		MessageBox.Show("Select characteristic", "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning)
		Return
	End If
	
	Dim Characteristic As wclGattCharacteristic = FCharacteristics(lvCharacteristics.SelectedItems(0).Index)
	Dim Res As Int32 = Client.ReadDescriptors(Characteristic, OpFlag(), FDescriptors)
	If Res <> wclErrors.WCL_E_SUCCESS Then
		MessageBox.Show("Error: 0x" + Res.ToString("X8"), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error)
		Return
	End If
	
	If FDescriptors Is Nothing Then
		Return
	End If
	
	For Each Descriptor As wclGattDescriptor In FDescriptors
		Dim s As String
		If Descriptor.Uuid.IsShortUuid Then
			s = Descriptor.Uuid.ShortUuid.ToString("X4")
		Else
			s = Descriptor.Uuid.LongUuid.ToString()
		End If
		Dim Item As ListViewItem = lvDescriptors.Items.Add(s)
		
		Item.SubItems.Add(Descriptor.Uuid.IsShortUuid.ToString())
		Item.SubItems.Add(Descriptor.ServiceHandle.ToString("X4"))
		Item.SubItems.Add(Descriptor.CharacteristicHandle.ToString("X4"))
		Item.SubItems.Add(Descriptor.Handle.ToString("X4"))
		Select Case Descriptor.DescriptorType
			Case wclGattDescriptorType.dtCharacteristicExtendedProperties
				Item.SubItems.Add("dtCharacteristicExtendedProperties")
			Case wclGattDescriptorType.dtCharacteristicUserDescription
				Item.SubItems.Add("dtCharacteristicUserDescription")
			Case wclGattDescriptorType.dtClientCharacteristicConfiguration
				Item.SubItems.Add("dtClientCharacteristicConfiguration")
			Case wclGattDescriptorType.dtServerCharacteristicConfiguration
				Item.SubItems.Add("dtServerCharacteristicConfiguration")
			Case wclGattDescriptorType.dtCharacteristicFormat
				Item.SubItems.Add("dtCharacteristicFormat")
			Case wclGattDescriptorType.dtCharacteristicAggregateFormat
				Item.SubItems.Add("dtCharacteristicAggregateFormat")
			Case Else
				Item.SubItems.Add("dtCustomDescriptor")
		End Select
	Next Descriptor
End Sub
void CGattClientDlg::OnBnClickedButtonGetDescriptors()
{
	FDescriptors.clear();
	lvDescriptors.DeleteAllItems();
	
	POSITION Pos = lvCharacteristics.GetFirstSelectedItemPosition();
	if (Pos == NULL)
	{
		AfxMessageBox(_T("Select characteristic"));
		return;
	}
	
	wclGattCharacteristics::iterator it = FCharacteristics.begin();
	std::advance(it, lvCharacteristics.GetNextSelectedItem(Pos));
	wclGattCharacteristic Characteristic = (*it);
	
	int Res = wclGattClient.ReadDescriptors(Characteristic, OpFlag(), FDescriptors);
	if (Res != WCL_E_SUCCESS)
	{
		AfxMessageBox(_T("Error: 0x") + IntToHex(Res));
		return;
	}
	
	if (FDescriptors.size() == 0)
		return;
	
	for (wclGattDescriptors::iterator i = FDescriptors.begin(); i != FDescriptors.end(); i++)
	{
		wclGattDescriptor Descriptor = (*i);
		
		int Item = lvDescriptors.GetItemCount();
		if (Descriptor.Uuid.IsShortUuid)
			lvDescriptors.InsertItem(Item, IntToHex(Descriptor.Uuid.ShortUuid));
		else
			lvDescriptors.InsertItem(Item, GUIDToString(Descriptor.Uuid.LongUuid));
		lvDescriptors.SetItemText(Item, 1, BoolToStr(Descriptor.Uuid.IsShortUuid));
		lvDescriptors.SetItemText(Item, 2, IntToHex(Descriptor.ServiceHandle));
		lvDescriptors.SetItemText(Item, 3, IntToHex(Descriptor.CharacteristicHandle));
		lvDescriptors.SetItemText(Item, 4, IntToHex(Descriptor.Handle));
		switch (Descriptor.DescriptorType)
		{
			case dtCharacteristicExtendedProperties:
				lvDescriptors.SetItemText(Item, 5, _T("dtCharacteristicExtendedProperties"));
				break;
			case dtCharacteristicUserDescription:
				lvDescriptors.SetItemText(Item, 5, _T("dtCharacteristicUserDescription"));
				break;
			case dtClientCharacteristicConfiguration:
				lvDescriptors.SetItemText(Item, 5, _T("dtClientCharacteristicConfiguration"));
				break;
			case dtServerCharacteristicConfiguration:
				lvDescriptors.SetItemText(Item, 5, _T("dtServerCharacteristicConfiguration"));
				break;
			case dtCharacteristicFormat:
				lvDescriptors.SetItemText(Item, 5, _T("dtCharacteristicFormat"));
				break;
			case dtCharacteristicAggregateFormat:
				lvDescriptors.SetItemText(Item, 5, _T("dtCharacteristicAggregateFormat"));
				break;
			default:
				lvDescriptors.SetItemText(Item, 5, _T("dtCustomDescriptor"));
				break;
		}
	}
}


To find more detailed information and code example about reading Bluetooth LE GATT descriptors refer to the GattDemo application from the Bluetooth Framework package.

Characteristic Value Change Notifications

If the Characteristic has either IsNotifiable or IsIndicatable property set to True the Characteristic can send notifications to the application when the Characteristic's value changed. Bluetooth Framework supports both notification types.

To subscribe for the Characteristic's changes the application must call the Subscribe method first (that should be done for each Characteristic the application is interested in). After that to start receiving notification an application must write the Client Configuration Descriptor for the selected characteristic. To make it simple Bluetooth Framework includes the WriteClientConfiguration method that does all the things to write the descriptor. Once the notification has been received the OnCharacteristicChanged even fires. The new value of the Characteristic is passed to the event handler as a Raw Bytes Array. The application can detect which Characteristic has been changed (if it is supscribed to more than single Characteristic) by Characteristic's Handle that is also passed to the event handler.

The code snippets below show how to:

  • Subscribe to the Characterstic change notifications
  • Handle the OnCharacteristicChanged event
  • Unsubscribe from the Characterstic change notifications
procedure TfmMain.btSubscribeClick(Sender: TObject);
var
	Characteristic: TwclGattCharacteristic;
	Res: Integer;
begin
	if lvCharacteristics.Selected = nil then begin
		MessageDlg('Select characteristic', mtWarning, [mbOK], 0);
		Exit;
	end;
	
	Characteristic := FCharacteristics[lvCharacteristics.Selected.Index];
	
	Res := wclGattClient.Subscribe(Characteristic);
	if Res <> WCL_E_SUCCESS then
		MessageDlg('Error: 0x' + IntToHex(Res, 8), mtError, [mbOK], 0);
end;

procedure TfmMain.btCCCDSubscribeClick(Sender: TObject);
var
	Characteristic: TwclGattCharacteristic;
	Res: Integer;
begin
	if lvCharacteristics.Selected = nil then begin
		MessageDlg('Select characteristic', mtWarning, [mbOK], 0);
		Exit;
	end;
	
	Characteristic := FCharacteristics[lvCharacteristics.Selected.Index];
	
	Res := wclGattClient.WriteClientConfiguration(Characteristic, True, OpFlag);
	if Res <> WCL_E_SUCCESS then
		MessageDlg('Error: 0x' + IntToHex(Res, 8), mtError, [mbOK], 0);
end;

procedure TfmMain.wclGattClientCharacteristicChanged(Sender: TObject; const Handle: Word; const Value: TwclGattCharacteristicValue);
var
	Str: string;
	i: Integer;
begin
	TraceEvent(TwclGattClient(Sender).Address, 'ValueChanged', 'Handle', IntToHex(Handle, 4));
	if Value = nil then
		TraceEvent(0, '', 'Value', '')
	else
		if Length(Value) = 0 then
			TraceEvent(0, '', 'Value', '')
		else begin
			Str := '';
			
			for i := Low(Value) to High(Value) do
				Str := Str + IntToHex(Value[i], 2);
				
			TraceEvent(0, '', 'Value', Str);
		end;
end;

procedure TfmMain.btCCCDUnsubscribeClick(Sender: TObject);
var
	Characteristic: TwclGattCharacteristic;
	Res: Integer;
begin
	if lvCharacteristics.Selected = nil then begin
		MessageDlg('Select characteristic', mtWarning, [mbOK], 0);
		Exit;
	end;
	
	Characteristic := FCharacteristics[lvCharacteristics.Selected.Index];
	
	Res := wclGattClient.WriteClientConfiguration(Characteristic, False, OpFlag);
	if Res <> WCL_E_SUCCESS then
		MessageDlg('Error: 0x' + IntToHex(Res, 8), mtError, [mbOK], 0);
end;

procedure TfmMain.btUnsubscribeClick(Sender: TObject);
var
	Characteristic: TwclGattCharacteristic;
	Res: Integer;
begin
	if lvCharacteristics.Selected = nil then begin
		MessageDlg('Select characteristic', mtWarning, [mbOK], 0);
		Exit;
	end;
	
	Characteristic := FCharacteristics[lvCharacteristics.Selected.Index];
	
	Res := wclGattClient.Unsubscribe(Characteristic);
	if Res <> WCL_E_SUCCESS then
		MessageDlg('Error: 0x' + IntToHex(Res, 8), mtError, [mbOK], 0);
end;
void __fastcall TfmMain::btSubscribeClick(TObject *Sender)
{
	if (lvCharacteristics->Selected == NULL)
	{
		MessageDlg("Select characteristic", mtWarning, TMsgDlgButtons() << mbOK, 0);
		return;
	}
	
	TwclGattCharacteristic Characteristic = FCharacteristics[lvCharacteristics->Selected->Index];
	
	int Res = wclGattClient->Subscribe(Characteristic);
	if (Res != WCL_E_SUCCESS)
		MessageDlg("Error: 0x" + IntToHex(Res, 8), mtError, TMsgDlgButtons() << mbOK, 0);
}

void __fastcall TfmMain::btCCCDSubscribeClick(TObject *Sender)
{
	if (lvCharacteristics->Selected == NULL)
	{
		MessageDlg("Select characteristic", mtWarning, TMsgDlgButtons() << mbOK, 0);
		return;
	}
	
	TwclGattCharacteristic Characteristic = FCharacteristics[lvCharacteristics->Selected->Index];
	
	int Res = wclGattClient->WriteClientConfiguration(Characteristic, true, OpFlag());
	if (Res != WCL_E_SUCCESS)
		MessageDlg("Error: 0x" + IntToHex(Res, 8), mtError, TMsgDlgButtons() << mbOK, 0);
}

void __fastcall TfmMain::wclGattClientCharacteristicChanged(TObject *Sender, const WORD Handle, const TwclGattCharacteristicValue Value)
{
	TraceEvent(((TwclGattClient*)Sender)->Address, "ValueChanged", "Handle", IntToStr(Handle));
	if (Value.Length == 0)
		TraceEvent(0, "", "Value", "");
	else
	{
		String Str = "";
		
		for (int i = 0;  i <  Value.Length; i++)
			Str = Str + IntToHex(Value[i], 2);
		
		TraceEvent(0, "", "Value", Str);
	}
}

void __fastcall TfmMain::btCCCDUnsubscribeClick(TObject *Sender)
{
	if (lvCharacteristics->Selected == NULL)
	{
		MessageDlg("Select characteristic", mtWarning, TMsgDlgButtons() << mbOK, 0);
		return;
	}
	
	TwclGattCharacteristic Characteristic = FCharacteristics[lvCharacteristics->Selected->Index];
	
	int Res = wclGattClient->WriteClientConfiguration(Characteristic, false, OpFlag());
	if (Res != WCL_E_SUCCESS)
		MessageDlg("Error: 0x" + IntToHex(Res, 8), mtError, TMsgDlgButtons() << mbOK, 0);
}

void __fastcall TfmMain::btUnsubscribeClick(TObject *Sender)
{
	if (lvCharacteristics->Selected == NULL)
	{
		MessageDlg("Select characteristic", mtWarning, TMsgDlgButtons() << mbOK, 0);
		return;
	}
	
	TwclGattCharacteristic Characteristic = FCharacteristics[lvCharacteristics->Selected->Index];
	
	int Res = wclGattClient->Unsubscribe(Characteristic);
	if (Res != WCL_E_SUCCESS)
		MessageDlg("Error: 0x" + IntToHex(Res, 8), mtError, TMsgDlgButtons() << mbOK, 0);
}
private void btSubscribe_Click(object sender, EventArgs e)
{
	if (lvCharacteristics.SelectedItems.Count == 0)
	{
		MessageBox.Show("Select characteristic", "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning);
		return;
	}
	
	wclGattCharacteristic Characteristic = FCharacteristics[lvCharacteristics.SelectedItems[0].Index];
	
	Int32 Res = Client.Subscribe(Characteristic);
	if (Res != wclErrors.WCL_E_SUCCESS)
		MessageBox.Show("Error: 0x" + Res.ToString("X8"), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
}

private void btWriteSubsc_Click(object sender, EventArgs e)
{
	if (lvCharacteristics.SelectedItems.Count == 0)
	{
		MessageBox.Show("Select characteristic", "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning);
		return;
	}
	
	wclGattCharacteristic Characteristic = FCharacteristics[lvCharacteristics.SelectedItems[0].Index];
	
	Int32 Res = Client.WriteClientConfiguration(Characteristic, true, OpFlag());
	if (Res != wclErrors.WCL_E_SUCCESS)
		MessageBox.Show("Error: 0x" + Res.ToString("X8"), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
}

void Client_OnCharacteristicChanged(object Sender, ushort Handle, byte[] Value)
{
	TraceEvent(((wclGattClient)Sender).Address, "ValueChanged", "Handle", Handle.ToString("X4"));
	if (Value == null)
		TraceEvent(0, "", "Value", "");
	else
		if (Value.Length == 0)
			TraceEvent(0, "", "Value", "");
		else
		{
			String Str = "";
			
			for (Int32 i = 0;  i <  Value.Length; i++)
				Str = Str + Value[i].ToString("X2");
			
			TraceEvent(0, "", "Value", Str);
		}
}

private void btWriteUnsubsc_Click(object sender, EventArgs e)
{
	if (lvCharacteristics.SelectedItems.Count == 0)
	{
		MessageBox.Show("Select characteristic", "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning);
		return;
	}
	
	wclGattCharacteristic Characteristic = FCharacteristics[lvCharacteristics.SelectedItems[0].Index];
	
	Int32 Res = Client.WriteClientConfiguration(Characteristic, false, OpFlag());
	if (Res != wclErrors.WCL_E_SUCCESS)
		MessageBox.Show("Error: 0x" + Res.ToString("X8"), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
}

private void btUnsubscribe_Click(object sender, EventArgs e)
{
	if (lvCharacteristics.SelectedItems.Count == 0)
	{
		MessageBox.Show("Select characteristic", "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning);
		return;
	}
	
	wclGattCharacteristic Characteristic = FCharacteristics[lvCharacteristics.SelectedItems[0].Index];
	
	Int32 Res = Client.Unsubscribe(Characteristic);
	if (Res != wclErrors.WCL_E_SUCCESS)
		MessageBox.Show("Error: 0x" + Res.ToString("X8"), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
Private Sub btSubscribe_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btSubscribe.Click
	If lvCharacteristics.SelectedItems.Count = 0 Then
		MessageBox.Show("Select characteristic", "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning)
		Return
	End If
	
	Dim Characteristic As wclGattCharacteristic = FCharacteristics(lvCharacteristics.SelectedItems(0).Index)
	
	Dim Res As Int32 = Client.Subscribe(Characteristic)
	If Res <> wclErrors.WCL_E_SUCCESS Then
		MessageBox.Show("Error: 0x" + Res.ToString("X8"), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error)
	End If
End Sub

Private Sub btWriteSubsc_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btWriteSubsc.Click
	If lvCharacteristics.SelectedItems.Count = 0 Then
		MessageBox.Show("Select characteristic", "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning)
		Return
	End If
	
	Dim Characteristic As wclGattCharacteristic = FCharacteristics(lvCharacteristics.SelectedItems(0).Index)
	
	Dim Res As Int32 = Client.WriteClientConfiguration(Characteristic, True, OpFlag())
	If Res <> wclErrors.WCL_E_SUCCESS Then
		MessageBox.Show("Error: 0x" + Res.ToString("X8"), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error)
	End If
End Sub

Private Sub Client_OnCharacteristicChanged(ByVal Sender As Object, ByVal Handle As UShort, ByVal Value() As Byte) Handles Client.OnCharacteristicChanged
	TraceEvent(CType(Sender, wclGattClient).Address, "ValueChanged", "Handle", Handle.ToString("X4"))
	If Value Is Nothing Then
		TraceEvent(0, "", "Value", "")
	Else
		If Value.Length = 0 Then
			TraceEvent(0, "", "Value", "")
		Else
			Dim Str As String = ""
			
			Dim i As Int32
			For i = 0 To Value.Length - 1
				Str = Str + Value(i).ToString("X2")
			Next i
			
			TraceEvent(0, "", "Value", Str)
		End If
	End If
End Sub

Private Sub btWriteUnsubsc_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btWriteUnsubsc.Click
	If lvCharacteristics.SelectedItems.Count = 0 Then
		MessageBox.Show("Select characteristic", "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning)
		Return
	End If
	
	Dim Characteristic As wclGattCharacteristic = FCharacteristics(lvCharacteristics.SelectedItems(0).Index)
	
	Dim Res As Int32 = Client.WriteClientConfiguration(Characteristic, False, OpFlag())
	If Res <> wclErrors.WCL_E_SUCCESS Then
		MessageBox.Show("Error: 0x" + Res.ToString("X8"), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error)
	End If
End Sub

Private Sub btUnsubscribe_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btUnsubscribe.Click
	If lvCharacteristics.SelectedItems.Count = 0 Then
		MessageBox.Show("Select characteristic", "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning)
		Return
	End If
	
	Dim Characteristic As wclGattCharacteristic = FCharacteristics(lvCharacteristics.SelectedItems(0).Index)
	
	Dim Res As Int32 = Client.Unsubscribe(Characteristic)
	If Res <> wclErrors.WCL_E_SUCCESS Then
		MessageBox.Show("Error: 0x" + Res.ToString("X8"), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error)
	End If
End Sub
void CGattClientDlg::OnBnClickedButtonSubscribe()
{
	POSITION Pos = lvCharacteristics.GetFirstSelectedItemPosition();
	if (Pos == NULL)
	{
		AfxMessageBox(_T("Select characteristic"));
		return;
	}
	
	wclGattCharacteristics::iterator it = FCharacteristics.begin();
	std::advance(it, lvCharacteristics.GetNextSelectedItem(Pos));
	wclGattCharacteristic Characteristic = (*it);
	
	int Res = wclGattClient.Subscribe(Characteristic);
	if (Res != WCL_E_SUCCESS)
		AfxMessageBox(_T("Error: 0x") + IntToHex(Res));
}

void CGattClientDlg::OnBnClickedButtonWriteCccdSubscribe()
{
	POSITION Pos = lvCharacteristics.GetFirstSelectedItemPosition();
	if (Pos == NULL)
	{
		AfxMessageBox(_T("Select characteristic"));
		return;
	}
	
	wclGattCharacteristics::iterator it = FCharacteristics.begin();
	std::advance(it, lvCharacteristics.GetNextSelectedItem(Pos));
	wclGattCharacteristic Characteristic = (*it);
	
	int Res = wclGattClient.WriteClientConfiguration(Characteristic, true, OpFlag());
	if (Res != WCL_E_SUCCESS)
		AfxMessageBox(_T("Error: 0x") + IntToHex(Res));
}

void CGattClientDlg::wclGattClientCharacteristicChanged(void* Sender, unsigned short Handle, wclGattCharacteristicValue& Value)
{
	TraceEvent(((CwclGattClient*)Sender)->GetAddress(), _T("ValueChanged"), _T("Handle"), IntToHex(Handle));
	if (Value.size() == 0)
		TraceEvent(0, _T(""), _T("Value"), _T(""));
	else
	{
		CString Str = _T("");
		for (wclGattCharacteristicValue::iterator i = Value.begin(); i != Value.end(); i++)
			Str = Str + IntToHex((*i));
		
		TraceEvent(0, _T(""), _T("Value"), Str);
	}
}

void CGattClientDlg::OnBnClickedButtonWriteCccdUnsubscribe()
{
	POSITION Pos = lvCharacteristics.GetFirstSelectedItemPosition();
	if (Pos == NULL)
	{
		AfxMessageBox(_T("Select characteristic"));
		return;
	}
	
	wclGattCharacteristics::iterator it = FCharacteristics.begin();
	std::advance(it, lvCharacteristics.GetNextSelectedItem(Pos));
	wclGattCharacteristic Characteristic = (*it);
	
	int Res = wclGattClient.WriteClientConfiguration(Characteristic, false, OpFlag());
	if (Res != WCL_E_SUCCESS)
		AfxMessageBox(_T("Error: 0x") + IntToHex(Res));
}

void CGattClientDlg::OnBnClickedButtonUnsubscribe()
{
	POSITION Pos = lvCharacteristics.GetFirstSelectedItemPosition();
	if (Pos == NULL)
	{
		AfxMessageBox(_T("Select characteristic"));
		return;
	}
	
	wclGattCharacteristics::iterator it = FCharacteristics.begin();
	std::advance(it, lvCharacteristics.GetNextSelectedItem(Pos));
	wclGattCharacteristic Characteristic = (*it);
	
	int Res = wclGattClient.Unsubscribe(Characteristic);
	if (Res != WCL_E_SUCCESS)
		AfxMessageBox(_T("Error: 0x") + IntToHex(Res));
}


To find more detailed information and code example about subscribing to the Characteristic change notifications refer to the GattDemo application from the Bluetooth Framework package.

Download Demo Application

The Bluetooth Framework package includes the GattClient sample application that shows how to use the wclGattClient class. To download Bluetooth Framework package visit the Bluetooth Framework page.

Should you have any questions Contact us.