BLE GATT Communication


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 connect to Bluetooth Low Energy peripheral devices using the Bluetooth Framework.

Table Of Contents


Generic Attributes

GATT Profile Diagram The Generic Attributes (GATT) defines 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 the hierarchy of services, characteristics, and other 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 or configuration flags relating to the owning characteristic.

GATT defines a client (BLE Central) and a server (BLE Peripheral) role. 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.

The Bluetooth Framework supports the GATT client role with BlueSoleil, Microsoft Bluetooth drivers, and the BLED112 USB Bluetooth dongle. BlueSoleil Bluetooth drivers and the BLED112 dongle allow the use of Bluetooth LE GATT features on any Windows platform starting from Windows XP. The Microsoft Bluetooth driver support the 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.

The Bluetooth Framework supports both types of the UUIDs through the 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: the public and the random device address.

  • Public Device Address
    A 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
    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
    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 Address
    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. A non-resolvable private address is the address that is random and cannot be "expected". A possible use case: a device that already communicated a non-resolvable address to a peer for a reconnection.

The Bluetooth Framework supports any type of Bluetooth LE device address. However, there is no way to resolve the Resolvable Private Address to a real device MAC at the moment.

Bluetooth Radio Object

To be able to communicate with a Bluetooth LE device, an application must get the currently available Bluetooth Radio object. The Bluetooth radio object represents a Bluetooth hardware device connected to your computer. It can be a built-in or external (USB, serial) Bluetooth adapter that works with BlueSoleil and Microsoft Bluetooth drivers or with the Silicon Labs BLED112 USB Bluetooth dongle (the 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 a good idea to place the code into your application 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: The Bluetooth Framework always detects Microsoft Bluetooth radio even if there are other drivers installed. Your application must not destroy the Bluetooth Radio Object returned by the wclBluetoothManager class.



  function TfmMain.GetRadio: TwclBluetoothRadio;
  var
      Res: Integer;
      Radio: TwclBluetoothRadio;
  begin
      Res := wclBluetoothManager.GetLeRadio(Radio);
      if Res <> WCL_E_SUCCESS then begin
          MessageDlg('Get working radio failed: 0x' + IntToHex(Res, 8), mtError, [mbOK], 0);
          Result := nil;
      end else
          Result := Radio;
  end;
                        

  TwclBluetoothRadio* __fastcall TfmMain::GetRadio()
  {
      TwclBluetoothRadio* Radio;
      int Res = wclBluetoothManager->GetLeRadio(Radio);
      if (Res != WCL_E_SUCCESS)
      {
          MessageDlg("Get working radio failed: 0x" + IntToHex(Res, 8), mtError, TMsgDlgButtons() << mbOK, 0);
          return NULL;
      }
      return Radio;
  }
                        

  private wclBluetoothRadio GetRadio()
  {
      wclBluetoothRadio Radio;
      Int32 Res = Manager.GetLeRadio(out Radio);
      if (Res != wclErrors.WCL_E_SUCCESS)
      {
          MessageBox.Show("Get working radio failed: 0x" + Res.ToString("X8"));
          return null;
      }
      return Radio;
  }
                        

  Private Function GetRadio() As wclBluetoothRadio
      Dim Radio As wclBluetoothRadio = Nothing
      Dim Res As Int32 = Manager.GetLeRadio(Radio)
      If Res <> wclErrors.WCL_E_SUCCESS Then
          MessageBox.Show("Get working radio failed: 0x" + Res.ToString("X8"))
          Return Nothing
      End If
      Return Radio
  End Function
                        

  CwclBluetoothRadio* CGattClientDlg::GetRadio()
  {
      CwclBluetoothRadio* Radio;
      int Res = wclBluetoothManager.GetLeRadio(Radio);
      if (Res != WCL_E_SUCCESS)
      {
          AfxMessageBox(_T("Get working radio failed"));
          return NULL;
      }
      return Radio;
  }
                        

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 is available with BlueSoleil Bluetooth drivers and with the BLED112 dongle on any Windows platform. With the Microsoft Bluetooth driver:

  • Windows 8
    You must discover and pair with a Bluetooth LE device through Windows UI. After the device is paired, you can discover it using in-app discovering methods.
  • Windows 10 1607 and below
    You also have to pair with a Bluetooth LE device through Windows UI. After the device is paired, you can discover it using in-app discovering methods.
  • Windows 10 1703 and above
    You do not need to pair with your device manually. You can discover nearby Bluetooth LE devices using in-app discovering.

To discover nearby Bluetooth LE devices, call the Discover method of the wclBluetoothRadio class.

NOTE: It is much better to use the wclBluetoothLeBeaconWatcher class to find nearby Bluetooth LE device. Please refer to the Beacons sample application to find how you can detect nearby Bluetooth LE devices using the wclBluetoothLeBeaconWatcher class.



  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 begin
          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;
  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 begin
          // 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;
  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"));
                  break;
          }
      }
  }
  
  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, it is better if you read it after the discovery is completed. The OnDiscoveringCompleted event handler is a good place to do that (as it is shown in the code above).

