Bluetooth Low Energy devices expose their data through GATT — the Generic Attribute Profile. Everything an application reads or writes on a BLE peripheral lives somewhere in the GATT hierarchy: a service, a characteristic, or a descriptor underneath. The Bluetooth Framework implements the full GATT client on Windows with a single API that works across .NET, C++, and VCL.

GATT hierarchy: profile, services, characteristics, and descriptors

Bluetooth Low Energy, also known as Bluetooth Smart, is aimed at applications in healthcare, fitness, beacons, security, and home entertainment. Compared to Classic Bluetooth, it consumes significantly less power and costs less, while maintaining a similar communication range. This article describes how to connect to BLE peripheral devices and work with their GATT data using the framework.


Generic Attributes

The Generic Attributes profile defines a hierarchical data structure exposed to connected BLE devices. At the top sits a profile — a description of a use case, its roles, and general behaviors. A profile is composed of one or more services. A service is a collection of characteristics and references to other services, encapsulating part of the device's behavior.

A characteristic consists of a type (represented by a UUID), a value, a set of properties indicating which operations it 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 two roles. The client (BLE Central) reads and writes data. The server (BLE Peripheral) stores the data, accepts requests and commands, and sends responses, notifications, and indications asynchronously. The Bluetooth Framework supports the GATT client role with BlueSoleil, Microsoft, and the BLED112 USB dongle. BlueSoleil drivers and BLED112 unlock GATT on any Windows platform starting from Windows XP. Microsoft drivers unlock it from Windows 8 onwards.


UUID

A Universally Unique ID identifies every service, characteristic, and descriptor — collectively known as attributes. These IDs travel over the air so that, for example, a peripheral can tell a central which services it provides. To save air time and memory, there are two kinds of UUID.

The first is a short 16-bit UUID. The predefined Heart Rate service, for instance, has the UUID 0x180D, and one of its enclosed characteristics, Heart Rate Measurement, has 0x2A37. These are energy- and memory-efficient, but the number of unique IDs is limited by a Bluetooth SIG rule: a device can only transmit predefined UUIDs directly over the air.

The second is a long 128-bit UUID, sometimes called vendor-specific. This is what vendors use for their own custom services and characteristics. The Bluetooth Framework supports both types through the wclGattUuid structure.


Device address

To communicate with a BLE device, your application must know its 48-bit (6-byte) address, which uniquely identifies a device among peers. There are four types.

A public device address is a standard IEEE-assigned universal LAN MAC address, split into an IEEE company ID (24 most significant bits) and a company-assigned device ID (24 least significant bits). A static random device address is regenerated after each power cycle — if it changes, peer devices holding the old address can no longer reconnect. A private random device address is used by devices that want to remain private. It may change periodically so the device cannot be tracked. A resolvable private address is one that can be resolved through a pre-shared Identity Resolving Key exchanged during pairing — only trusted peers can identify the device, while everyone else sees a randomly changing address.

The Bluetooth Framework supports all four address types. It does not, however, resolve a resolvable private address back to a real device MAC — that operation requires the IRK and is out of scope for a generic receiver.


Connecting

Bluetooth radio

To communicate with a BLE device, an application first needs a working Bluetooth radio. The radio represents a Bluetooth hardware device connected to the computer — built-in or external (USB, serial). It can work with BlueSoleil and Microsoft drivers, or with the Silicon Labs BLED112 USB dongle. The Bluetooth Framework does not support BLE over Toshiba drivers.

Get the active radio object through the wclBluetoothManager class. Open it first — a good place is application initialization.

var
  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 manager is open, get a working radio object. The Bluetooth Framework always detects the Microsoft Bluetooth radio even if other drivers are installed. Do not destroy the object returned by the manager.

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

Discovery

Once you have a working radio, the next step is to discover nearby BLE devices. Depending on the driver, the procedure differs. In-app discovery works on BlueSoleil drivers and on BLED112 across any Windows platform. With the Microsoft Bluetooth driver, the situation depends on the Windows version.

On Windows 8 and on Windows 10 up to build 1607, you must first discover and pair the device through Windows UI. After pairing, you can use in-app discovery. On Windows 10 1703 and newer, in-app discovery works directly, without prior pairing. To discover devices, call the Discover method of the wclBluetoothRadio class.

For finding nearby BLE devices specifically, the wclBluetoothLeBeaconWatcher class is the better choice. The Beacons sample application demonstrates how to use it.

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('');
  Item.Data := Radio;
  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;
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("");
  Item->Data = (void*)Radio;
  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;
    }
  }
}
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"));
  }
}

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("");
  Item.Tag = Radio;
  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;
    }
  }
}
Private Sub btDiscover_Click(sender As Object, e As 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"))
    End If
  End If
End Sub

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

Private Sub Manager_OnDeviceFound(Sender As Object,
    Radio As wclBluetooth.wclBluetoothRadio, 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("")
  Item.Tag = Radio
  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
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(""));
  lvDevices.SetItemData(Item, (DWORD_PTR)Radio);
  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;
    }
  }
}

If your application needs the device name or other attributes, read them after discovery completes. The OnDiscoveringCompleted handler is the right place for that.

Connect and disconnect

After discovering devices, you can connect to one. To connect, you need its address from the discovery step and the working radio object. The connection is asynchronous: calling Connect on the wclGattClient class only starts the procedure. When the connection is established — successfully or not — the OnConnect event fires with the real result.

If the connection succeeds (the Error parameter equals WCL_E_SUCCESS), the client stays in the csConnected state. You can then read services, characteristics, and other attributes, or change the connection parameters. If the connection fails, the client returns to csDisconnected, and your application can retry or try another device.

To disconnect, call Disconnect. After disconnecting, the OnDisconnect event fires. The same event fires if the remote 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
  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
  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)
{
  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)
{
  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");
  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"));
    }
    catch (Exception ex)
    {
      MessageBox.Show(ex.Message);
    }
  }
}

void Client_OnConnect(object Sender, int Error)
{
  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"));
}

void Client_OnDisconnect(object Sender, int Reason)
{
  TraceEvent(((wclGattClient)Sender).Address, "Disconnected",
    "Reason", "0x" + Reason.ToString("X8"));
}
Private Sub btConnect_Click(sender As Object, e As EventArgs) Handles btConnect.Click
  If lvDevices.SelectedItems.Count = 0 Then
    MessageBox.Show("Select device")
  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"))
      End If
    Catch ex As Exception
      MessageBox.Show(ex.Message)
    End Try
  End If
End Sub

Private Sub Client_OnConnect(Sender As Object, [Error] As Integer) Handles Client.OnConnect
  TraceEvent(CType(Sender, wclGattClient).Address, "Connected",
    "Error", "0x" + [Error].ToString("X8"))
End Sub

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

Private Sub Client_OnDisconnect(Sender As Object, Reason As Integer) Handles Client.OnDisconnect
  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)
{
  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)
{
  TraceEvent(((CwclGattClient*)Sender)->GetAddress(), _T("Disconnected"),
    _T("Reason"), _T("0x") + IntToHex(Reason));
}

Reading data

Services

Once a connection is established, you can read the device's services. Services are collections of characteristics and relationships to other services that encapsulate part of the device's behavior. Each service has a UUID that uniquely identifies it. To read them, use the ReadServices method of the wclGattClient. Service reading is synchronous.

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"));
    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(sender As Object, e As 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"))
    Return
  End If
  If FServices Is Nothing Then Return
  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
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));
  }
}

Characteristics

After reading services, you can read the characteristics of the selected service. A characteristic encapsulates a single data point, though it may contain an array of related data. Each is identified by a pre-defined 16-bit or 128-bit UUID. Characteristics are the primary point of interaction with a BLE peripheral — you read from them, write to them, and subscribe to their change notifications.