Connecting To And Disconnect From BLE Devices

After discovering Bluetooth Low Energy devices, you can connect to them. 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: The connection procedure is asynchronous. The call to the Connect method of the wclGattClient class simply starts the connection procedure. After the connection is established (with or without success), the OnConnect event is called with the real connection result code.

In case the connection was established with success (the Error parameter passed to the OnConnect event handler is WCL_E_SUCCESS) the wclGattClient stays in the csConnected state, and you can read the device's GATT services, characteristics, and other attributes. You can also change the Bluetooth LE connection parameters.

If the connection was not successful, the wclGattClient switches back to the csDisconnected state, and your application can try to connect to it once again or try to connect to another BLE device.

To disconnect from the connected BLE device, call the Disconnect method of the wclGattClient class. After disconnecting, the OnDisconnect event is called. The event is also called 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
          try
              Item := lvDevices.Selected;
              wclGattClient.Address := StrToInt64('$' + Item.Caption);
              wclGattClient.ConnectOnRead := cbConnectOnRead.Checked;
              Res := wclGattClient.Connect(TwclBluetoothRadio(Item.Data));
              if Res <> WCL_E_SUCCESS then
                  MessageDlg('Error: 0x' + IntToHex(Res, 8), mtError, [mbOK], 0);
          except
              on E: Exception do
                  MessageDlg(E.Message, mtError, [mbOK], 0);
          end;
      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;
          try
          {
              wclGattClient->Address = StrToInt64("$" + Item->Caption);
              wclGattClient->ConnectOnRead = cbConnectOnRead->Checked;
              int Res = wclGattClient->Connect((TwclBluetoothRadio*)Item->Data);
              if (Res != WCL_E_SUCCESS)
                  MessageDlg("Error: 0x" + IntToHex(Res, 8), mtError, TMsgDlgButtons() << mbOK, 0);
          }
          catch(const Exception& e)
          {
              MessageDlg(e.Message, 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];
          try
          {
              Client.Address = Convert.ToInt64(Item.Text, 16);
              Client.ConnectOnRead = cbConnectOnRead.Checked;
              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);
          }
          catch (Exception ex)
          {
              MessageBox.Show(ex.Message, "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)
          Try
              Client.Address = Convert.ToInt64(Item.Text, 16)
              Client.ConnectOnRead = cbConnectOnRead.Checked
              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
          Catch ex As Exception
              MessageBox.Show(ex.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error)
          End Try
      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);
          try
          {
              wclGattClient.SetAddress(StrToInt64(lvDevices.GetItemText(Item, 0)));
              wclGattClient.SetConnectOnRead(cbConnectOnRead.GetCheck() != FALSE);
              int Res = wclGattClient.Connect((CwclBluetoothRadio*)lvDevices.GetItemData(Item));
              if (Res != WCL_E_SUCCESS)
                  AfxMessageBox(_T("Error: 0x") + IntToHex(Res));
          }
          catch(wclException* ex)
          {
              MessageBoxA(NULL, ex->what(), "ERROR", 0);
          }
      }
  }
  
  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));
  }
                        

Reading Services

Once a connection to the Bluetooth Low Energy device has been established, you can read the device's 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 a UUID that uniquely identifies the services. To read GATT services, the Bluetooth Framework provides the ReadServices method of the wclGattClient class. NOTE: services reading is a synchronous 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));
      }
  }
                        

Reading Characteristics

Once you read the services, you can read the 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 is described via a pre-defined 16-bit or 128-bit UUID.

Characteristics are the main point that you will interact with your BLE peripheral, so it is 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 characteristics.

Each characteristic, in addition to the UUID, has a few properties that describe the characteristic's "features". The Bluetooth Framework provides all the characteristic's information through the wclGattCharacteristic structure. Below are the descriptions of the important characteristic's properties:

  • IsBroadcastable
    Value of true indicates that the characteristic can be broadcast.
  • IsReadable
    Value of true indicates that the characteristic can be read.
  • IsWritable
    Value of true indicates that the characteristic can be written to.
  • IsWritableWithoutResponse
    Value of true indicates that the characteristic can be written to without requiring a response.
  • IsSignedWritable
    Value of true indicates that the characteristic can be signed writable.
  • IsNotifiable
    Value of true indicates that the characteristic can be updated by the device through value notifications, and the new value will be returned through the event.
  • IsIndicatable
    Value of true indicates that the characteristic can be updated by the device through value indications, and the new value will be returned through the event.
  • HasExtendedProperties
    Value of 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));
      }
  }
                        

Reading And Writing Characteristic Value

If a characteristic has the IsReadable property set to true, an application can read the characteristic's value. The 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 a characteristic has either the 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));
  }
                        

Reading Descriptors

In addition to the properties, a 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 a 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;
          }
      }
  }
                        

Characteristic Value Change Notifications

If a characteristic has either the IsNotifiable or IsIndicatable property set to true, the characteristic can send notifications to the application when the characteristic's value changes. The Bluetooth Framework supports both notification types.


What Are Notifications And Indications

Indications and notifications are commands that could be sent through the attribute (ATT) protocol. So, there are two roles defined at the ATT layer:

  • Client devices access remote resources over a BLE link using the GATT protocol. It is also known as Central.
  • Server devices have the GATT database, access control methods, and provide resources to the remote client. This is also known as Peripheral.

BLE standard defines two ways to transfer data from the server to the client: notification and indication. Notifications and indications are initiated by the server but enabled by the client.

Notifications do not need to be acknowledged, so they are faster. Hence, the server does not know if the message reaches the client. Indications need to be acknowledged to be communicated. The client sent a confirmation message back to the server; this way, the server knows that the message reached the client. One interesting thing defined by the ATT protocol is that a server can't send two consecutive indications if the confirmation was not received. In other words, the server has to wait for the confirmation of each indication in order to send the next indication. That is why indications are slower than notifications.


How To Subsribe To And Unsubscribe From Value Change Notifications

To subscribe to 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 notifications, an application must write the Client Configuration Descriptor for the selected characteristic. To make it simple, the Bluetooth Framework includes the WriteClientConfiguration method that does all the things to write the descriptor. Once the notification has been received, the OnCharacteristicChanged event is called. 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 subscribed to more than one characteristic) by the characteristic's Handle that is also passed to the event handler. The code snippets below show how to subscribe to the characteristic change notifications, handle the OnCharacteristicChanged event, and unsubscribe from the characteristic change notifications.

The Bluetooth Framework also provides methods for fast characteristic changes notification subscribing (SubscribeForNotifications) and unsubscribing (UnsubscribeFromNotifications). These methods internally call the Subscribe and WriteClientConfiguration methods when needed.

NOTE: DFRobot Bluno boards have a bug in the Client Configuration Descriptor implementation, and the common subscribing method will not work with such boards. To be able to receive notifications from such boards, an application must set the ForceNotifications property of the wclGattClient class to true.



  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 begin
          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;
  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));
  }
                        


Why Do I Get 0x000510B6 Error Code

For some characteristics, calling the Subscribe, Unsubscribe, and WriteClientConfiguration methods may return the 0x000510B6 (WCL_E_BLUETOOTH_LE_INVALID_CHARACTERISTIC_CONFIGURATION) error code. The error means that the characteristic has both IsIndicatable and IsNotifiable properties set to true. You must select one of them by setting the other to false.