The Bluetooth Framework exposes characteristic metadata through the wclGattCharacteristic structure. The important properties are:

  • IsBroadcastable — the characteristic can be broadcast.
  • IsReadable — the characteristic can be read.
  • IsWritable — the characteristic can be written to.
  • IsWritableWithoutResponse — the characteristic supports write without response.
  • IsSignedWritable — the characteristic supports signed write.
  • IsNotifiable — the characteristic can send value notifications.
  • IsIndicatable — the characteristic can send value indications.
  • HasExtendedProperties — the characteristic has extended properties exposed through a 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;
  }
  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");
    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"));
    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(sender As Object, e As EventArgs) Handles btGetCharacteristics.Click
  FCharacteristics = Nothing
  lvCharacteristics.Items.Clear()
  If lvServices.SelectedItems.Count = 0 Then
    MessageBox.Show("Select service")
    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"))
    Return
  End If
  If FCharacteristics Is Nothing Then Return
  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
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));
  }
}

Values

If a characteristic has IsReadable set to true, the application can read its value. The Bluetooth Framework represents the value as a raw bytes array — the meaning depends on the characteristic's UUID. Bluetooth SIG defines the meaning for the assigned numbers. Vendor-defined characteristics have meanings specified by their vendors.

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;
private void btGetCharValue_Click(object sender, EventArgs e)
{
  if (lvCharacteristics.SelectedItems.Count == 0)
  {
    MessageBox.Show("Select characteristic");
    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"));
    return;
  }
  if (Value == null) return;
  try
  {
    if (Value.Length == 0)
      MessageBox.Show("Value is empty");
    else
    {
      String Str = "";
      for (Int32 i = 0; i < Value.Length; i++)
        Str = Str + Value[i].ToString("X2");
      MessageBox.Show("Value: " + Str);
    }
  }
  finally
  {
    Value = null;
  }
}
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 any of IsWritable, IsWritableWithoutResponse, or IsSignedWritable set to true, the application can write to it.

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;
private void btSetValue_Click(object sender, EventArgs e)
{
  if (lvCharacteristics.SelectedItems.Count == 0)
  {
    MessageBox.Show("Select characteristic");
    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"));
}
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));
}

Descriptors

In addition to properties, a characteristic may have descriptors — defined attributes that describe a characteristic value. Each descriptor is identified by a UUID. Bluetooth SIG defines the assigned numbers for predefined descriptors. Vendors can use custom UUIDs for vendor-specific 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;
private void btGetDesc_Click(object sender, EventArgs e)
{
  FDescriptors = null;
  lvDescriptors.Items.Clear();
  if (lvCharacteristics.SelectedItems.Count == 0)
  {
    MessageBox.Show("Select characteristic");
    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"));
    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;
    }
  }
}
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;
    }
  }
}

Notifications

Notifications vs indications

If a characteristic has IsNotifiable or IsIndicatable set to true, it can push value changes to the application. The Bluetooth Framework supports both.

At the ATT layer, two roles exist. Client devices (Centrals) access remote resources over a BLE link. Server devices (Peripherals) hold the GATT database, control access, and provide resources to clients. The BLE standard defines two ways for a server to push data to a client: notifications and indications. Both are initiated by the server but enabled by the client.

Notifications are not acknowledged, so they are faster — but the server does not know whether the message reached the client. Indications require a confirmation from the client, so the server knows the message arrived. The ATT protocol specifies that a server cannot send two consecutive indications without a confirmation in between. That is why indications are slower.

Subscribe and unsubscribe

To subscribe to a characteristic's changes, call Subscribe first — once for each characteristic of interest. Then write the Client Configuration Descriptor for the characteristic to actually start receiving notifications. The Bluetooth Framework provides WriteClientConfiguration to do that in one call. When a notification arrives, the OnCharacteristicChanged event fires with the new value as a raw byte array and the characteristic's handle.

The Bluetooth Framework also provides convenience methods for fast subscription and unsubscription: SubscribeForNotifications and UnsubscribeFromNotifications. Internally they call Subscribe and WriteClientConfiguration as needed.