If you are interested in receiving notifications, then set the IsIndicatable property to false. If you are interested in receiving indications, then set the IsNotifiable property to false. Below you can find the same code as above but with characteristic properties checked.



  procedure TfmMain.btSubscribeClick(Sender: TObject);
  var
      Characteristic: TwclGattCharacteristic;
      Res: Integer;
  begin
      ...
      Characteristic := FCharacteristics[lvCharacteristics.Selected.Index];
      // Let say we prefer notifications in case if both properties set to True.
      if Characteristic.IsIndicatable and Characteristic.IsNotifiable then
          Characteristic.IsIndicatable := False;
      Res := wclGattClient.Subscribe(Characteristic);
      ...
  end;
  
  procedure TfmMain.btCCCDSubscribeClick(Sender: TObject);
  var
      Characteristic: TwclGattCharacteristic;
      Res: Integer;
  begin
      ...
      Characteristic := FCharacteristics[lvCharacteristics.Selected.Index];
      // Let say we prefer notifications in case if both properties set to True.
      if Characteristic.IsIndicatable and Characteristic.IsNotifiable then
          Characteristic.IsIndicatable := False;
      Res := wclGattClient.WriteClientConfiguration(Characteristic, True, OpFlag);
      ...
  end;
  
  procedure TfmMain.btCCCDUnsubscribeClick(Sender: TObject);
  var
      Characteristic: TwclGattCharacteristic;
      Res: Integer;
  begin
      ...
      Characteristic := FCharacteristics[lvCharacteristics.Selected.Index];
      // Let say we prefer notifications in case if both properties set to True.
      if Characteristic.IsIndicatable and Characteristic.IsNotifiable then
          Characteristic.IsIndicatable := False;
      Res := wclGattClient.WriteClientConfiguration(Characteristic, False, OpFlag);
      ...
  end;
  
  procedure TfmMain.btUnsubscribeClick(Sender: TObject);
  var
      Characteristic: TwclGattCharacteristic;
      Res: Integer;
  begin
      ...
      Characteristic := FCharacteristics[lvCharacteristics.Selected.Index];
      // Let say we prefer notifications in case if both properties set to True.
      if Characteristic.IsIndicatable and Characteristic.IsNotifiable then
          Characteristic.IsIndicatable := False;
      Res := wclGattClient.Unsubscribe(Characteristic);
      ...
  end;
                        

  void __fastcall TfmMain::btSubscribeClick(TObject *Sender)
  {
      ...
      TwclGattCharacteristic Characteristic = FCharacteristics[lvCharacteristics->Selected->Index];
      // Let say we prefer notifications in case if both properties set to True.
      if (Characteristic.IsIndicatable && Characteristic.IsNotifiable)
          Characteristic.IsIndicatable = false;
      int Res = wclGattClient->Subscribe(Characteristic);
      ...
  }
  
  void __fastcall TfmMain::btCCCDSubscribeClick(TObject *Sender)
  {
      ...
      TwclGattCharacteristic Characteristic = FCharacteristics[lvCharacteristics->Selected->Index];
      // Let say we prefer notifications in case if both properties set to True.
      if (Characteristic.IsIndicatable && Characteristic.IsNotifiable)
          Characteristic.IsIndicatable = false;
      int Res = wclGattClient->WriteClientConfiguration(Characteristic, true, OpFlag());
      ...
  }
  
  void __fastcall TfmMain::btCCCDUnsubscribeClick(TObject *Sender)
  {
      ...
      TwclGattCharacteristic Characteristic = FCharacteristics[lvCharacteristics->Selected->Index];
      // Let say we prefer notifications in case if both properties set to True.
      if (Characteristic.IsIndicatable && Characteristic.IsNotifiable)
          Characteristic.IsIndicatable = false;
      int Res = wclGattClient->WriteClientConfiguration(Characteristic, false, OpFlag());
      ...
  }
  
  void __fastcall TfmMain::btUnsubscribeClick(TObject *Sender)
  {
      ...
      TwclGattCharacteristic Characteristic = FCharacteristics[lvCharacteristics->Selected->Index];
      // Let say we prefer notifications in case if both properties set to True.
      if (Characteristic.IsIndicatable && Characteristic.IsNotifiable)
          Characteristic.IsIndicatable = false;
      int Res = wclGattClient->Unsubscribe(Characteristic);
      ...
  }
                        

  private void btSubscribe_Click(object sender, EventArgs e)
  {
      ...
      wclGattCharacteristic Characteristic = FCharacteristics[lvCharacteristics.SelectedItems[0].Index];
      // Let say we prefer notifications in case if both properties set to True.
      if (Characteristic.IsIndicatable && Characteristic.IsNotifiable)
          Characteristic.IsIndicatable = false;
      Int32 Res = Client.Subscribe(Characteristic);
      ...
  }
  
  private void btWriteSubsc_Click(object sender, EventArgs e)
  {
      ...
      wclGattCharacteristic Characteristic = FCharacteristics[lvCharacteristics.SelectedItems[0].Index];
      // Let say we prefer notifications in case if both properties set to True.
      if (Characteristic.IsIndicatable && Characteristic.IsNotifiable)
          Characteristic.IsIndicatable = false;
      Int32 Res = Client.WriteClientConfiguration(Characteristic, true, OpFlag());
      ...
  }
  
  private void btWriteUnsubsc_Click(object sender, EventArgs e)
  {
      ...
      wclGattCharacteristic Characteristic = FCharacteristics[lvCharacteristics.SelectedItems[0].Index];
      // Let say we prefer notifications in case if both properties set to True.
      if (Characteristic.IsIndicatable && Characteristic.IsNotifiable)
          Characteristic.IsIndicatable = false;
      Int32 Res = Client.WriteClientConfiguration(Characteristic, false, OpFlag());
      ...
  }
  
  private void btUnsubscribe_Click(object sender, EventArgs e)
  {
      ...
      wclGattCharacteristic Characteristic = FCharacteristics[lvCharacteristics.SelectedItems[0].Index];
      // Let say we prefer notifications in case if both properties set to True.
      if (Characteristic.IsIndicatable && Characteristic.IsNotifiable)
          Characteristic.IsIndicatable = false;
      Int32 Res = Client.Unsubscribe(Characteristic);
      ...
  }
                        

  Private Sub btSubscribe_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btSubscribe.Click
      ...
      Dim Characteristic As wclGattCharacteristic = FCharacteristics(lvCharacteristics.SelectedItems(0).Index)
      ' Let say we prefer notifications in case if both properties set to True.
      If Characteristic.IsIndicatable And Characteristic.IsNotifiable Then
          Characteristic.IsIndicatable = False
      End If
      Dim Res As Int32 = Client.Subscribe(Characteristic)
      ...
  End Sub
  
  Private Sub btWriteSubsc_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btWriteSubsc.Click
      ...
      Dim Characteristic As wclGattCharacteristic = FCharacteristics(lvCharacteristics.SelectedItems(0).Index)
      ' Let say we prefer notifications in case if both properties set to True.
      If Characteristic.IsIndicatable And Characteristic.IsNotifiable Then
          Characteristic.IsIndicatable = False
      End If
      Dim Res As Int32 = Client.WriteClientConfiguration(Characteristic, True, OpFlag())
      ...
  End Sub
  
  Private Sub btWriteUnsubsc_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btWriteUnsubsc.Click
      ...
      Dim Characteristic As wclGattCharacteristic = FCharacteristics(lvCharacteristics.SelectedItems(0).Index)
      ' Let say we prefer notifications in case if both properties set to True.
      If Characteristic.IsIndicatable And Characteristic.IsNotifiable Then
          Characteristic.IsIndicatable = False
      End If
      Dim Res As Int32 = Client.WriteClientConfiguration(Characteristic, False, OpFlag())
      ...
  End Sub
  
  Private Sub btUnsubscribe_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btUnsubscribe.Click
      ...
      Dim Characteristic As wclGattCharacteristic = FCharacteristics(lvCharacteristics.SelectedItems(0).Index)
      ' Let say we prefer notifications in case if both properties set to True.
      If Characteristic.IsIndicatable And Characteristic.IsNotifiable Then
          Characteristic.IsIndicatable = False
      End If
      Dim Res As Int32 = Client.Unsubscribe(Characteristic)
      ...
  End Sub
                        

  void CGattClientDlg::OnBnClickedButtonSubscribe()
  {
      ...
      wclGattCharacteristic Characteristic = (*it);
      // Let say we prefer notifications in case if both properties set to True.
      if (Characteristic.IsIndicatable && Characteristic.IsNotifiable)
          Characteristic.IsIndicatable = false;
      int Res = wclGattClient.Subscribe(Characteristic);
      ...
  }
  
  void CGattClientDlg::OnBnClickedButtonWriteCccdSubscribe()
  {
      ...
      wclGattCharacteristic Characteristic = (*it);
      // Let say we prefer notifications in case if both properties set to True.
      if (Characteristic.IsIndicatable && Characteristic.IsNotifiable)
          Characteristic.IsIndicatable = false;
      int Res = wclGattClient.WriteClientConfiguration(Characteristic, true, OpFlag());
      ...
  }
  
  void CGattClientDlg::OnBnClickedButtonWriteCccdUnsubscribe()
  {
      ...
      wclGattCharacteristic Characteristic = (*it);
      // Let say we prefer notifications in case if both properties set to True.
      if (Characteristic.IsIndicatable && Characteristic.IsNotifiable)
          Characteristic.IsIndicatable = false;
      int Res = wclGattClient.WriteClientConfiguration(Characteristic, false, OpFlag());
      ...
  }
  
  void CGattClientDlg::OnBnClickedButtonUnsubscribe()
  {
      ...
      wclGattCharacteristic Characteristic = (*it);
      // Let say we prefer notifications in case if both properties set to True.
      if (Characteristic.IsIndicatable && Characteristic.IsNotifiable)
          Characteristic.IsIndicatable = false;
      int Res = wclGattClient.Unsubscribe(Characteristic);
      ...
  }
                        


For more details, please refer to the GattClient sample application source code.