Note: DFRobot Bluno boards have a bug in the Client Configuration Descriptor implementation. The standard subscription method does not work with them. To receive notifications from such boards, 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;
private void btSubscribe_Click(object sender, EventArgs e)
{
  if (lvCharacteristics.SelectedItems.Count == 0)
  {
    MessageBox.Show("Select characteristic");
    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"));
}

private void btWriteSubsc_Click(object sender, EventArgs e)
{
  if (lvCharacteristics.SelectedItems.Count == 0)
  {
    MessageBox.Show("Select characteristic");
    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"));
}

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

private void btUnsubscribe_Click(object sender, EventArgs e)
{
  if (lvCharacteristics.SelectedItems.Count == 0)
  {
    MessageBox.Show("Select characteristic");
    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"));
}
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));
}

Error 0x000510B6

For some characteristics, calling Subscribe, Unsubscribe, or WriteClientConfiguration returns error 0x000510B6, which corresponds to WCL_E_BLUETOOTH_LE_INVALID_CHARACTERISTIC_CONFIGURATION. The error means the characteristic has both IsIndicatable and IsNotifiable set to true. You must set one of them to false before subscribing.

If you want notifications, set IsIndicatable to false. If you want indications, set IsNotifiable to false. The code below is the same as above, but with the characteristic property check added.

procedure TfmMain.btSubscribeClick(Sender: TObject);
var
  Characteristic: TwclGattCharacteristic;
  Res: Integer;
begin
  ...
  Characteristic := FCharacteristics[lvCharacteristics.Selected.Index];
  // We prefer notifications if both properties are 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];
  if Characteristic.IsIndicatable and Characteristic.IsNotifiable then
    Characteristic.IsIndicatable := False;
  Res := wclGattClient.WriteClientConfiguration(Characteristic, True, OpFlag);
  ...
end;
private void btSubscribe_Click(object sender, EventArgs e)
{
  ...
  wclGattCharacteristic Characteristic =
    FCharacteristics[lvCharacteristics.SelectedItems[0].Index];
  // We prefer notifications if both properties are 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];
  if (Characteristic.IsIndicatable && Characteristic.IsNotifiable)
    Characteristic.IsIndicatable = false;
  Int32 Res = Client.WriteClientConfiguration(Characteristic, true, OpFlag());
  ...
}
void CGattClientDlg::OnBnClickedButtonSubscribe()
{
  ...
  wclGattCharacteristic Characteristic = (*it);
  // We prefer notifications if both properties are set to True.
  if (Characteristic.IsIndicatable && Characteristic.IsNotifiable)
    Characteristic.IsIndicatable = false;
  int Res = wclGattClient.Subscribe(Characteristic);
  ...
}

void CGattClientDlg::OnBnClickedButtonWriteCccdSubscribe()
{
  ...
  wclGattCharacteristic Characteristic = (*it);
  if (Characteristic.IsIndicatable && Characteristic.IsNotifiable)
    Characteristic.IsIndicatable = false;
  int Res = wclGattClient.WriteClientConfiguration(
    Characteristic, true, OpFlag());
  ...
}

For complete working examples, refer to the GattClient sample application in the Bluetooth Framework package.


Frequently asked questions

What is GATT in Bluetooth Low Energy?
GATT (Generic Attributes) defines a hierarchical data structure exposed by BLE devices. It consists of profiles, services, characteristics, and descriptors. The GATT client (central) communicates with the GATT server (peripheral) to read and write data.
How do I discover nearby Bluetooth LE devices with the Bluetooth Framework?
Use the Discover method of the wclBluetoothRadio class. On Windows 10 1703 and newer, you can discover devices directly; on older Windows versions, you must pair the device first through Windows UI.
Why do I get error 0x000510B6 when subscribing to characteristic notifications?
This error means the characteristic has both IsIndicatable and IsNotifiable set to true. You must set one of them to false before subscribing to resolve the conflict.